mirror of https://github.com/flutter/samples.git
parent
baa1f976b2
commit
fcf6c06152
@ -0,0 +1,3 @@
|
||||
[submodule "web/slide_puzzle"]
|
||||
path = web/slide_puzzle
|
||||
url = git@github.com:kevmoo/slide_puzzle.git
|
@ -0,0 +1 @@
|
||||
Subproject commit 5ef5526acb58f9ffb6f3fb22118cbd825613dc73
|
@ -1,4 +0,0 @@
|
||||
Get the numbers in order by clicking tiles next to the open space.
|
||||
|
||||
Created by [Kevin Moore](https://twitter.com/kevmoo). Original source at
|
||||
[github.com/kevmoo/slide_puzzle](https://github.com/kevmoo/slide_puzzle).
|
@ -1,94 +0,0 @@
|
||||
Copyright (c) 2011 by Sorkin Type Co (www.sorkintype.com),
|
||||
with Reserved Font Name "Plaster".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Before Width: | Height: | Size: 726 KiB |
@ -1,33 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'src/core/puzzle_animator.dart';
|
||||
import 'src/flutter.dart';
|
||||
import 'src/puzzle_home_state.dart';
|
||||
|
||||
void main() => runApp(PuzzleApp());
|
||||
|
||||
class PuzzleApp extends StatelessWidget {
|
||||
final int rows, columns;
|
||||
|
||||
PuzzleApp({int columns = 4, int rows = 4})
|
||||
: columns = columns ?? 4,
|
||||
rows = rows ?? 4;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
title: 'Slide Puzzle',
|
||||
home: _PuzzleHome(rows, columns),
|
||||
);
|
||||
}
|
||||
|
||||
class _PuzzleHome extends StatefulWidget {
|
||||
final int _rows, _columns;
|
||||
|
||||
const _PuzzleHome(this._rows, this._columns);
|
||||
|
||||
@override
|
||||
PuzzleHomeState createState() =>
|
||||
PuzzleHomeState(PuzzleAnimator(_columns, _rows));
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'core/puzzle_proxy.dart';
|
||||
|
||||
abstract class AppState {
|
||||
PuzzleProxy get puzzle;
|
||||
|
||||
Listenable get animationNotifier;
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:math' show Point;
|
||||
|
||||
const zeroPoint = Point<double>(0, 0);
|
||||
|
||||
const _epsilon = 0.0001;
|
||||
|
||||
/// Represents a point object with a location and velocity.
|
||||
class Body {
|
||||
Point<double> _velocity;
|
||||
Point<double> _location;
|
||||
|
||||
Point<double> get velocity => _velocity;
|
||||
|
||||
Point<double> get location => _location;
|
||||
|
||||
Body({Point<double> location = zeroPoint, Point<double> velocity = zeroPoint})
|
||||
: assert(location.magnitude.isFinite),
|
||||
_location = location,
|
||||
assert(velocity.magnitude.isFinite),
|
||||
_velocity = velocity;
|
||||
|
||||
factory Body.raw(double x, double y, double vx, double vy) =>
|
||||
Body(location: Point(x, y), velocity: Point(vx, vy));
|
||||
|
||||
Body clone() => Body(location: _location, velocity: _velocity);
|
||||
|
||||
/// Add the velocity specified in [delta] to `this`.
|
||||
void kick(Point<double> delta) {
|
||||
assert(delta.magnitude.isFinite);
|
||||
_velocity = delta;
|
||||
}
|
||||
|
||||
/// [drag] must be greater than or equal to zero. It defines the percent of
|
||||
/// the previous velocity that is lost every second.
|
||||
bool animate(double seconds,
|
||||
{Point<double> force = zeroPoint,
|
||||
double drag = 0,
|
||||
double maxVelocity,
|
||||
Point<double> snapTo}) {
|
||||
assert(seconds.isFinite && seconds > 0,
|
||||
'milliseconds must be finite and > 0 (was $seconds)');
|
||||
|
||||
force ??= zeroPoint;
|
||||
assert(force.x.isFinite && force.y.isFinite, 'force must be finite');
|
||||
|
||||
drag ??= 0;
|
||||
assert(drag.isFinite && drag >= 0, 'drag must be finiate and >= 0');
|
||||
|
||||
maxVelocity ??= double.infinity;
|
||||
assert(maxVelocity > 0, 'maxVelocity must be null or > 0');
|
||||
|
||||
final dragVelocity = _velocity * (1 - drag * seconds);
|
||||
|
||||
if (_sameDirection(_velocity, dragVelocity)) {
|
||||
assert(dragVelocity.magnitude <= _velocity.magnitude,
|
||||
'Huh? $dragVelocity $_velocity');
|
||||
_velocity = dragVelocity;
|
||||
} else {
|
||||
_velocity = zeroPoint;
|
||||
}
|
||||
|
||||
// apply force to velocity
|
||||
_velocity += force * seconds;
|
||||
|
||||
// apply terminal velocity
|
||||
if (_velocity.magnitude > maxVelocity) {
|
||||
_velocity = _unitPoint(_velocity) * maxVelocity;
|
||||
}
|
||||
|
||||
// update location
|
||||
final locationDelta = _velocity * seconds;
|
||||
if (locationDelta.magnitude > _epsilon ||
|
||||
(force.magnitude * seconds) > _epsilon) {
|
||||
_location += locationDelta;
|
||||
return true;
|
||||
} else {
|
||||
if (snapTo != null && (_location.distanceTo(snapTo) < _epsilon * 2)) {
|
||||
_location = snapTo;
|
||||
}
|
||||
_velocity = zeroPoint;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Body @(${_location.x},${_location.y}) ↕(${_velocity.x},${_velocity.y})';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is Body &&
|
||||
other._location == _location &&
|
||||
other._velocity == _velocity;
|
||||
|
||||
// Since this is a mutable class, a constant value is returned for `hashCode`
|
||||
// This ensures values don't get lost in a Hashing data structure.
|
||||
// Note: this means you shouldn't use this type in most Map/Set impls.
|
||||
@override
|
||||
int get hashCode => 199;
|
||||
}
|
||||
|
||||
Point<double> _unitPoint(Point<double> source) {
|
||||
final result = source * (1 / source.magnitude);
|
||||
return Point(result.x.isNaN ? 0 : result.x, result.y.isNaN ? 0 : result.y);
|
||||
}
|
||||
|
||||
bool _sameDirection(Point a, Point b) {
|
||||
return a.x.sign == b.x.sign && a.y.sign == b.y.sign;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
class Point extends math.Point<int> {
|
||||
Point(int x, int y) : super(x, y);
|
||||
|
||||
@override
|
||||
Point operator +(math.Point<int> other) => Point(x + other.x, y + other.y);
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' show Random, max;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'point_int.dart';
|
||||
import 'util.dart';
|
||||
|
||||
part 'puzzle_simple.dart';
|
||||
|
||||
part 'puzzle_smart.dart';
|
||||
|
||||
final _rnd = Random();
|
||||
|
||||
final _spacesRegexp = RegExp(' +');
|
||||
|
||||
abstract class Puzzle {
|
||||
int get width;
|
||||
|
||||
int get length;
|
||||
|
||||
int operator [](int index);
|
||||
|
||||
int indexOf(int value);
|
||||
|
||||
List<int> get _intView;
|
||||
|
||||
List<int> _copyData();
|
||||
|
||||
Puzzle _newWithValues(List<int> values);
|
||||
|
||||
Puzzle clone();
|
||||
|
||||
int get height => length ~/ width;
|
||||
|
||||
Puzzle._();
|
||||
|
||||
factory Puzzle._raw(int width, List<int> source) {
|
||||
if (source.length <= 16) {
|
||||
return _PuzzleSmart(width, source);
|
||||
}
|
||||
return _PuzzleSimple(width, source);
|
||||
}
|
||||
|
||||
factory Puzzle.raw(int width, List<int> source) {
|
||||
requireArgument(width >= 3, 'width', 'Must be at least 3.');
|
||||
requireArgument(source.length >= 6, 'source', 'Must be at least 6 items');
|
||||
_validate(source);
|
||||
|
||||
return Puzzle._raw(width, source);
|
||||
}
|
||||
|
||||
factory Puzzle(int width, int height) =>
|
||||
Puzzle.raw(width, _randomList(width, height));
|
||||
|
||||
factory Puzzle.parse(String input) {
|
||||
final rows = LineSplitter.split(input).map((line) {
|
||||
final splits = line.trim().split(_spacesRegexp);
|
||||
return splits.map(int.parse).toList();
|
||||
}).toList();
|
||||
|
||||
return Puzzle.raw(rows.first.length, rows.expand((row) => row).toList());
|
||||
}
|
||||
|
||||
int valueAt(int x, int y) {
|
||||
final i = _getIndex(x, y);
|
||||
return this[i];
|
||||
}
|
||||
|
||||
int get tileCount => length - 1;
|
||||
|
||||
bool isCorrectPosition(int cellValue) => cellValue == this[cellValue];
|
||||
|
||||
bool get solvable => isSolvable(width, _intView);
|
||||
|
||||
Puzzle reset({List<int> source}) {
|
||||
final data = (source == null)
|
||||
? _randomizeList(width, _intView)
|
||||
: Uint8List.fromList(source);
|
||||
|
||||
if (data.length != length) {
|
||||
throw ArgumentError.value(source, 'source', 'Cannot change the size!');
|
||||
}
|
||||
_validate(data);
|
||||
if (!isSolvable(width, data)) {
|
||||
throw ArgumentError.value(source, 'source', 'Not a solvable puzzle.');
|
||||
}
|
||||
|
||||
return _newWithValues(data);
|
||||
}
|
||||
|
||||
int get incorrectTiles {
|
||||
var count = tileCount;
|
||||
for (var i = 0; i < tileCount; i++) {
|
||||
if (isCorrectPosition(i)) {
|
||||
count--;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
Point openPosition() => coordinatesOf(tileCount);
|
||||
|
||||
/// A measure of how close the puzzle is to being solved.
|
||||
///
|
||||
/// The sum of all of the distances squared `(x + y)^2 ` each tile has to move
|
||||
/// to be in the correct position.
|
||||
///
|
||||
/// `0` - you've won!
|
||||
int get fitness {
|
||||
var value = 0;
|
||||
for (var i = 0; i < tileCount; i++) {
|
||||
if (!isCorrectPosition(i)) {
|
||||
final correctColumn = i % width;
|
||||
final correctRow = i ~/ width;
|
||||
|
||||
final index = indexOf(i);
|
||||
final x = index % width;
|
||||
final y = index ~/ width;
|
||||
|
||||
final delta = (correctColumn - x).abs() + (correctRow - y).abs();
|
||||
|
||||
value += delta * delta;
|
||||
}
|
||||
}
|
||||
return value * incorrectTiles;
|
||||
}
|
||||
|
||||
Puzzle clickRandom({bool vertical}) {
|
||||
final clickable = clickableValues(vertical: vertical).toList();
|
||||
return clickValue(clickable[_rnd.nextInt(clickable.length)]);
|
||||
}
|
||||
|
||||
Iterable<Puzzle> allMovable() =>
|
||||
(clickableValues()..shuffle(_rnd)).map(_clickValue);
|
||||
|
||||
List<int> clickableValues({bool vertical}) {
|
||||
final open = openPosition();
|
||||
final doRow = vertical == null || vertical == false;
|
||||
final doColumn = vertical == null || vertical;
|
||||
|
||||
final values =
|
||||
Uint8List((doRow ? (width - 1) : 0) + (doColumn ? (height - 1) : 0));
|
||||
|
||||
var index = 0;
|
||||
|
||||
if (doRow) {
|
||||
for (var x = 0; x < width; x++) {
|
||||
if (x != open.x) {
|
||||
values[index++] = valueAt(x, open.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doColumn) {
|
||||
for (var y = 0; y < height; y++) {
|
||||
if (y != open.y) {
|
||||
values[index++] = valueAt(open.x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
bool _movable(int tileValue) {
|
||||
if (tileValue == tileCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final target = coordinatesOf(tileValue);
|
||||
final lastCoord = openPosition();
|
||||
if (lastCoord.x != target.x && lastCoord.y != target.y) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Puzzle clickValue(int tileValue) {
|
||||
if (!_movable(tileValue)) {
|
||||
return null;
|
||||
}
|
||||
return _clickValue(tileValue);
|
||||
}
|
||||
|
||||
Puzzle _clickValue(int tileValue) {
|
||||
assert(_movable(tileValue));
|
||||
final target = coordinatesOf(tileValue);
|
||||
|
||||
final newStore = _copyData();
|
||||
|
||||
_shift(newStore, target.x, target.y);
|
||||
return _newWithValues(newStore);
|
||||
}
|
||||
|
||||
void _shift(List<int> source, int targetX, int targetY) {
|
||||
final lastCoord = openPosition();
|
||||
final deltaX = lastCoord.x - targetX;
|
||||
final deltaY = lastCoord.y - targetY;
|
||||
|
||||
if ((deltaX.abs() + deltaY.abs()) > 1) {
|
||||
final shiftPointX = targetX + deltaX.sign;
|
||||
final shiftPointY = targetY + deltaY.sign;
|
||||
_shift(source, shiftPointX, shiftPointY);
|
||||
_staticSwap(source, targetX, targetY, shiftPointX, shiftPointY);
|
||||
} else {
|
||||
_staticSwap(source, lastCoord.x, lastCoord.y, targetX, targetY);
|
||||
}
|
||||
}
|
||||
|
||||
void _staticSwap(List<int> source, int ax, int ay, int bx, int by) {
|
||||
final aIndex = ax + ay * width;
|
||||
final aValue = source[aIndex];
|
||||
final bIndex = bx + by * width;
|
||||
|
||||
source[aIndex] = source[bIndex];
|
||||
source[bIndex] = aValue;
|
||||
}
|
||||
|
||||
Point coordinatesOf(int value) {
|
||||
final index = indexOf(value);
|
||||
final x = index % width;
|
||||
final y = index ~/ width;
|
||||
assert(_getIndex(x, y) == index);
|
||||
return Point(x, y);
|
||||
}
|
||||
|
||||
int _getIndex(int x, int y) {
|
||||
assert(x >= 0 && x < width);
|
||||
assert(y >= 0 && y < height);
|
||||
return x + y * width;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _toString();
|
||||
|
||||
String _toString() {
|
||||
final grid = List<List<String>>.generate(
|
||||
height,
|
||||
(row) => List<String>.generate(
|
||||
width, (col) => valueAt(col, row).toString()));
|
||||
|
||||
final longestLength =
|
||||
grid.expand((r) => r).fold(0, (int l, cell) => max(l, cell.length));
|
||||
|
||||
return grid
|
||||
.map((r) => r.map((v) => v.padLeft(longestLength)).join(' '))
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _randomList(int width, int height) => _randomizeList(
|
||||
width, List<int>.generate(width * height, (i) => i, growable: false));
|
||||
|
||||
Uint8List _randomizeList(int width, List<int> existing) {
|
||||
final copy = Uint8List.fromList(existing);
|
||||
do {
|
||||
copy.shuffle(_rnd);
|
||||
} while (!isSolvable(width, copy) ||
|
||||
copy.any((v) => copy[v] == v || copy[v] == existing[v]));
|
||||
return copy;
|
||||
}
|
||||
|
||||
void _validate(List<int> source) {
|
||||
for (var i = 0; i < source.length; i++) {
|
||||
requireArgument(
|
||||
source.contains(i),
|
||||
'source',
|
||||
'Must contain each number from 0 to `length - 1` '
|
||||
'once and only once.');
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:math' show Point;
|
||||
|
||||
enum PuzzleEvent { click, random, reset, noop }
|
||||
|
||||
abstract class PuzzleProxy {
|
||||
int get width;
|
||||
|
||||
int get height;
|
||||
|
||||
int get length;
|
||||
|
||||
bool get solved;
|
||||
|
||||
void clickOrShake(int tileValue);
|
||||
|
||||
int get tileCount;
|
||||
|
||||
Point<double> location(int index);
|
||||
|
||||
bool isCorrectPosition(int value);
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
part of 'puzzle.dart';
|
||||
|
||||
class _PuzzleSimple extends Puzzle {
|
||||
@override
|
||||
final int width;
|
||||
final Uint8List _source;
|
||||
|
||||
_PuzzleSimple(this.width, List<int> source)
|
||||
: _source = UnmodifiableUint8ListView(Uint8List.fromList(source)),
|
||||
super._();
|
||||
|
||||
@override
|
||||
int indexOf(int value) => _source.indexOf(value);
|
||||
|
||||
@override
|
||||
List<int> get _intView => _source;
|
||||
|
||||
@override
|
||||
List<int> _copyData() => Uint8List.fromList(_source);
|
||||
|
||||
@override
|
||||
Puzzle _newWithValues(List<int> values) => _PuzzleSimple(width, values);
|
||||
|
||||
@override
|
||||
int operator [](int index) => _source[index];
|
||||
|
||||
@override
|
||||
Puzzle clone() => _PuzzleSimple(width, _source);
|
||||
|
||||
@override
|
||||
int get length => _source.length;
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is Puzzle &&
|
||||
other.width == width &&
|
||||
other.length == _source.length) {
|
||||
for (var i = 0; i < _source.length; i++) {
|
||||
if (other[i] != _source[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var v = 0;
|
||||
for (var i = 0; i < _source.length; i++) {
|
||||
v = (v << 2) + _source[i];
|
||||
}
|
||||
v += v << 3;
|
||||
v ^= v >> 11;
|
||||
v += v << 15;
|
||||
return v;
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
part of 'puzzle.dart';
|
||||
|
||||
mixin _SliceListMixin on ListMixin<int> {
|
||||
static const _bitsPerValue = 4;
|
||||
static const _maxShift = _valuesPerCell - 1;
|
||||
|
||||
static const _bitsPerCell = 32;
|
||||
static const _valuesPerCell = _bitsPerCell ~/ _bitsPerValue;
|
||||
static const _valueMask = (1 << _bitsPerValue) - 1;
|
||||
|
||||
Uint32List get _data;
|
||||
|
||||
int _cellValue(int index) => _data[index ~/ _valuesPerCell];
|
||||
|
||||
@override
|
||||
int operator [](int index) {
|
||||
/*
|
||||
final bigValue = _data[index ~/ _valuesPerCell];
|
||||
final shiftedValue =
|
||||
bigValue >> (_maxShift - (index % _valuesPerCell)) * _bitsPerValue;
|
||||
final flattenedValue = shiftedValue & _valueMask;
|
||||
return flattenedValue;
|
||||
*/
|
||||
return (_cellValue(index) >>
|
||||
(_maxShift - (index % _valuesPerCell)) * _bitsPerValue) &
|
||||
_valueMask;
|
||||
}
|
||||
|
||||
@override
|
||||
int indexOf(Object value, [int start]) {
|
||||
for (var i = 0; i < _data.length; i++) {
|
||||
final cellValue = _data[i];
|
||||
for (var j = 0; j < _valuesPerCell; j++) {
|
||||
final option =
|
||||
(cellValue >> (_maxShift - j) * _bitsPerValue) & _valueMask;
|
||||
|
||||
if (value == option) {
|
||||
final k = i * _valuesPerCell + j;
|
||||
if (k < length && (start == null || k >= start)) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@override
|
||||
set length(int value) => throw UnsupportedError('immutable, yo!');
|
||||
}
|
||||
|
||||
class _SliceList with ListMixin<int>, _SliceListMixin {
|
||||
@override
|
||||
final Uint32List _data;
|
||||
|
||||
@override
|
||||
final int length;
|
||||
|
||||
_SliceList(this.length, this._data);
|
||||
|
||||
@override
|
||||
void operator []=(int index, int value) {
|
||||
var cellValue = _cellValue(index);
|
||||
|
||||
// clear out the target bits in `cellValue`
|
||||
|
||||
final sharedShift =
|
||||
(_SliceListMixin._maxShift - (index % _SliceListMixin._valuesPerCell)) *
|
||||
_SliceListMixin._bitsPerValue;
|
||||
|
||||
final wipeout = _SliceListMixin._valueMask << sharedShift;
|
||||
|
||||
cellValue &= ~wipeout;
|
||||
|
||||
final newShiftedValue = value << sharedShift;
|
||||
|
||||
cellValue |= newShiftedValue;
|
||||
|
||||
_data[index ~/ _SliceListMixin._valuesPerCell] = cellValue;
|
||||
}
|
||||
}
|
||||
|
||||
class _PuzzleSmart extends Puzzle with ListMixin<int>, _SliceListMixin {
|
||||
static const _bitsPerValue = 4;
|
||||
static const _maxShift = _valuesPerCell - 1;
|
||||
|
||||
static const _bitsPerCell = 32;
|
||||
static const _valuesPerCell = _bitsPerCell ~/ _bitsPerValue;
|
||||
static const _valueMask = (1 << _bitsPerValue) - 1;
|
||||
|
||||
@override
|
||||
final Uint32List _data;
|
||||
|
||||
@override
|
||||
final int width;
|
||||
|
||||
@override
|
||||
final int length;
|
||||
|
||||
int _fitnessCache;
|
||||
|
||||
@override
|
||||
int get fitness => _fitnessCache ??= super.fitness;
|
||||
|
||||
_PuzzleSmart(this.width, List<int> source)
|
||||
: length = source.length,
|
||||
_data = _create(source),
|
||||
super._();
|
||||
|
||||
@override
|
||||
void operator []=(int index, int value) =>
|
||||
throw UnsupportedError('immutable, yo!');
|
||||
|
||||
@override
|
||||
List<int> get _intView => this;
|
||||
|
||||
@override
|
||||
List<int> _copyData() => _SliceList(length, Uint32List.fromList(_data));
|
||||
|
||||
@override
|
||||
Puzzle _newWithValues(List<int> values) => _PuzzleSmart(width, values);
|
||||
|
||||
@override
|
||||
Puzzle clone() => _PuzzleSmart(width, this);
|
||||
|
||||
@override
|
||||
String toString() => _toString();
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (other is _PuzzleSmart &&
|
||||
other.width == width &&
|
||||
other._data.length == _data.length) {
|
||||
for (var i = 0; i < _data.length; i++) {
|
||||
if (other._data[i] != _data[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (other is Puzzle && other.width == width && other.length == length) {
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (other[i] != this[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var v = 0;
|
||||
for (var i = 0; i < _data.length; i++) {
|
||||
v = (v << 2) + _data[i];
|
||||
}
|
||||
v += v << 3;
|
||||
v ^= v >> 11;
|
||||
v += v << 15;
|
||||
return v;
|
||||
}
|
||||
|
||||
static Uint32List _create(List<int> source) {
|
||||
if (source is _SliceList) {
|
||||
return source._data;
|
||||
}
|
||||
|
||||
final data = Uint32List((source.length / _valuesPerCell).ceil());
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var value = 0;
|
||||
for (var j = 0; j < _valuesPerCell; j++) {
|
||||
final k = i * _valuesPerCell + j;
|
||||
if (k < source.length) {
|
||||
// shift the value over 4 bits for item 0, 3 for item 2, etc
|
||||
final sourceValue = source[k] << ((_maxShift - j) * _bitsPerValue);
|
||||
value |= sourceValue;
|
||||
}
|
||||
}
|
||||
data[i] = value;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
void requireArgument(bool truth, String argName, [String message]) {
|
||||
if (!truth) {
|
||||
if (message == null || message.isEmpty) {
|
||||
message = 'value was invalid';
|
||||
}
|
||||
throw ArgumentError('`$argName` - $message');
|
||||
}
|
||||
}
|
||||
|
||||
void requireArgumentNotNull(argument, String argName) {
|
||||
if (argument == null) {
|
||||
throw ArgumentError.notNull(argName);
|
||||
}
|
||||
}
|
||||
|
||||
// Logic from
|
||||
// https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html
|
||||
// Used with gratitude!
|
||||
bool isSolvable(int width, List<int> list) {
|
||||
final height = list.length ~/ width;
|
||||
assert(width * height == list.length);
|
||||
final inversions = _countInversions(list);
|
||||
|
||||
if (width.isOdd) {
|
||||
return inversions.isEven;
|
||||
}
|
||||
|
||||
final blankRow = list.indexOf(list.length - 1) ~/ width;
|
||||
|
||||
if ((height - blankRow).isEven) {
|
||||
return inversions.isOdd;
|
||||
} else {
|
||||
return inversions.isEven;
|
||||
}
|
||||
}
|
||||
|
||||
int _countInversions(List<int> items) {
|
||||
final tileCount = items.length - 1;
|
||||
var score = 0;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final value = items[i];
|
||||
if (value == tileCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var j = i + 1; j < items.length; j++) {
|
||||
final v = items[j];
|
||||
if (v != tileCount && v < value) {
|
||||
score++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
export 'package:flutter/material.dart';
|
||||
export 'package:flutter/scheduler.dart' show Ticker;
|
@ -1,17 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
abstract class PuzzleControls implements Listenable {
|
||||
void reset();
|
||||
|
||||
int get clickCount;
|
||||
|
||||
int get incorrectTiles;
|
||||
|
||||
bool get autoPlay;
|
||||
|
||||
void Function(bool newValue) get setAutoPlayFunction;
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_proxy.dart';
|
||||
import 'flutter.dart';
|
||||
|
||||
class PuzzleFlowDelegate extends FlowDelegate {
|
||||
final Size _tileSize;
|
||||
final PuzzleProxy _puzzleProxy;
|
||||
|
||||
PuzzleFlowDelegate(this._tileSize, this._puzzleProxy, Listenable repaint)
|
||||
: super(repaint: repaint);
|
||||
|
||||
@override
|
||||
Size getSize(BoxConstraints constraints) => Size(
|
||||
_tileSize.width * _puzzleProxy.width,
|
||||
_tileSize.height * _puzzleProxy.height);
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) =>
|
||||
BoxConstraints.tight(_tileSize);
|
||||
|
||||
@override
|
||||
void paintChildren(FlowPaintingContext context) {
|
||||
for (var i = 0; i < _puzzleProxy.length; i++) {
|
||||
final tileLocation = _puzzleProxy.location(i);
|
||||
context.paintChild(
|
||||
i,
|
||||
transform: Matrix4.translationValues(
|
||||
tileLocation.x * _tileSize.width,
|
||||
tileLocation.y * _tileSize.height,
|
||||
i.toDouble(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant PuzzleFlowDelegate oldDelegate) =>
|
||||
_tileSize != oldDelegate._tileSize ||
|
||||
!identical(oldDelegate._puzzleProxy, _puzzleProxy);
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'app_state.dart';
|
||||
import 'core/puzzle_animator.dart';
|
||||
import 'core/puzzle_proxy.dart';
|
||||
import 'flutter.dart';
|
||||
import 'puzzle_controls.dart';
|
||||
import 'puzzle_flow_delegate.dart';
|
||||
import 'shared_theme.dart';
|
||||
import 'themes.dart';
|
||||
import 'value_tab_controller.dart';
|
||||
|
||||
class _PuzzleControls extends ChangeNotifier implements PuzzleControls {
|
||||
final PuzzleHomeState _parent;
|
||||
|
||||
_PuzzleControls(this._parent);
|
||||
|
||||
@override
|
||||
bool get autoPlay => _parent._autoPlay;
|
||||
|
||||
void _notify() => notifyListeners();
|
||||
|
||||
@override
|
||||
void Function(bool newValue) get setAutoPlayFunction {
|
||||
if (_parent.puzzle.solved) {
|
||||
return null;
|
||||
}
|
||||
return _parent._setAutoPlay;
|
||||
}
|
||||
|
||||
@override
|
||||
int get clickCount => _parent.puzzle.clickCount;
|
||||
|
||||
@override
|
||||
int get incorrectTiles => _parent.puzzle.incorrectTiles;
|
||||
|
||||
@override
|
||||
void reset() => _parent.puzzle.reset();
|
||||
}
|
||||
|
||||
class PuzzleHomeState extends State
|
||||
with SingleTickerProviderStateMixin, AppState {
|
||||
@override
|
||||
final PuzzleAnimator puzzle;
|
||||
|
||||
@override
|
||||
final _AnimationNotifier animationNotifier = _AnimationNotifier();
|
||||
|
||||
Duration _tickerTimeSinceLastEvent = Duration.zero;
|
||||
Ticker _ticker;
|
||||
Duration _lastElapsed;
|
||||
StreamSubscription _puzzleEventSubscription;
|
||||
|
||||
bool _autoPlay = false;
|
||||
_PuzzleControls _autoPlayListenable;
|
||||
|
||||
PuzzleHomeState(this.puzzle) {
|
||||
_puzzleEventSubscription = puzzle.onEvent.listen(_onPuzzleEvent);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_autoPlayListenable = _PuzzleControls(this);
|
||||
_ticker ??= createTicker(_onTick);
|
||||
_ensureTicking();
|
||||
}
|
||||
|
||||
void _setAutoPlay(bool newValue) {
|
||||
if (newValue != _autoPlay) {
|
||||
setState(() {
|
||||
// Only allow enabling autoPlay if the puzzle is not solved
|
||||
_autoPlayListenable._notify();
|
||||
_autoPlay = newValue && !puzzle.solved;
|
||||
if (_autoPlay) {
|
||||
_ensureTicking();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MultiProvider(
|
||||
providers: [
|
||||
Provider<AppState>.value(value: this),
|
||||
ListenableProvider<PuzzleControls>.value(
|
||||
listenable: _autoPlayListenable,
|
||||
)
|
||||
],
|
||||
child: Material(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
const SizedBox.expand(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Image(
|
||||
image: AssetImage('asset/seattle.jpg'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const LayoutBuilder(builder: _doBuild),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationNotifier.dispose();
|
||||
_ticker?.dispose();
|
||||
_autoPlayListenable?.dispose();
|
||||
_puzzleEventSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPuzzleEvent(PuzzleEvent e) {
|
||||
_autoPlayListenable._notify();
|
||||
if (e != PuzzleEvent.random) {
|
||||
_setAutoPlay(false);
|
||||
}
|
||||
_tickerTimeSinceLastEvent = Duration.zero;
|
||||
_ensureTicking();
|
||||
setState(() {
|
||||
// noop
|
||||
});
|
||||
}
|
||||
|
||||
void _ensureTicking() {
|
||||
if (!_ticker.isTicking) {
|
||||
_ticker.start();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTick(Duration elapsed) {
|
||||
if (elapsed == Duration.zero) {
|
||||
_lastElapsed = elapsed;
|
||||
}
|
||||
final delta = elapsed - _lastElapsed;
|
||||
_lastElapsed = elapsed;
|
||||
|
||||
if (delta.inMilliseconds <= 0) {
|
||||
// `_delta` may be negative or zero if `elapsed` is zero (first tick)
|
||||
// or during a restart. Just ignore this case.
|
||||
return;
|
||||
}
|
||||
|
||||
_tickerTimeSinceLastEvent += delta;
|
||||
puzzle.update(delta > _maxFrameDuration ? _maxFrameDuration : delta);
|
||||
|
||||
if (!puzzle.stable) {
|
||||
animationNotifier.animate();
|
||||
} else {
|
||||
if (!_autoPlay) {
|
||||
_ticker.stop();
|
||||
_lastElapsed = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (_autoPlay &&
|
||||
_tickerTimeSinceLastEvent > const Duration(milliseconds: 200)) {
|
||||
puzzle.playRandom();
|
||||
|
||||
if (puzzle.solved) {
|
||||
_setAutoPlay(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimationNotifier extends ChangeNotifier {
|
||||
void animate() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
const _maxFrameDuration = Duration(milliseconds: 34);
|
||||
|
||||
Widget _updateConstraints(
|
||||
BoxConstraints constraints, Widget Function(bool small) builder) {
|
||||
const _smallWidth = 580;
|
||||
|
||||
final constraintWidth =
|
||||
constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0;
|
||||
|
||||
final constraintHeight =
|
||||
constraints.hasBoundedHeight ? constraints.maxHeight : 1000.0;
|
||||
|
||||
return builder(constraintWidth < _smallWidth || constraintHeight < 690);
|
||||
}
|
||||
|
||||
Widget _doBuild(BuildContext _, BoxConstraints constraints) =>
|
||||
_updateConstraints(constraints, _doBuildCore);
|
||||
|
||||
Widget _doBuildCore(bool small) => ValueTabController<SharedTheme>(
|
||||
values: themes,
|
||||
child: Consumer<SharedTheme>(
|
||||
builder: (_, theme, __) => AnimatedContainer(
|
||||
duration: puzzleAnimationDuration,
|
||||
color: theme.puzzleThemeBackground,
|
||||
child: Center(
|
||||
child: theme.styledWrapper(
|
||||
small,
|
||||
SizedBox(
|
||||
width: 580,
|
||||
child: Consumer<AppState>(
|
||||
builder: (context, appState, _) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.black26,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: TabBar(
|
||||
controller: ValueTabController.of(context),
|
||||
labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12),
|
||||
labelColor: theme.puzzleAccentColor,
|
||||
indicatorColor: theme.puzzleAccentColor,
|
||||
indicatorWeight: 1.5,
|
||||
unselectedLabelColor: Colors.black.withOpacity(0.6),
|
||||
tabs: themes
|
||||
.map((st) => Text(
|
||||
st.name.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Flow(
|
||||
delegate: PuzzleFlowDelegate(
|
||||
small ? const Size(90, 90) : const Size(140, 140),
|
||||
appState.puzzle,
|
||||
appState.animationNotifier,
|
||||
),
|
||||
children: List<Widget>.generate(
|
||||
appState.puzzle.length,
|
||||
(i) => theme.tileButtonCore(
|
||||
i, appState.puzzle, small),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.black26, width: 1),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
bottom: 6,
|
||||
top: 2,
|
||||
right: 10,
|
||||
),
|
||||
child: Consumer<PuzzleControls>(
|
||||
builder: (_, controls, __) =>
|
||||
Row(children: theme.bottomControls(controls)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
@ -1,116 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_proxy.dart';
|
||||
import 'flutter.dart';
|
||||
import 'puzzle_controls.dart';
|
||||
import 'widgets/material_interior_alt.dart';
|
||||
|
||||
final puzzleAnimationDuration = kThemeAnimationDuration * 3;
|
||||
|
||||
abstract class SharedTheme {
|
||||
const SharedTheme();
|
||||
|
||||
String get name;
|
||||
|
||||
Color get puzzleThemeBackground;
|
||||
|
||||
RoundedRectangleBorder puzzleBorder(bool small);
|
||||
|
||||
Color get puzzleBackgroundColor;
|
||||
|
||||
Color get puzzleAccentColor;
|
||||
|
||||
EdgeInsetsGeometry tilePadding(PuzzleProxy puzzle) => const EdgeInsets.all(6);
|
||||
|
||||
Widget tileButton(int i, PuzzleProxy puzzle, bool small);
|
||||
|
||||
Ink createInk(
|
||||
Widget child, {
|
||||
DecorationImage image,
|
||||
EdgeInsetsGeometry padding,
|
||||
}) =>
|
||||
Ink(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
image: image,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
Widget createButton(
|
||||
PuzzleProxy puzzle,
|
||||
bool small,
|
||||
int tileValue,
|
||||
Widget content, {
|
||||
Color color,
|
||||
RoundedRectangleBorder shape,
|
||||
}) =>
|
||||
AnimatedContainer(
|
||||
duration: puzzleAnimationDuration,
|
||||
padding: tilePadding(puzzle),
|
||||
child: RaisedButton(
|
||||
elevation: 4,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
animationDuration: puzzleAnimationDuration,
|
||||
onPressed: () => puzzle.clickOrShake(tileValue),
|
||||
shape: shape ?? puzzleBorder(small),
|
||||
padding: const EdgeInsets.symmetric(),
|
||||
child: content,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
|
||||
// Thought about using AnimatedContainer here, but it causes some weird
|
||||
// resizing behavior
|
||||
Widget styledWrapper(bool small, Widget child) => MaterialInterior(
|
||||
duration: puzzleAnimationDuration,
|
||||
shape: puzzleBorder(small),
|
||||
color: puzzleBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
|
||||
TextStyle get _infoStyle => TextStyle(
|
||||
color: puzzleAccentColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
List<Widget> bottomControls(PuzzleControls controls) => <Widget>[
|
||||
IconButton(
|
||||
onPressed: controls.reset,
|
||||
icon: Icon(Icons.refresh, color: puzzleAccentColor),
|
||||
),
|
||||
Checkbox(
|
||||
value: controls.autoPlay,
|
||||
onChanged: controls.setAutoPlayFunction,
|
||||
activeColor: puzzleAccentColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Container(),
|
||||
),
|
||||
Text(
|
||||
controls.clickCount.toString(),
|
||||
textAlign: TextAlign.right,
|
||||
style: _infoStyle,
|
||||
),
|
||||
const Text(' Moves'),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
controls.incorrectTiles.toString(),
|
||||
textAlign: TextAlign.right,
|
||||
style: _infoStyle,
|
||||
),
|
||||
),
|
||||
const Text(' Tiles left ')
|
||||
];
|
||||
|
||||
Widget tileButtonCore(int i, PuzzleProxy puzzle, bool small) {
|
||||
if (i == puzzle.tileCount && !puzzle.solved) {
|
||||
return const Center();
|
||||
}
|
||||
|
||||
return tileButton(i, puzzle, small);
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_proxy.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
|
||||
const _yellowIsh = Color.fromARGB(255, 248, 244, 233);
|
||||
const _chocolate = Color.fromARGB(255, 66, 66, 68);
|
||||
const _orangeIsh = Color.fromARGB(255, 224, 107, 83);
|
||||
|
||||
class ThemePlaster extends SharedTheme {
|
||||
@override
|
||||
String get name => 'Plaster';
|
||||
|
||||
const ThemePlaster();
|
||||
|
||||
@override
|
||||
Color get puzzleThemeBackground => _chocolate;
|
||||
|
||||
@override
|
||||
Color get puzzleBackgroundColor => _yellowIsh;
|
||||
|
||||
@override
|
||||
Color get puzzleAccentColor => _orangeIsh;
|
||||
|
||||
@override
|
||||
RoundedRectangleBorder puzzleBorder(bool small) => RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
color: Color.fromARGB(255, 103, 103, 105),
|
||||
width: 8,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(small ? 10 : 18),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget tileButton(int i, PuzzleProxy puzzle, bool small) {
|
||||
final correctColumn = i % puzzle.width;
|
||||
final correctRow = i ~/ puzzle.width;
|
||||
|
||||
final primary = (correctColumn + correctRow).isEven;
|
||||
|
||||
if (i == puzzle.tileCount) {
|
||||
assert(puzzle.solved);
|
||||
return Center(
|
||||
child: Icon(
|
||||
Icons.thumb_up,
|
||||
size: small ? 50 : 72,
|
||||
color: _orangeIsh,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final content = Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: primary ? _yellowIsh : _chocolate,
|
||||
fontFamily: 'Plaster',
|
||||
fontSize: small ? 40 : 77,
|
||||
),
|
||||
);
|
||||
|
||||
return createButton(
|
||||
puzzle,
|
||||
small,
|
||||
i,
|
||||
content,
|
||||
color: primary ? _orangeIsh : _yellowIsh,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: primary ? _chocolate : _orangeIsh, width: 5),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_proxy.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
import 'widgets/decoration_image_plus.dart';
|
||||
|
||||
class ThemeSeattle extends SharedTheme {
|
||||
@override
|
||||
String get name => 'Seattle';
|
||||
|
||||
const ThemeSeattle();
|
||||
|
||||
@override
|
||||
Color get puzzleThemeBackground => const Color.fromARGB(153, 90, 135, 170);
|
||||
|
||||
@override
|
||||
Color get puzzleBackgroundColor => Colors.white70;
|
||||
|
||||
@override
|
||||
Color get puzzleAccentColor => const Color(0xff000579f);
|
||||
|
||||
@override
|
||||
RoundedRectangleBorder puzzleBorder(bool small) =>
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(1),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry tilePadding(PuzzleProxy puzzle) =>
|
||||
puzzle.solved ? const EdgeInsets.all(1) : const EdgeInsets.all(4);
|
||||
|
||||
@override
|
||||
Widget tileButton(int i, PuzzleProxy puzzle, bool small) {
|
||||
if (i == puzzle.tileCount && !puzzle.solved) {
|
||||
assert(puzzle.solved);
|
||||
}
|
||||
|
||||
final decorationImage = DecorationImagePlus(
|
||||
puzzleWidth: puzzle.width,
|
||||
puzzleHeight: puzzle.height,
|
||||
pieceIndex: i,
|
||||
fit: BoxFit.cover,
|
||||
image: const AssetImage('asset/seattle.jpg'));
|
||||
|
||||
final correctPosition = puzzle.isCorrectPosition(i);
|
||||
final content = createInk(
|
||||
puzzle.solved
|
||||
? const Center()
|
||||
: Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: const CircleBorder(),
|
||||
color: correctPosition ? Colors.black38 : Colors.white54,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
color: correctPosition ? Colors.white : Colors.black,
|
||||
fontSize: small ? 25 : 42,
|
||||
),
|
||||
),
|
||||
),
|
||||
image: decorationImage,
|
||||
padding: EdgeInsets.all(small ? 20 : 32),
|
||||
);
|
||||
|
||||
return createButton(puzzle, small, i, content);
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'core/puzzle_proxy.dart';
|
||||
import 'flutter.dart';
|
||||
import 'shared_theme.dart';
|
||||
|
||||
const _accentBlue = Color(0xff000579e);
|
||||
|
||||
class ThemeSimple extends SharedTheme {
|
||||
@override
|
||||
String get name => 'Simple';
|
||||
|
||||
const ThemeSimple();
|
||||
|
||||
@override
|
||||
Color get puzzleThemeBackground => Colors.white;
|
||||
|
||||
@override
|
||||
Color get puzzleBackgroundColor => Colors.white70;
|
||||
|
||||
@override
|
||||
Color get puzzleAccentColor => _accentBlue;
|
||||
|
||||
@override
|
||||
RoundedRectangleBorder puzzleBorder(bool small) =>
|
||||
const RoundedRectangleBorder(
|
||||
side: BorderSide(color: Colors.black26, width: 1),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(4),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget tileButton(int i, PuzzleProxy puzzle, bool small) {
|
||||
if (i == puzzle.tileCount) {
|
||||
assert(puzzle.solved);
|
||||
return const Center(
|
||||
child: Icon(
|
||||
Icons.thumb_up,
|
||||
size: 72,
|
||||
color: _accentBlue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final correctPosition = puzzle.isCorrectPosition(i);
|
||||
|
||||
final content = createInk(
|
||||
Center(
|
||||
child: Text(
|
||||
(i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: correctPosition ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: small ? 30 : 49,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return createButton(
|
||||
puzzle,
|
||||
small,
|
||||
i,
|
||||
content,
|
||||
color: const Color.fromARGB(255, 13, 87, 155),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'theme_plaster.dart';
|
||||
import 'theme_seattle.dart';
|
||||
import 'theme_simple.dart';
|
||||
|
||||
const themes = [
|
||||
ThemeSimple(),
|
||||
ThemeSeattle(),
|
||||
ThemePlaster(),
|
||||
];
|
@ -1,92 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ValueTabController<T> extends StatefulWidget {
|
||||
/// Creates a default tab controller for the given [child] widget.
|
||||
const ValueTabController({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.values,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
|
||||
///
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
final List<T> values;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// Typical usage:
|
||||
///
|
||||
/// ```dart
|
||||
/// TabController controller = DefaultTabBarController.of(context);
|
||||
/// ```
|
||||
static TabController of(BuildContext context) {
|
||||
final scope = context.inheritFromWidgetOfExactType(_ValueTabControllerScope)
|
||||
as _ValueTabControllerScope;
|
||||
return scope?.controller;
|
||||
}
|
||||
|
||||
@override
|
||||
_ValueTabControllerState<T> createState() => _ValueTabControllerState<T>();
|
||||
}
|
||||
|
||||
class _ValueTabControllerState<T> extends State<ValueTabController<T>>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _notifier = ValueNotifier<T>(null);
|
||||
|
||||
TabController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TabController(
|
||||
vsync: this,
|
||||
length: widget.values.length,
|
||||
initialIndex: 0,
|
||||
);
|
||||
|
||||
_notifier.value = widget.values.first;
|
||||
|
||||
_controller.addListener(() {
|
||||
_notifier.value = widget.values[_controller.index];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _ValueTabControllerScope(
|
||||
controller: _controller,
|
||||
enabled: TickerMode.of(context),
|
||||
child: ValueListenableProvider.value(
|
||||
valueListenable: _notifier,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ValueTabControllerScope extends InheritedWidget {
|
||||
const _ValueTabControllerScope(
|
||||
{Key key, this.controller, this.enabled, Widget child})
|
||||
: super(key: key, child: child);
|
||||
|
||||
final TabController controller;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_ValueTabControllerScope old) =>
|
||||
enabled != old.enabled || controller != old.controller;
|
||||
}
|
@ -1,334 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: omit_local_variable_types, annotate_overrides
|
||||
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import '../flutter.dart';
|
||||
|
||||
// A model on top of DecorationImage that supports slicing up the source image
|
||||
// efficiently to draw it as tiles in the puzzle game
|
||||
@immutable
|
||||
class DecorationImagePlus implements DecorationImage {
|
||||
final int puzzleWidth, puzzleHeight, pieceIndex;
|
||||
|
||||
/// Creates an image to show in a [BoxDecoration].
|
||||
///
|
||||
/// The [image], [alignment], [repeat], and [matchTextDirection] arguments
|
||||
/// must not be null.
|
||||
const DecorationImagePlus({
|
||||
@required this.image,
|
||||
@required this.puzzleWidth,
|
||||
@required this.puzzleHeight,
|
||||
@required this.pieceIndex,
|
||||
this.colorFilter,
|
||||
this.fit,
|
||||
this.alignment = Alignment.center,
|
||||
this.centerSlice,
|
||||
this.repeat = ImageRepeat.noRepeat,
|
||||
this.matchTextDirection = false,
|
||||
}) : assert(image != null),
|
||||
assert(alignment != null),
|
||||
assert(repeat != null),
|
||||
assert(matchTextDirection != null),
|
||||
assert(puzzleHeight > 1 &&
|
||||
puzzleHeight > 1 &&
|
||||
pieceIndex >= 0 &&
|
||||
pieceIndex < (puzzleHeight * puzzleWidth));
|
||||
|
||||
/// The image to be painted into the decoration.
|
||||
///
|
||||
/// Typically this will be an [AssetImage] (for an image shipped with the
|
||||
/// application) or a [NetworkImage] (for an image obtained from the network).
|
||||
final ImageProvider image;
|
||||
|
||||
/// A color filter to apply to the image before painting it.
|
||||
final ColorFilter colorFilter;
|
||||
|
||||
/// How the image should be inscribed into the box.
|
||||
///
|
||||
/// The default is [BoxFit.scaleDown] if [centerSlice] is null, and
|
||||
/// [BoxFit.fill] if [centerSlice] is not null.
|
||||
///
|
||||
/// See the discussion at [_paintImage] for more details.
|
||||
final BoxFit fit;
|
||||
|
||||
/// How to align the image within its bounds.
|
||||
///
|
||||
/// The alignment aligns the given position in the image to the given position
|
||||
/// in the layout bounds. For example, an [Alignment] alignment of (-1.0,
|
||||
/// -1.0) aligns the image to the top-left corner of its layout bounds, while a
|
||||
/// [Alignment] alignment of (1.0, 1.0) aligns the bottom right of the
|
||||
/// image with the bottom right corner of its layout bounds. Similarly, an
|
||||
/// alignment of (0.0, 1.0) aligns the bottom middle of the image with the
|
||||
/// middle of the bottom edge of its layout bounds.
|
||||
///
|
||||
/// To display a subpart of an image, consider using a [CustomPainter] and
|
||||
/// [Canvas.drawImageRect].
|
||||
///
|
||||
/// If the [alignment] is [TextDirection]-dependent (i.e. if it is a
|
||||
/// [AlignmentDirectional]), then a [TextDirection] must be available
|
||||
/// when the image is painted.
|
||||
///
|
||||
/// Defaults to [Alignment.center].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Alignment], a class with convenient constants typically used to
|
||||
/// specify an [AlignmentGeometry].
|
||||
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
|
||||
/// relative to text direction.
|
||||
final AlignmentGeometry alignment;
|
||||
|
||||
/// The center slice for a nine-patch image.
|
||||
///
|
||||
/// The region of the image inside the center slice will be stretched both
|
||||
/// horizontally and vertically to fit the image into its destination. The
|
||||
/// region of the image above and below the center slice will be stretched
|
||||
/// only horizontally and the region of the image to the left and right of
|
||||
/// the center slice will be stretched only vertically.
|
||||
///
|
||||
/// The stretching will be applied in order to make the image fit into the box
|
||||
/// specified by [fit]. When [centerSlice] is not null, [fit] defaults to
|
||||
/// [BoxFit.fill], which distorts the destination image size relative to the
|
||||
/// image's original aspect ratio. Values of [BoxFit] which do not distort the
|
||||
/// destination image size will result in [centerSlice] having no effect
|
||||
/// (since the nine regions of the image will be rendered with the same
|
||||
/// scaling, as if it wasn't specified).
|
||||
final Rect centerSlice;
|
||||
|
||||
/// How to paint any portions of the box that would not otherwise be covered
|
||||
/// by the image.
|
||||
final ImageRepeat repeat;
|
||||
|
||||
/// Whether to paint the image in the direction of the [TextDirection].
|
||||
///
|
||||
/// If this is true, then in [TextDirection.ltr] contexts, the image will be
|
||||
/// drawn with its origin in the top left (the "normal" painting direction for
|
||||
/// images); and in [TextDirection.rtl] contexts, the image will be drawn with
|
||||
/// a scaling factor of -1 in the horizontal direction so that the origin is
|
||||
/// in the top right.
|
||||
final bool matchTextDirection;
|
||||
|
||||
/// Creates a [DecorationImagePainterPlus] for this [DecorationImagePlus].
|
||||
///
|
||||
/// The `onChanged` argument must not be null. It will be called whenever the
|
||||
/// image needs to be repainted, e.g. because it is loading incrementally or
|
||||
/// because it is animated.
|
||||
DecorationImagePainterPlus createPainter(VoidCallback onChanged) {
|
||||
assert(onChanged != null);
|
||||
return DecorationImagePainterPlus._(this, onChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is DecorationImagePlus &&
|
||||
other.runtimeType == runtimeType &&
|
||||
image == other.image &&
|
||||
colorFilter == other.colorFilter &&
|
||||
fit == other.fit &&
|
||||
alignment == other.alignment &&
|
||||
centerSlice == other.centerSlice &&
|
||||
repeat == other.repeat &&
|
||||
matchTextDirection == other.matchTextDirection;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(image, colorFilter, fit, alignment,
|
||||
centerSlice, repeat, matchTextDirection);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final List<String> properties = <String>[];
|
||||
properties.add('$image');
|
||||
if (colorFilter != null) properties.add('$colorFilter');
|
||||
if (fit != null &&
|
||||
!(fit == BoxFit.fill && centerSlice != null) &&
|
||||
!(fit == BoxFit.scaleDown && centerSlice == null)) {
|
||||
properties.add('$fit');
|
||||
}
|
||||
properties.add('$alignment');
|
||||
if (centerSlice != null) properties.add('centerSlice: $centerSlice');
|
||||
if (repeat != ImageRepeat.noRepeat) properties.add('$repeat');
|
||||
if (matchTextDirection) properties.add('match text direction');
|
||||
return '$runtimeType(${properties.join(", ")})';
|
||||
}
|
||||
|
||||
@override
|
||||
ImageErrorListener get onError => (error, stackTrace) {
|
||||
developer.log(
|
||||
'Failed to load image.\n'
|
||||
'$error\n'
|
||||
'$stackTrace',
|
||||
name: 'slide_puzzle.decoration_image_plus');
|
||||
};
|
||||
}
|
||||
|
||||
/// The painter for a [DecorationImagePlus].
|
||||
///
|
||||
/// To obtain a painter, call [DecorationImagePlus.createPainter].
|
||||
///
|
||||
/// To paint, call [paint]. The `onChanged` callback passed to
|
||||
/// [DecorationImagePlus.createPainter] will be called if the image needs to paint
|
||||
/// again (e.g. because it is animated or because it had not yet loaded the
|
||||
/// first time the [paint] method was called).
|
||||
///
|
||||
/// This object should be disposed using the [dispose] method when it is no
|
||||
/// longer needed.
|
||||
class DecorationImagePainterPlus implements DecorationImagePainter {
|
||||
DecorationImagePainterPlus._(this._details, this._onChanged)
|
||||
: assert(_details != null);
|
||||
|
||||
final DecorationImagePlus _details;
|
||||
final VoidCallback _onChanged;
|
||||
|
||||
ImageStream _imageStream;
|
||||
ImageInfo _image;
|
||||
|
||||
/// Draw the image onto the given canvas.
|
||||
///
|
||||
/// The image is drawn at the position and size given by the `rect` argument.
|
||||
///
|
||||
/// The image is clipped to the given `clipPath`, if any.
|
||||
///
|
||||
/// The `configuration` object is used to resolve the image (e.g. to pick
|
||||
/// resolution-specific assets), and to implement the
|
||||
/// [DecorationImagePlus.matchTextDirection] feature.
|
||||
///
|
||||
/// If the image needs to be painted again, e.g. because it is animated or
|
||||
/// because it had not yet been loaded the first time this method was called,
|
||||
/// then the `onChanged` callback passed to [DecorationImagePlus.createPainter]
|
||||
/// will be called.
|
||||
void paint(Canvas canvas, Rect rect, Path clipPath,
|
||||
ImageConfiguration configuration) {
|
||||
assert(canvas != null);
|
||||
assert(rect != null);
|
||||
assert(configuration != null);
|
||||
|
||||
if (_details.matchTextDirection) {
|
||||
assert(() {
|
||||
// We check this first so that the assert will fire immediately, not just
|
||||
// when the image is ready.
|
||||
if (configuration.textDirection == null) {
|
||||
throw FlutterError(
|
||||
'ImageDecoration.matchTextDirection can only be used when a TextDirection is available.\n'
|
||||
'When DecorationImagePainter.paint() was called, there was no text direction provided '
|
||||
'in the ImageConfiguration object to match.\n'
|
||||
'The DecorationImage was:\n'
|
||||
' $_details\n'
|
||||
'The ImageConfiguration was:\n'
|
||||
' $configuration');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
final ImageStream newImageStream = _details.image.resolve(configuration);
|
||||
if (newImageStream.key != _imageStream?.key) {
|
||||
final listener = ImageStreamListener(_imageListener);
|
||||
_imageStream?.removeListener(listener);
|
||||
_imageStream = newImageStream;
|
||||
_imageStream.addListener(listener);
|
||||
}
|
||||
if (_image == null) return;
|
||||
|
||||
if (clipPath != null) {
|
||||
canvas.save();
|
||||
canvas.clipPath(clipPath);
|
||||
}
|
||||
|
||||
_paintImage(
|
||||
canvas: canvas,
|
||||
puzzleWidth: _details.puzzleWidth,
|
||||
puzzleHeight: _details.puzzleHeight,
|
||||
pieceIndex: _details.pieceIndex,
|
||||
rect: rect,
|
||||
image: _image.image,
|
||||
scale: _image.scale,
|
||||
colorFilter: _details.colorFilter,
|
||||
fit: _details.fit,
|
||||
alignment: _details.alignment.resolve(configuration.textDirection),
|
||||
);
|
||||
|
||||
if (clipPath != null) canvas.restore();
|
||||
}
|
||||
|
||||
void _imageListener(ImageInfo value, bool synchronousCall) {
|
||||
if (_image == value) return;
|
||||
_image = value;
|
||||
assert(_onChanged != null);
|
||||
if (!synchronousCall) _onChanged();
|
||||
}
|
||||
|
||||
/// Releases the resources used by this painter.
|
||||
///
|
||||
/// This should be called whenever the painter is no longer needed.
|
||||
///
|
||||
/// After this method has been called, the object is no longer usable.
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
_imageStream?.removeListener(ImageStreamListener(_imageListener));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType(stream: $_imageStream, image: $_image) for $_details';
|
||||
}
|
||||
}
|
||||
|
||||
void _paintImage(
|
||||
{@required Canvas canvas,
|
||||
@required Rect rect,
|
||||
@required ui.Image image,
|
||||
@required int puzzleWidth,
|
||||
@required int puzzleHeight,
|
||||
@required int pieceIndex,
|
||||
double scale = 1.0,
|
||||
ColorFilter colorFilter,
|
||||
BoxFit fit,
|
||||
Alignment alignment = Alignment.center}) {
|
||||
assert(canvas != null);
|
||||
assert(image != null);
|
||||
assert(alignment != null);
|
||||
|
||||
if (rect.isEmpty) return;
|
||||
final outputSize = rect.size;
|
||||
final inputSize = Size(image.width.toDouble(), image.height.toDouble());
|
||||
fit ??= BoxFit.scaleDown;
|
||||
final FittedSizes fittedSizes =
|
||||
applyBoxFit(fit, inputSize / scale, outputSize);
|
||||
final Size sourceSize = fittedSizes.source * scale;
|
||||
final destinationSize = fittedSizes.destination;
|
||||
final Paint paint = Paint()
|
||||
..isAntiAlias = false
|
||||
..filterQuality = FilterQuality.medium;
|
||||
if (colorFilter != null) paint.colorFilter = colorFilter;
|
||||
final double halfWidthDelta =
|
||||
(outputSize.width - destinationSize.width) / 2.0;
|
||||
final double halfHeightDelta =
|
||||
(outputSize.height - destinationSize.height) / 2.0;
|
||||
final double dx = halfWidthDelta + (alignment.x) * halfWidthDelta;
|
||||
final double dy = halfHeightDelta + alignment.y * halfHeightDelta;
|
||||
final Offset destinationPosition = rect.topLeft.translate(dx, dy);
|
||||
final Rect destinationRect = destinationPosition & destinationSize;
|
||||
final Rect sourceRect =
|
||||
alignment.inscribe(sourceSize, Offset.zero & inputSize);
|
||||
|
||||
final sliceSize =
|
||||
Size(sourceRect.width / puzzleWidth, sourceRect.height / puzzleHeight);
|
||||
|
||||
final col = pieceIndex % puzzleWidth;
|
||||
final row = pieceIndex ~/ puzzleWidth;
|
||||
|
||||
final sliceRect = Rect.fromLTWH(
|
||||
sourceRect.left + col * sliceSize.width,
|
||||
sourceRect.top + row * sliceSize.height,
|
||||
sliceSize.width,
|
||||
sliceSize.height);
|
||||
|
||||
canvas.drawImageRect(image, sliceRect, destinationRect, paint);
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import '../flutter.dart';
|
||||
|
||||
// Copied from
|
||||
// https://github.com/flutter/flutter/blob/f5b02e3c05ed1ab31e890add84fb56e35de2d392/packages/flutter/lib/src/material/material.dart#L593-L715
|
||||
// So I could have animated color!
|
||||
// TODO(kevmoo): file a feature request for this?
|
||||
class MaterialInterior extends ImplicitlyAnimatedWidget {
|
||||
const MaterialInterior({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.shape,
|
||||
@required this.color,
|
||||
Curve curve = Curves.linear,
|
||||
@required Duration duration,
|
||||
}) : assert(child != null),
|
||||
assert(shape != null),
|
||||
assert(color != null),
|
||||
super(key: key, curve: curve, duration: duration);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
/// The border of the widget.
|
||||
///
|
||||
/// This border will be painted, and in addition the outer path of the border
|
||||
/// determines the physical shape.
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// The target background color.
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
_MaterialInteriorState createState() => _MaterialInteriorState();
|
||||
}
|
||||
|
||||
class _MaterialInteriorState extends AnimatedWidgetBaseState<MaterialInterior> {
|
||||
ShapeBorderTween _border;
|
||||
ColorTween _color;
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_border = visitor(_border, widget.shape,
|
||||
(value) => ShapeBorderTween(begin: value as ShapeBorder))
|
||||
as ShapeBorderTween;
|
||||
_color = visitor(
|
||||
_color, widget.color, (value) => ColorTween(begin: value as Color))
|
||||
as ColorTween;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shape = _border.evaluate(animation);
|
||||
return PhysicalShape(
|
||||
child: _ShapeBorderPaint(
|
||||
child: widget.child,
|
||||
shape: shape,
|
||||
),
|
||||
clipper: ShapeBorderClipper(
|
||||
shape: shape,
|
||||
textDirection: Directionality.of(context),
|
||||
),
|
||||
color: _color.evaluate(animation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShapeBorderPaint extends StatelessWidget {
|
||||
const _ShapeBorderPaint({
|
||||
@required this.child,
|
||||
@required this.shape,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
child: child,
|
||||
foregroundPainter: _ShapeBorderPainter(shape, Directionality.of(context)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShapeBorderPainter extends CustomPainter {
|
||||
_ShapeBorderPainter(this.border, this.textDirection);
|
||||
|
||||
final ShapeBorder border;
|
||||
final TextDirection textDirection;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
border.paint(canvas, Offset.zero & size, textDirection: textDirection);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_ShapeBorderPainter oldDelegate) {
|
||||
return oldDelegate.border != border;
|
||||
}
|
||||
}
|
@ -1,426 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.39.8"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.12"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.9"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.14.0+3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.1"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.12"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.1+1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.4"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.6"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.6+3"
|
||||
multi_server_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: multi_server_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
node_interop:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_interop
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
node_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.8"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.4"
|
||||
pedantic:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
provider:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.4"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.10.9"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.3"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.15"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.7+15"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
sdks:
|
||||
dart: ">=2.7.0 <3.0.0"
|
@ -1,27 +0,0 @@
|
||||
name: slide_puzzle
|
||||
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: ">=2.0.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
pedantic: ^1.3.0
|
||||
provider: ^2.0.0
|
||||
test: ^1.3.4
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- asset/
|
||||
|
||||
fonts:
|
||||
- family: Plaster
|
||||
fonts:
|
||||
- asset: asset/fonts/plaster/Plaster-Regular.ttf
|
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>slide_puzzle</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in new issue