小白带你践行Flutter拼图GAME

小白带你践行Flutter拼图GAME

小白带你践行Flutter拼图GAME

下载

https://github.com/kevmoo/slide_puzzle.git

运行效果

小白带你践行Flutter拼图GAME

 

源代码分析

小白带你践行Flutter拼图GAME

 

1) main.dart是入口代码

//main.dartimport '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)

 

运行效果:

小白带你践行Flutter拼图GAME

 

上一篇:大顶堆 小顶堆应用----中位数查找


下一篇:幕客前端基础入门-css文本样式