1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 23:08:59 +00:00

Add flutter_web samples (#75)

This commit is contained in:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
// Copyright 2019 The Chromium Authors. 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_animator.dart';
import 'flutter.dart';
import 'shared_theme.dart';
abstract class AppState {
TabController get tabController;
PuzzleProxy get puzzle;
bool get autoPlay;
void setAutoPlay(bool newValue);
AnimationNotifier get animationNotifier;
Iterable<SharedTheme> get themeData;
SharedTheme get currentTheme;
set currentTheme(SharedTheme theme);
}
abstract class AnimationNotifier implements Listenable {
void animate();
void dispose();
}

View File

@@ -0,0 +1,113 @@
// Copyright 2019 The Chromium Authors. 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;
}

View File

@@ -0,0 +1,12 @@
// Copyright 2019 The Chromium Authors. 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);
}

View File

@@ -0,0 +1,275 @@
// Copyright 2019 The Chromium Authors. 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.');
}
}

View File

@@ -0,0 +1,205 @@
// Copyright 2019 The Chromium Authors. 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 'dart:math' show Point, Random;
import 'body.dart';
import 'puzzle.dart';
enum PuzzleEvent { click, reset, noop }
abstract class PuzzleProxy {
int get width;
int get height;
int get length;
bool get solved;
void reset();
void clickOrShake(int tileValue);
int get tileCount;
int get clickCount;
int get incorrectTiles;
Point<double> location(int index);
bool isCorrectPosition(int value);
}
class PuzzleAnimator implements PuzzleProxy {
final _rnd = Random();
final List<Body> _locations;
final _controller = StreamController<PuzzleEvent>();
bool _nextRandomVertical = true;
Puzzle _puzzle;
int _clickCount = 0;
bool _stable;
bool get stable => _stable;
@override
bool get solved => _puzzle.incorrectTiles == 0;
@override
int get width => _puzzle.width;
@override
int get height => _puzzle.height;
@override
int get length => _puzzle.length;
@override
int get tileCount => _puzzle.tileCount;
@override
int get incorrectTiles => _puzzle.incorrectTiles;
@override
int get clickCount => _clickCount;
@override
void reset() => _resetCore();
Stream<PuzzleEvent> get onEvent => _controller.stream;
@override
bool isCorrectPosition(int value) => _puzzle.isCorrectPosition(value);
@override
Point<double> location(int index) => _locations[index].location;
int _lastBadClick;
int _badClickCount = 0;
PuzzleAnimator(int width, int height) : this._(Puzzle(width, height));
PuzzleAnimator._(this._puzzle)
: _locations = List.generate(_puzzle.length, (i) {
return Body.raw(
(_puzzle.width - 1.0) / 2, (_puzzle.height - 1.0) / 2, 0, 0);
});
void playRandom() {
if (_puzzle.fitness == 0) {
return;
}
_puzzle = _puzzle.clickRandom(vertical: _nextRandomVertical);
_nextRandomVertical = !_nextRandomVertical;
_clickCount++;
_controller.add(PuzzleEvent.click);
}
@override
void clickOrShake(int tileValue) {
if (solved) {
_controller.add(PuzzleEvent.noop);
_shake(tileValue);
_lastBadClick = null;
_badClickCount = 0;
return;
}
_controller.add(PuzzleEvent.click);
if (!_clickValue(tileValue)) {
_shake(tileValue);
// This is logic to allow a user to skip to the end useful for testing
// click on 5 un-movable tiles in a row, but not the same tile twice
// in a row
if (tileValue != _lastBadClick) {
_badClickCount++;
if (_badClickCount >= 5) {
// Do the reset!
final newValues = List.generate(_puzzle.length, (i) {
if (i == _puzzle.tileCount) {
return _puzzle.tileCount - 1;
} else if (i == (_puzzle.tileCount - 1)) {
return _puzzle.tileCount;
}
return i;
});
_resetCore(source: newValues);
_clickCount = 999;
}
} else {
_badClickCount = 0;
}
_lastBadClick = tileValue;
} else {
_lastBadClick = null;
_badClickCount = 0;
}
}
void _resetCore({List<int> source}) {
_puzzle = _puzzle.reset(source: source);
_clickCount = 0;
_lastBadClick = null;
_badClickCount = 0;
_controller.add(PuzzleEvent.reset);
}
bool _clickValue(int value) {
final newPuzzle = _puzzle.clickValue(value);
if (newPuzzle == null) {
return false;
} else {
_clickCount++;
_puzzle = newPuzzle;
return true;
}
}
void _shake(int tileValue) {
Point<double> deltaDouble;
if (solved) {
deltaDouble = Point(_rnd.nextDouble() - 0.5, _rnd.nextDouble() - 0.5);
} else {
final delta = _puzzle.openPosition() - _puzzle.coordinatesOf(tileValue);
deltaDouble = Point(delta.x.toDouble(), delta.y.toDouble());
}
deltaDouble *= 0.5 / deltaDouble.magnitude;
_locations[tileValue].kick(deltaDouble);
}
void update(Duration timeDelta) {
assert(!timeDelta.isNegative);
assert(timeDelta != Duration.zero);
var animationSeconds = timeDelta.inMilliseconds / 60.0;
if (animationSeconds == 0) {
animationSeconds = 0.1;
}
assert(animationSeconds > 0);
_stable = true;
for (var i = 0; i < _puzzle.length; i++) {
final target = _target(i);
final body = _locations[i];
_stable = !body.animate(animationSeconds,
force: target - body.location,
drag: .9,
maxVelocity: 1.0,
snapTo: target) &&
_stable;
}
}
Point<double> _target(int item) {
final target = _puzzle.coordinatesOf(item);
return Point(target.x.toDouble(), target.y.toDouble());
}
}

View File

@@ -0,0 +1,63 @@
// Copyright 2019 The Chromium Authors. 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;
}
}

View File

@@ -0,0 +1,188 @@
// Copyright 2019 The Chromium Authors. 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;
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2019 The Chromium Authors. 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;
}

View File

@@ -0,0 +1,7 @@
// Copyright 2019 The Chromium Authors. 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_web/material.dart';
export 'package:flutter_web/scheduler.dart' show Ticker;

View File

@@ -0,0 +1,43 @@
// Copyright 2019 The Chromium Authors. 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_animator.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);
}

View File

@@ -0,0 +1,177 @@
// Copyright 2019 The Chromium Authors. 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 'app_state.dart';
import 'core/puzzle_animator.dart';
import 'flutter.dart';
import 'shared_theme.dart';
import 'theme_plaster.dart';
import 'theme_seattle.dart';
import 'theme_simple.dart';
class PuzzleHomeState extends State
with TickerProviderStateMixin
implements AppState {
TabController _tabController;
AnimationController _controller;
@override
final PuzzleAnimator puzzle;
@override
final animationNotifier = _AnimationNotifier();
@override
TabController get tabController => _tabController;
SharedTheme _currentTheme;
@override
SharedTheme get currentTheme => _currentTheme;
@override
set currentTheme(SharedTheme theme) {
setState(() {
_currentTheme = theme;
});
}
Duration _tickerTimeSinceLastEvent = Duration.zero;
Ticker _ticker;
Duration _lastElapsed;
StreamSubscription sub;
@override
bool autoPlay = false;
PuzzleHomeState(this.puzzle) {
sub = puzzle.onEvent.listen(_onPuzzleEvent);
_themeDataCache = List.unmodifiable([
ThemeSimple(this),
ThemeSeattle(this),
ThemePlaster(this),
]);
_currentTheme = themeData.first;
}
@override
void initState() {
super.initState();
_ticker ??= createTicker(_onTick);
_ensureTicking();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_tabController = TabController(vsync: this, length: _themeDataCache.length);
_tabController.addListener(() {
currentTheme = _themeDataCache[_tabController.index];
});
}
List<SharedTheme> _themeDataCache;
@override
Iterable<SharedTheme> get themeData => _themeDataCache;
@override
void setAutoPlay(bool newValue) {
if (newValue != autoPlay) {
setState(() {
// Only allow enabling autoPlay if the puzzle is not solved
autoPlay = newValue && !puzzle.solved;
if (autoPlay) {
_ensureTicking();
}
});
}
}
@override
Widget build(BuildContext context) =>
LayoutBuilder(builder: _currentTheme.build);
@override
void dispose() {
animationNotifier.dispose();
_tabController.dispose();
_controller?.dispose();
_ticker?.dispose();
sub.cancel();
super.dispose();
}
void _onPuzzleEvent(PuzzleEvent e) {
_tickerTimeSinceLastEvent = Duration.zero;
_ensureTicking();
if (e == PuzzleEvent.noop) {
assert(e == PuzzleEvent.noop);
_controller
..reset()
..forward();
}
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 implements AnimationNotifier {
_AnimationNotifier();
@override
void animate() {
notifyListeners();
}
}
const _maxFrameDuration = Duration(milliseconds: 34);

View File

@@ -0,0 +1,247 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'app_state.dart';
import 'core/puzzle_animator.dart';
import 'flutter.dart';
import 'puzzle_flow_delegate.dart';
import 'widgets/material_interior_alt.dart';
abstract class SharedTheme {
SharedTheme(this._appState);
final AppState _appState;
PuzzleProxy get puzzle => _appState.puzzle;
String get name;
Color get puzzleThemeBackground;
RoundedRectangleBorder get puzzleBorder;
Color get puzzleBackgroundColor;
Color get puzzleAccentColor;
EdgeInsetsGeometry get tilePadding => const EdgeInsets.all(6);
Widget tileButton(int i);
Ink createInk(
Widget child, {
DecorationImage image,
EdgeInsetsGeometry padding,
}) =>
Ink(
padding: padding,
decoration: BoxDecoration(
image: image,
),
child: child,
);
Widget createButton(
int tileValue,
Widget content, {
Color color,
RoundedRectangleBorder shape,
}) =>
AnimatedContainer(
duration: _puzzleAnimationDuration,
padding: tilePadding,
child: RaisedButton(
elevation: 4,
clipBehavior: Clip.hardEdge,
animationDuration: _puzzleAnimationDuration,
onPressed: () => _tilePress(tileValue),
shape: shape ?? puzzleBorder,
padding: const EdgeInsets.symmetric(),
child: content,
color: color,
),
);
double _previousConstraintWidth;
bool _small;
bool get small => _small;
void _updateConstraints(BoxConstraints constraints) {
const _smallWidth = 580;
final constraintWidth =
constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0;
if (constraintWidth == _previousConstraintWidth) {
assert(_small != null);
return;
}
_previousConstraintWidth = constraintWidth;
if (_previousConstraintWidth < _smallWidth) {
_small = true;
} else {
_small = false;
}
}
Widget build(BuildContext context, BoxConstraints constraints) {
_updateConstraints(constraints);
return Material(
child: Stack(
children: <Widget>[
const SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: Image(
image: AssetImage('seattle.jpg'),
),
),
),
AnimatedContainer(
duration: _puzzleAnimationDuration,
color: puzzleThemeBackground,
child: Center(
child: _styledWrapper(
SizedBox(
width: 580,
child: 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: _appState.tabController,
labelPadding: const EdgeInsets.fromLTRB(0, 20, 0, 12),
labelColor: puzzleAccentColor,
indicatorColor: puzzleAccentColor,
indicatorWeight: 1.5,
unselectedLabelColor: Colors.black.withOpacity(0.6),
tabs: _appState.themeData
.map((st) => Text(
st.name.toUpperCase(),
style: const TextStyle(
letterSpacing: 0.5,
),
))
.toList(),
),
),
Container(
constraints: const BoxConstraints.tightForFinite(),
padding: const EdgeInsets.all(10),
child: Flow(
delegate: PuzzleFlowDelegate(
small ? const Size(90, 90) : const Size(140, 140),
puzzle,
_appState.animationNotifier,
),
children: List<Widget>.generate(
puzzle.length,
_tileButton,
),
),
),
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: Row(children: _bottomControls(context)),
)
],
),
),
),
),
)
],
));
}
Duration get _puzzleAnimationDuration => kThemeAnimationDuration * 3;
// Thought about using AnimatedContainer here, but it causes some weird
// resizing behavior
Widget _styledWrapper(Widget child) => MaterialInterior(
duration: _puzzleAnimationDuration,
shape: puzzleBorder,
color: puzzleBackgroundColor,
child: child,
);
void Function(bool newValue) get _setAutoPlay {
if (puzzle.solved) {
return null;
}
return _appState.setAutoPlay;
}
void _tilePress(int tileValue) {
_appState.setAutoPlay(false);
_appState.puzzle.clickOrShake(tileValue);
}
TextStyle get _infoStyle => TextStyle(
color: puzzleAccentColor,
fontWeight: FontWeight.bold,
);
List<Widget> _bottomControls(BuildContext context) => <Widget>[
IconButton(
onPressed: puzzle.reset,
icon: Icon(Icons.refresh, color: puzzleAccentColor),
//Icons.refresh,
),
Checkbox(
value: _appState.autoPlay,
onChanged: _setAutoPlay,
activeColor: puzzleAccentColor,
),
Expanded(
child: Container(),
),
Text(
puzzle.clickCount.toString(),
textAlign: TextAlign.right,
style: _infoStyle,
),
const Text(' Moves'),
SizedBox(
width: 28,
child: Text(
puzzle.incorrectTiles.toString(),
textAlign: TextAlign.right,
style: _infoStyle,
),
),
const Text(' Tiles left ')
];
Widget _tileButton(int i) {
if (i == puzzle.tileCount && !puzzle.solved) {
return const Center();
}
return tileButton(i);
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'app_state.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';
ThemePlaster(AppState baseTheme) : super(baseTheme);
@override
Color get puzzleThemeBackground => _chocolate;
@override
Color get puzzleBackgroundColor => _yellowIsh;
@override
Color get puzzleAccentColor => _orangeIsh;
@override
RoundedRectangleBorder get puzzleBorder => 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) {
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(
i,
content,
color: primary ? _orangeIsh : _yellowIsh,
shape: RoundedRectangleBorder(
side: BorderSide(color: primary ? _chocolate : _orangeIsh, width: 5),
borderRadius: BorderRadius.circular(5),
),
);
}
}

View File

@@ -0,0 +1,74 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'app_state.dart';
import 'flutter.dart';
import 'shared_theme.dart';
import 'widgets/decoration_image_plus.dart';
class ThemeSeattle extends SharedTheme {
@override
String get name => 'Seattle';
ThemeSeattle(AppState proxy) : super(proxy);
@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 get puzzleBorder => const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(1),
),
);
@override
EdgeInsetsGeometry get tilePadding =>
puzzle.solved ? const EdgeInsets.all(1) : const EdgeInsets.all(4);
@override
Widget tileButton(int i) {
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('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(i, content);
}
}

View File

@@ -0,0 +1,68 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'app_state.dart';
import 'flutter.dart';
import 'shared_theme.dart';
const _accentBlue = Color(0xff000579e);
class ThemeSimple extends SharedTheme {
@override
String get name => 'Simple';
ThemeSimple(AppState proxy) : super(proxy);
@override
Color get puzzleThemeBackground => Colors.white;
@override
Color get puzzleBackgroundColor => Colors.white70;
@override
Color get puzzleAccentColor => _accentBlue;
@override
RoundedRectangleBorder get puzzleBorder => const RoundedRectangleBorder(
side: BorderSide(color: Colors.black26, width: 1),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
);
@override
Widget tileButton(int i) {
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(
i,
content,
color: const Color.fromARGB(255, 13, 87, 155),
);
}
}

View File

@@ -0,0 +1,322 @@
// Copyright 2019 The Chromium Authors. 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 'package:flutter_web_ui/ui.dart' 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>['$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(", ")})';
}
}
/// 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) {
_imageStream?.removeListener(_imageListener);
_imageStream = newImageStream..addListener(_imageListener);
}
if (_image == null) return;
if (clipPath != null) {
canvas
..save()
..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(_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);
}

View File

@@ -0,0 +1,106 @@
// Copyright 2019 The Chromium Authors. 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;
}
}