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 893587db03a..9a8a0733ed8 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 e1eb2335173..86f686da502 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 e738a8d67bb..bed17891de8 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 4f896e95014..d700c08a024 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 6754c5062e7..35939b789d4 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 50d7c8b6188..a85f2d8debd 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 9ce9d295eab..e2f1bf926d7 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 a564dea82a8..c3ac4303bc5 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);