From f87c2bb577f301e57dfc0e432604b60e37c29fac Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 24 May 2019 12:08:02 -0700 Subject: [PATCH] web/slide_puzzle: more provider cleanup --- .../lib/src/puzzle_home_state.dart | 161 ++++++++++++++---- web/slide_puzzle/lib/src/shared_theme.dart | 133 +-------------- web/slide_puzzle/lib/src/themes.dart | 87 ++++++++++ web/slide_puzzle/pubspec.yaml | 2 +- web/slide_puzzle/web/main.dart | 2 +- 5 files changed, 228 insertions(+), 157 deletions(-) diff --git a/web/slide_puzzle/lib/src/puzzle_home_state.dart b/web/slide_puzzle/lib/src/puzzle_home_state.dart index 33fc4807d..c740d98b1 100644 --- a/web/slide_puzzle/lib/src/puzzle_home_state.dart +++ b/web/slide_puzzle/lib/src/puzzle_home_state.dart @@ -9,20 +9,17 @@ import 'package:provider/provider.dart'; import 'app_state.dart'; import 'core/puzzle_animator.dart'; import 'flutter.dart'; +import 'puzzle_flow_delegate.dart'; import 'shared_theme.dart'; import 'themes.dart'; class PuzzleHomeState extends State with TickerProviderStateMixin, AppState { - TabController _tabController; - AnimationController _controller; - @override final PuzzleAnimator puzzle; @override final _AnimationNotifier animationNotifier = _AnimationNotifier(); - SharedTheme _currentTheme; Duration _tickerTimeSinceLastEvent = Duration.zero; Ticker _ticker; Duration _lastElapsed; @@ -33,8 +30,6 @@ class PuzzleHomeState extends State with TickerProviderStateMixin, AppState { PuzzleHomeState(this.puzzle) { _puzzleEventSubscription = puzzle.onEvent.listen(_onPuzzleEvent); - - _currentTheme = themes.first; } @override @@ -42,19 +37,6 @@ class PuzzleHomeState extends State with TickerProviderStateMixin, AppState { super.initState(); _ticker ??= createTicker(_onTick); _ensureTicking(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - - _tabController = TabController(vsync: this, length: themes.length); - - _tabController.addListener(() { - setState(() { - _currentTheme = themes[_tabController.index]; - }); - }); } @override @@ -70,20 +52,41 @@ class PuzzleHomeState extends State with TickerProviderStateMixin, AppState { } } + bool _badHack; + @override Widget build(BuildContext context) => MultiProvider( providers: [ - ListenableProvider.value(listenable: _tabController), - Provider.value(value: this), + Provider.value( + value: this, + updateShouldNotify: (p, c) { + if (c.autoPlay != _badHack) { + _badHack = c.autoPlay; + return true; + } + return false; + }), ], - child: LayoutBuilder(builder: _currentTheme.build), + child: Material( + child: Stack( + children: [ + const SizedBox.expand( + child: FittedBox( + fit: BoxFit.cover, + child: Image( + image: AssetImage('seattle.jpg'), + ), + ), + ), + const LayoutBuilder(builder: _doBuild), + ], + ), + ), ); @override void dispose() { animationNotifier.dispose(); - _tabController.dispose(); - _controller?.dispose(); _ticker?.dispose(); _puzzleEventSubscription.cancel(); super.dispose(); @@ -92,12 +95,6 @@ class PuzzleHomeState extends State with TickerProviderStateMixin, AppState { void _onPuzzleEvent(PuzzleEvent e) { _tickerTimeSinceLastEvent = Duration.zero; _ensureTicking(); - if (e == PuzzleEvent.noop) { - assert(e == PuzzleEvent.noop); - _controller - ..reset() - ..forward(); - } setState(() { // noop }); @@ -152,3 +149,107 @@ class _AnimationNotifier extends ChangeNotifier { } 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; + + return builder(constraintWidth < _smallWidth); +} + +Widget _doBuild(BuildContext _, BoxConstraints constraints) => + _updateConstraints(constraints, _doBuildCore); + +Widget _doBuildCore(bool small) => PuzzleThemeTabController( + child: Consumer( + builder: (_, theme, __) => AnimatedContainer( + duration: puzzleAnimationDuration, + color: theme.puzzleThemeBackground, + child: Center( + child: theme.styledWrapper( + small, + SizedBox( + width: 580, + child: Consumer( + builder: (context, appState, _) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.black26, + width: 1, + ), + ), + ), + margin: + const EdgeInsets.symmetric(horizontal: 20), + child: TabBar( + controller: + PuzzleThemeTabController.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(), + ), + ), + Container( + constraints: + const BoxConstraints.tightForFinite(), + padding: const EdgeInsets.all(10), + child: Flow( + delegate: PuzzleFlowDelegate( + small + ? const Size(90, 90) + : const Size(140, 140), + appState.puzzle, + appState.animationNotifier, + ), + children: List.generate( + appState.puzzle.length, + (i) => theme.tileButtonCore( + i, appState, 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: Row( + children: theme.bottomControls(appState)), + ) + ], + ), + ), + ), + ), + ), + ), + ), + ); diff --git a/web/slide_puzzle/lib/src/shared_theme.dart b/web/slide_puzzle/lib/src/shared_theme.dart index 8a544d88b..e51a216dc 100644 --- a/web/slide_puzzle/lib/src/shared_theme.dart +++ b/web/slide_puzzle/lib/src/shared_theme.dart @@ -2,15 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:provider/provider.dart'; - import 'app_state.dart'; import 'core/puzzle_animator.dart'; import 'flutter.dart'; -import 'puzzle_flow_delegate.dart'; -import 'themes.dart'; import 'widgets/material_interior_alt.dart'; +final puzzleAnimationDuration = kThemeAnimationDuration * 3; + abstract class SharedTheme { const SharedTheme(); @@ -50,12 +48,12 @@ abstract class SharedTheme { RoundedRectangleBorder shape, }) => AnimatedContainer( - duration: _puzzleAnimationDuration, + duration: puzzleAnimationDuration, padding: tilePadding(appState.puzzle), child: RaisedButton( elevation: 4, clipBehavior: Clip.hardEdge, - animationDuration: _puzzleAnimationDuration, + animationDuration: puzzleAnimationDuration, onPressed: () => appState.clickOrShake(tileValue), shape: shape ?? puzzleBorder(small), padding: const EdgeInsets.symmetric(), @@ -64,125 +62,10 @@ abstract class SharedTheme { ), ); - Widget _updateConstraints( - BoxConstraints constraints, Widget Function(bool small) builder) { - const _smallWidth = 580; - - final constraintWidth = - constraints.hasBoundedWidth ? constraints.maxWidth : 1000.0; - - return builder(constraintWidth < _smallWidth); - } - - Widget build(BuildContext context, BoxConstraints constraints) => - _updateConstraints( - constraints, - (small) => Material( - child: Stack( - children: [ - const SizedBox.expand( - child: FittedBox( - fit: BoxFit.cover, - child: Image( - image: AssetImage('seattle.jpg'), - ), - ), - ), - AnimatedContainer( - duration: _puzzleAnimationDuration, - color: puzzleThemeBackground, - child: Center( - child: _styledWrapper( - small, - SizedBox( - width: 580, - child: Consumer( - builder: (context, appState, _) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.black26, - width: 1, - ), - ), - ), - margin: const EdgeInsets.symmetric( - horizontal: 20), - child: TabBar( - controller: - Provider.of(context), - labelPadding: const EdgeInsets.fromLTRB( - 0, 20, 0, 12), - labelColor: puzzleAccentColor, - indicatorColor: 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(), - ), - ), - Container( - constraints: - const BoxConstraints.tightForFinite(), - padding: const EdgeInsets.all(10), - child: Flow( - delegate: PuzzleFlowDelegate( - small - ? const Size(90, 90) - : const Size(140, 140), - appState.puzzle, - appState.animationNotifier, - ), - children: List.generate( - appState.puzzle.length, - (i) => - _tileButton(i, appState, 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: Row( - children: _bottomControls(appState)), - ) - ], - ), - ), - ), - ), - ), - ) - ], - ))); - - Duration get _puzzleAnimationDuration => kThemeAnimationDuration * 3; - // Thought about using AnimatedContainer here, but it causes some weird // resizing behavior - Widget _styledWrapper(bool small, Widget child) => MaterialInterior( - duration: _puzzleAnimationDuration, + Widget styledWrapper(bool small, Widget child) => MaterialInterior( + duration: puzzleAnimationDuration, shape: puzzleBorder(small), color: puzzleBackgroundColor, child: child, @@ -193,7 +76,7 @@ abstract class SharedTheme { fontWeight: FontWeight.bold, ); - List _bottomControls(AppState appState) => [ + List bottomControls(AppState appState) => [ IconButton( onPressed: appState.puzzle.reset, icon: Icon(Icons.refresh, color: puzzleAccentColor), @@ -224,7 +107,7 @@ abstract class SharedTheme { const Text(' Tiles left ') ]; - Widget _tileButton(int i, AppState appState, bool small) { + Widget tileButtonCore(int i, AppState appState, bool small) { if (i == appState.puzzle.tileCount && !appState.puzzle.solved) { return const Center(); } diff --git a/web/slide_puzzle/lib/src/themes.dart b/web/slide_puzzle/lib/src/themes.dart index 7889db6c3..e4309ad33 100644 --- a/web/slide_puzzle/lib/src/themes.dart +++ b/web/slide_puzzle/lib/src/themes.dart @@ -1,3 +1,7 @@ +import 'package:flutter_web/material.dart'; +import 'package:provider/provider.dart'; + +import 'shared_theme.dart'; import 'theme_plaster.dart'; import 'theme_seattle.dart'; import 'theme_simple.dart'; @@ -7,3 +11,86 @@ const themes = [ ThemeSeattle(), ThemePlaster(), ]; + +class PuzzleThemeTabController extends StatefulWidget { + /// Creates a default tab controller for the given [child] widget. + const PuzzleThemeTabController({ + Key key, + @required this.child, + }) : 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; + + /// 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(_PuzzleThemeTabControllerScope) + as _PuzzleThemeTabControllerScope; + return scope?.controller; + } + + @override + _PuzzleThemeTabControllerState createState() => + _PuzzleThemeTabControllerState(); +} + +class _PuzzleThemeTabControllerState extends State + with SingleTickerProviderStateMixin { + final _notifier = ValueNotifier(themes.first); + + TabController _controller; + + @override + void initState() { + super.initState(); + _controller = TabController( + vsync: this, + length: themes.length, + initialIndex: 0, + ); + + _controller.addListener(() { + _notifier.value = themes[_controller.index]; + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => _PuzzleThemeTabControllerScope( + controller: _controller, + enabled: TickerMode.of(context), + child: ValueListenableProvider.value( + valueListenable: _notifier, + child: widget.child, + ), + ); +} + +class _PuzzleThemeTabControllerScope extends InheritedWidget { + const _PuzzleThemeTabControllerScope( + {Key key, this.controller, this.enabled, Widget child}) + : super(key: key, child: child); + + final TabController controller; + final bool enabled; + + @override + bool updateShouldNotify(_PuzzleThemeTabControllerScope old) => + enabled != old.enabled || controller != old.controller; +} diff --git a/web/slide_puzzle/pubspec.yaml b/web/slide_puzzle/pubspec.yaml index 7d4458df1..7878ca2f0 100644 --- a/web/slide_puzzle/pubspec.yaml +++ b/web/slide_puzzle/pubspec.yaml @@ -1,4 +1,4 @@ -name: flutter_web.examples.slide_puzzle +name: slide_puzzle environment: sdk: ">=2.2.0 <3.0.0" diff --git a/web/slide_puzzle/web/main.dart b/web/slide_puzzle/web/main.dart index 3c8ee9114..436328329 100644 --- a/web/slide_puzzle/web/main.dart +++ b/web/slide_puzzle/web/main.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_web_ui/ui.dart' as ui; -import 'package:flutter_web.examples.slide_puzzle/main.dart' as app; +import 'package:slide_puzzle/main.dart' as app; void main() async { await ui.webOnlyInitializePlatform();