From 628884a8a8bebf4d932debafa90525eedc8e33d5 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 4 Aug 2016 13:05:18 -0700 Subject: [PATCH] Make AppBar a Hero (#5214) This patch improves the Post and Shrine transitions by making the AppBar into a Hero and changing the default MaterialPageTransition. Now the AppBar transitions smoothly between screens and the MaterialPageTransition doesn't involve a fade effect. Also, rejigger the bounds of the image header in Pesto to avoid the "pop" at the end of the animation by laying out the image header at its final visual size instead of relying on occlusion to size the image header. Fixes #5202 Fixes #5204 --- .../lib/demo/grid_list_demo.dart | 8 ++--- .../flutter_gallery/lib/demo/pesto_demo.dart | 10 +++--- .../flutter/lib/src/material/app_bar.dart | 26 +++++++++++---- .../lib/src/material/flexible_space_bar.dart | 33 +++++++++++-------- packages/flutter/lib/src/material/page.dart | 26 ++++++--------- packages/flutter/lib/src/material/tabs.dart | 7 ++-- .../flutter/lib/src/widgets/editable.dart | 2 +- packages/flutter/lib/src/widgets/heroes.dart | 2 +- 8 files changed, 65 insertions(+), 49 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/grid_list_demo.dart b/examples/flutter_gallery/lib/demo/grid_list_demo.dart index 893587db03..9a8a0733ed 100644 --- a/examples/flutter_gallery/lib/demo/grid_list_demo.dart +++ b/examples/flutter_gallery/lib/demo/grid_list_demo.dart @@ -59,11 +59,9 @@ class GridDemoPhotoItem extends StatelessWidget { appBar: new AppBar( title: new Text(photo.title) ), - body: new Material( - child: new Hero( - tag: photoHeroTag, - child: new Image.asset(photo.assetName, fit: ImageFit.cover) - ) + body: new Hero( + tag: photoHeroTag, + child: new Image.asset(photo.assetName, fit: ImageFit.cover) ) ); } diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index e1eb233517..86f686da50 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -325,9 +325,10 @@ class _RecipePageState extends State<_RecipePage> { // adjusts based on the size of the screen. If the recipe sheet touches // the edge of the screen, use a slightly different layout. Widget _buildContainer(BuildContext context) { - bool isFavorite = favoriteRecipes.contains(config.recipe); - Size screenSize = MediaQuery.of(context).size; - bool fullWidth = (screenSize.width < _kRecipePageMaxWidth); + final bool isFavorite = favoriteRecipes.contains(config.recipe); + final Size screenSize = MediaQuery.of(context).size; + final bool fullWidth = (screenSize.width < _kRecipePageMaxWidth); + final double appBarHeight = _getAppBarHeight(context); const double fabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size return new Stack( children: [ @@ -335,6 +336,7 @@ class _RecipePageState extends State<_RecipePage> { top: 0.0, left: 0.0, right: 0.0, + height: appBarHeight + fabHalfSize, child: new Hero( tag: config.recipe.imagePath, child: new Image.asset( @@ -346,7 +348,7 @@ class _RecipePageState extends State<_RecipePage> { new ScrollableViewport( child: new RepaintBoundary( child: new Padding( - padding: new EdgeInsets.only(top: _getAppBarHeight(context)), + padding: new EdgeInsets.only(top: appBarHeight), child: new Stack( children: [ new Padding( diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index e738a8d67b..bed17891de 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -14,6 +14,8 @@ import 'tabs.dart'; import 'theme.dart'; import 'typography.dart'; +final Object _kDefaultHeroTag = new Object(); + /// A widget that can appear at the bottom of an [AppBar]. The [Scaffold] uses /// the bottom widget's [bottomHeight] to handle layout for /// [AppBarBehavior.scroll] and [AppBarBehavior.under]. @@ -73,6 +75,7 @@ class AppBar extends StatelessWidget { this.textTheme, this.padding: EdgeInsets.zero, this.centerTitle, + this.heroTag, double expandedHeight, double collapsedHeight }) : _expandedHeight = expandedHeight, @@ -153,6 +156,11 @@ class AppBar extends StatelessWidget { /// Defaults to being adapted to the current [TargetPlatform]. final bool centerTitle; + /// The tag to apply to the app bar's [Hero] widget. + /// + /// Defaults to a tag that matches other app bars. + final Object heroTag; + final double _expandedHeight; final double _collapsedHeight; @@ -169,6 +177,7 @@ class AppBar extends StatelessWidget { Brightness brightness, TextTheme textTheme, EdgeInsets padding, + Object heroTag, double expandedHeight, double collapsedHeight }) { @@ -185,6 +194,7 @@ class AppBar extends StatelessWidget { iconTheme: iconTheme ?? this.iconTheme, textTheme: textTheme ?? this.textTheme, padding: padding ?? this.padding, + heroTag: heroTag ?? this.heroTag, expandedHeight: expandedHeight ?? this._expandedHeight, collapsedHeight: collapsedHeight ?? this._collapsedHeight ); @@ -365,13 +375,17 @@ class AppBar extends StatelessWidget { ); } - appBar = new Material( - color: backgroundColor ?? themeData.primaryColor, - elevation: elevation, - child: appBar + return new Hero( + tag: heroTag ?? _kDefaultHeroTag, + child: new Material( + color: backgroundColor ?? themeData.primaryColor, + elevation: elevation, + child: new Align( + alignment: FractionalOffset.topCenter, + child: appBar + ) + ) ); - - return appBar; } @override diff --git a/packages/flutter/lib/src/material/flexible_space_bar.dart b/packages/flutter/lib/src/material/flexible_space_bar.dart index 4f896e9501..d700c08a02 100644 --- a/packages/flutter/lib/src/material/flexible_space_bar.dart +++ b/packages/flutter/lib/src/material/flexible_space_bar.dart @@ -6,7 +6,6 @@ import 'dart:math' as math; import 'package:flutter/widgets.dart'; -import 'debug.dart'; import 'constants.dart'; import 'scaffold.dart'; import 'theme.dart'; @@ -58,7 +57,20 @@ class FlexibleSpaceBar extends StatefulWidget { } class _FlexibleSpaceBarState extends State { - Animation _scaffoldAnimation; + final ProxyAnimation _scaffoldAnimation = new ProxyAnimation(); + double _lastAppBarHeight; + + @override + void initState() { + super.initState(); + _scaffoldAnimation.addListener(_handleTick); + } + + @override + void dispose() { + _scaffoldAnimation.removeListener(_handleTick); + super.dispose(); + } void _handleTick() { setState(() { @@ -66,13 +78,6 @@ class _FlexibleSpaceBarState extends State { }); } - @override - void deactivate() { - _scaffoldAnimation?.removeListener(_handleTick); - _scaffoldAnimation = null; - super.deactivate(); - } - bool _getEffectiveCenterTitle(ThemeData theme) { if (config.centerTitle != null) return config.centerTitle; @@ -88,16 +93,18 @@ class _FlexibleSpaceBarState extends State { @override Widget build(BuildContext context) { - assert(debugCheckHasScaffold(context)); final double statusBarHeight = MediaQuery.of(context).padding.top; final ScaffoldState scaffold = Scaffold.of(context); - _scaffoldAnimation ??= scaffold.appBarAnimation..addListener(_handleTick); - final double appBarHeight = scaffold.appBarHeight + statusBarHeight; + if (scaffold != null) { + _scaffoldAnimation.parent ??= scaffold.appBarAnimation; + _lastAppBarHeight = scaffold.appBarHeight; + } + final double appBarHeight = (_lastAppBarHeight ?? kToolBarHeight) + statusBarHeight; final double toolBarHeight = kToolBarHeight + statusBarHeight; final List children = []; // background image - if (config.background != null) { + if (config.background != null && scaffold != null) { final double fadeStart = (appBarHeight - toolBarHeight * 2.0) / appBarHeight; final double fadeEnd = (appBarHeight - toolBarHeight) / appBarHeight; final CurvedAnimation opacityCurve = new CurvedAnimation( diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 6754c5062e..35939b789d 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -6,6 +6,11 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; +final FractionalOffsetTween _kMaterialPageTransitionTween = new FractionalOffsetTween( + begin: FractionalOffset.bottomLeft, + end: FractionalOffset.topLeft +); + class _MaterialPageTransition extends AnimatedWidget { _MaterialPageTransition({ Key key, @@ -13,28 +18,17 @@ class _MaterialPageTransition extends AnimatedWidget { this.child }) : super( key: key, - animation: new CurvedAnimation(parent: animation, curve: Curves.easeOut) + animation: _kMaterialPageTransitionTween.animate(new CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn)) ); final Widget child; - final Tween _position = new Tween( - begin: const Point(0.0, 75.0), - end: Point.origin - ); - @override Widget build(BuildContext context) { - Point position = _position.evaluate(animation); - Matrix4 transform = new Matrix4.identity() - ..translate(position.x, position.y); - return new Transform( - transform: transform, - // TODO(ianh): tell the transform to be un-transformed for hit testing - child: new Opacity( - opacity: animation.value, - child: child - ) + // TODO(ianh): tell the transform to be un-transformed for hit testing + return new SlideTransition( + position: animation, + child: child ); } } diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 50d7c8b618..a85f2d8deb 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -779,7 +779,8 @@ class _TabBarState extends ScrollableState> implements TabBarSelect super.initState(); scrollBehavior.isScrollable = config.isScrollable; _initSelection(TabBarSelection.of(context)); - _lastSelectedIndex = _selection.index; + if (_selection != null) + _lastSelectedIndex = _selection.index; } @override @@ -969,7 +970,7 @@ class _TabBarState extends ScrollableState> implements TabBarSelect setState(() { _tabBarSize = tabBarSize; _tabWidths = tabWidths; - _indicatorRect = _tabIndicatorRect(_selection.index); + _indicatorRect = _selection != null ? _tabIndicatorRect(_selection.index) : Rect.zero; _updateScrollBehavior(); }); } @@ -981,7 +982,7 @@ class _TabBarState extends ScrollableState> implements TabBarSelect // render object via our return value. _viewportSize = dimensions.containerSize; _updateScrollBehavior(); - if (config.isScrollable) + if (config.isScrollable && _selection != null) scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll); return scrollOffsetToPixelDelta(scrollOffset); } diff --git a/packages/flutter/lib/src/widgets/editable.dart b/packages/flutter/lib/src/widgets/editable.dart index 9ce9d295ea..e2f1bf926d 100644 --- a/packages/flutter/lib/src/widgets/editable.dart +++ b/packages/flutter/lib/src/widgets/editable.dart @@ -419,7 +419,7 @@ class RawInputLineState extends ScrollableState { if (focused) { _selectionOverlay.update(config.value); } else { - _selectionOverlay.hide(); + _selectionOverlay?.hide(); _selectionOverlay = null; } }); diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index a564dea82a..c3ac4303bc 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -554,7 +554,7 @@ class HeroController extends NavigatorObserver { _to.offstage = false; Animation animation = _animation; - Curve curve = Curves.ease; + Curve curve = Curves.fastOutSlowIn; if (animation.status == AnimationStatus.reverse) { animation = new ReverseAnimation(animation); curve = new Interval(animation.value, 1.0, curve: curve);