小白带你践行Flutter拼图GAME
下载
https://github.com/kevmoo/slide_puzzle.git
运行效果
源代码分析
1) main.dart是入口代码
//main.dart
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: '幻灯片拼图',
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));
}
定义app_state.dart
import 'package:flutter/foundation.dart';
import 'core/puzzle_proxy.dart';
abstract class AppState {
PuzzleProxy get puzzle;
Listenable get animationNotifier;
}
flutter.dart
export 'package:flutter/material.dart';
export 'package:flutter/scheduler.dart' show Ticker;
定义控件类
PuzzleControls实现Listenable的监控
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;
}
继承流程委托
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);
}
实现PuzzleConrol类
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)),
),
)
],
),
),
),
),
),
),
),
);
定义主题
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);
}
}
定义三种主题样式
import 'theme_plaster.dart';
import 'theme_seattle.dart';
import 'theme_simple.dart';
const themes = [
ThemeSimple(),
ThemeSeattle(),
ThemePlaster(),
];
3种样式
样式1:
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),
),
);
}
}
样式2
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);
}
}
样式3
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),
);
}
}
AS运行日志
Syncing files to device Android SDK built for x86...
D/EGL_emulation( 5021): eglMakeCurrent: 0xdf11a0c0: ver 3 0 (tinfo 0xdf10f070)
I/OpenGLRenderer( 5021): Davey! duration=2287ms; Flags=1, IntendedVsync=258843971182, Vsync=258843971182, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=258859266525, AnimationStart=258862226525, PerformTraversalsStart=258866966525, DrawStart=261012198525, SyncQueued=261017953525, SyncStart=261022699525, IssueDrawCommandsStart=261034468525, SwapBuffers=261089841525, FrameCompleted=261136182525, DequeueBufferDuration=29212000, QueueBufferDuration=1417000,
I/Choreographer( 5021): Skipped 133 frames! The application may be doing too much work on its main thread.
D/EGL_emulation( 5021): eglMakeCurrent: 0xf247fb20: ver 3 0 (tinfo 0xe7152e30)
运行效果: