Commit 628884a8 authored by Adam Barth's avatar Adam Barth Committed by GitHub

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
parent 8f3c498f
...@@ -59,11 +59,9 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -59,11 +59,9 @@ class GridDemoPhotoItem extends StatelessWidget {
appBar: new AppBar( appBar: new AppBar(
title: new Text(photo.title) title: new Text(photo.title)
), ),
body: new Material( body: new Hero(
child: new Hero( tag: photoHeroTag,
tag: photoHeroTag, child: new Image.asset(photo.assetName, fit: ImageFit.cover)
child: new Image.asset(photo.assetName, fit: ImageFit.cover)
)
) )
); );
} }
......
...@@ -325,9 +325,10 @@ class _RecipePageState extends State<_RecipePage> { ...@@ -325,9 +325,10 @@ class _RecipePageState extends State<_RecipePage> {
// adjusts based on the size of the screen. If the recipe sheet touches // adjusts based on the size of the screen. If the recipe sheet touches
// the edge of the screen, use a slightly different layout. // the edge of the screen, use a slightly different layout.
Widget _buildContainer(BuildContext context) { Widget _buildContainer(BuildContext context) {
bool isFavorite = favoriteRecipes.contains(config.recipe); final bool isFavorite = favoriteRecipes.contains(config.recipe);
Size screenSize = MediaQuery.of(context).size; final Size screenSize = MediaQuery.of(context).size;
bool fullWidth = (screenSize.width < _kRecipePageMaxWidth); final bool fullWidth = (screenSize.width < _kRecipePageMaxWidth);
final double appBarHeight = _getAppBarHeight(context);
const double fabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size const double fabHalfSize = 28.0; // TODO(mpcomplete): needs to adapt to screen size
return new Stack( return new Stack(
children: <Widget>[ children: <Widget>[
...@@ -335,6 +336,7 @@ class _RecipePageState extends State<_RecipePage> { ...@@ -335,6 +336,7 @@ class _RecipePageState extends State<_RecipePage> {
top: 0.0, top: 0.0,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
height: appBarHeight + fabHalfSize,
child: new Hero( child: new Hero(
tag: config.recipe.imagePath, tag: config.recipe.imagePath,
child: new Image.asset( child: new Image.asset(
...@@ -346,7 +348,7 @@ class _RecipePageState extends State<_RecipePage> { ...@@ -346,7 +348,7 @@ class _RecipePageState extends State<_RecipePage> {
new ScrollableViewport( new ScrollableViewport(
child: new RepaintBoundary( child: new RepaintBoundary(
child: new Padding( child: new Padding(
padding: new EdgeInsets.only(top: _getAppBarHeight(context)), padding: new EdgeInsets.only(top: appBarHeight),
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new Padding( new Padding(
......
...@@ -14,6 +14,8 @@ import 'tabs.dart'; ...@@ -14,6 +14,8 @@ import 'tabs.dart';
import 'theme.dart'; import 'theme.dart';
import 'typography.dart'; import 'typography.dart';
final Object _kDefaultHeroTag = new Object();
/// A widget that can appear at the bottom of an [AppBar]. The [Scaffold] uses /// A widget that can appear at the bottom of an [AppBar]. The [Scaffold] uses
/// the bottom widget's [bottomHeight] to handle layout for /// the bottom widget's [bottomHeight] to handle layout for
/// [AppBarBehavior.scroll] and [AppBarBehavior.under]. /// [AppBarBehavior.scroll] and [AppBarBehavior.under].
...@@ -73,6 +75,7 @@ class AppBar extends StatelessWidget { ...@@ -73,6 +75,7 @@ class AppBar extends StatelessWidget {
this.textTheme, this.textTheme,
this.padding: EdgeInsets.zero, this.padding: EdgeInsets.zero,
this.centerTitle, this.centerTitle,
this.heroTag,
double expandedHeight, double expandedHeight,
double collapsedHeight double collapsedHeight
}) : _expandedHeight = expandedHeight, }) : _expandedHeight = expandedHeight,
...@@ -153,6 +156,11 @@ class AppBar extends StatelessWidget { ...@@ -153,6 +156,11 @@ class AppBar extends StatelessWidget {
/// Defaults to being adapted to the current [TargetPlatform]. /// Defaults to being adapted to the current [TargetPlatform].
final bool centerTitle; 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 _expandedHeight;
final double _collapsedHeight; final double _collapsedHeight;
...@@ -169,6 +177,7 @@ class AppBar extends StatelessWidget { ...@@ -169,6 +177,7 @@ class AppBar extends StatelessWidget {
Brightness brightness, Brightness brightness,
TextTheme textTheme, TextTheme textTheme,
EdgeInsets padding, EdgeInsets padding,
Object heroTag,
double expandedHeight, double expandedHeight,
double collapsedHeight double collapsedHeight
}) { }) {
...@@ -185,6 +194,7 @@ class AppBar extends StatelessWidget { ...@@ -185,6 +194,7 @@ class AppBar extends StatelessWidget {
iconTheme: iconTheme ?? this.iconTheme, iconTheme: iconTheme ?? this.iconTheme,
textTheme: textTheme ?? this.textTheme, textTheme: textTheme ?? this.textTheme,
padding: padding ?? this.padding, padding: padding ?? this.padding,
heroTag: heroTag ?? this.heroTag,
expandedHeight: expandedHeight ?? this._expandedHeight, expandedHeight: expandedHeight ?? this._expandedHeight,
collapsedHeight: collapsedHeight ?? this._collapsedHeight collapsedHeight: collapsedHeight ?? this._collapsedHeight
); );
...@@ -365,13 +375,17 @@ class AppBar extends StatelessWidget { ...@@ -365,13 +375,17 @@ class AppBar extends StatelessWidget {
); );
} }
appBar = new Material( return new Hero(
color: backgroundColor ?? themeData.primaryColor, tag: heroTag ?? _kDefaultHeroTag,
elevation: elevation, child: new Material(
child: appBar color: backgroundColor ?? themeData.primaryColor,
elevation: elevation,
child: new Align(
alignment: FractionalOffset.topCenter,
child: appBar
)
)
); );
return appBar;
} }
@override @override
......
...@@ -6,7 +6,6 @@ import 'dart:math' as math; ...@@ -6,7 +6,6 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'debug.dart';
import 'constants.dart'; import 'constants.dart';
import 'scaffold.dart'; import 'scaffold.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -58,7 +57,20 @@ class FlexibleSpaceBar extends StatefulWidget { ...@@ -58,7 +57,20 @@ class FlexibleSpaceBar extends StatefulWidget {
} }
class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
Animation<double> _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() { void _handleTick() {
setState(() { setState(() {
...@@ -66,13 +78,6 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { ...@@ -66,13 +78,6 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
}); });
} }
@override
void deactivate() {
_scaffoldAnimation?.removeListener(_handleTick);
_scaffoldAnimation = null;
super.deactivate();
}
bool _getEffectiveCenterTitle(ThemeData theme) { bool _getEffectiveCenterTitle(ThemeData theme) {
if (config.centerTitle != null) if (config.centerTitle != null)
return config.centerTitle; return config.centerTitle;
...@@ -88,16 +93,18 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> { ...@@ -88,16 +93,18 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasScaffold(context));
final double statusBarHeight = MediaQuery.of(context).padding.top; final double statusBarHeight = MediaQuery.of(context).padding.top;
final ScaffoldState scaffold = Scaffold.of(context); final ScaffoldState scaffold = Scaffold.of(context);
_scaffoldAnimation ??= scaffold.appBarAnimation..addListener(_handleTick); if (scaffold != null) {
final double appBarHeight = scaffold.appBarHeight + statusBarHeight; _scaffoldAnimation.parent ??= scaffold.appBarAnimation;
_lastAppBarHeight = scaffold.appBarHeight;
}
final double appBarHeight = (_lastAppBarHeight ?? kToolBarHeight) + statusBarHeight;
final double toolBarHeight = kToolBarHeight + statusBarHeight; final double toolBarHeight = kToolBarHeight + statusBarHeight;
final List<Widget> children = <Widget>[]; final List<Widget> children = <Widget>[];
// background image // background image
if (config.background != null) { if (config.background != null && scaffold != null) {
final double fadeStart = (appBarHeight - toolBarHeight * 2.0) / appBarHeight; final double fadeStart = (appBarHeight - toolBarHeight * 2.0) / appBarHeight;
final double fadeEnd = (appBarHeight - toolBarHeight) / appBarHeight; final double fadeEnd = (appBarHeight - toolBarHeight) / appBarHeight;
final CurvedAnimation opacityCurve = new CurvedAnimation( final CurvedAnimation opacityCurve = new CurvedAnimation(
......
...@@ -6,6 +6,11 @@ import 'dart:async'; ...@@ -6,6 +6,11 @@ import 'dart:async';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
final FractionalOffsetTween _kMaterialPageTransitionTween = new FractionalOffsetTween(
begin: FractionalOffset.bottomLeft,
end: FractionalOffset.topLeft
);
class _MaterialPageTransition extends AnimatedWidget { class _MaterialPageTransition extends AnimatedWidget {
_MaterialPageTransition({ _MaterialPageTransition({
Key key, Key key,
...@@ -13,28 +18,17 @@ class _MaterialPageTransition extends AnimatedWidget { ...@@ -13,28 +18,17 @@ class _MaterialPageTransition extends AnimatedWidget {
this.child this.child
}) : super( }) : super(
key: key, key: key,
animation: new CurvedAnimation(parent: animation, curve: Curves.easeOut) animation: _kMaterialPageTransitionTween.animate(new CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn))
); );
final Widget child; final Widget child;
final Tween<Point> _position = new Tween<Point>(
begin: const Point(0.0, 75.0),
end: Point.origin
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Point position = _position.evaluate(animation); // TODO(ianh): tell the transform to be un-transformed for hit testing
Matrix4 transform = new Matrix4.identity() return new SlideTransition(
..translate(position.x, position.y); position: animation,
return new Transform( child: child
transform: transform,
// TODO(ianh): tell the transform to be un-transformed for hit testing
child: new Opacity(
opacity: animation.value,
child: child
)
); );
} }
} }
......
...@@ -779,7 +779,8 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect ...@@ -779,7 +779,8 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
super.initState(); super.initState();
scrollBehavior.isScrollable = config.isScrollable; scrollBehavior.isScrollable = config.isScrollable;
_initSelection(TabBarSelection.of(context)); _initSelection(TabBarSelection.of(context));
_lastSelectedIndex = _selection.index; if (_selection != null)
_lastSelectedIndex = _selection.index;
} }
@override @override
...@@ -969,7 +970,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect ...@@ -969,7 +970,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
setState(() { setState(() {
_tabBarSize = tabBarSize; _tabBarSize = tabBarSize;
_tabWidths = tabWidths; _tabWidths = tabWidths;
_indicatorRect = _tabIndicatorRect(_selection.index); _indicatorRect = _selection != null ? _tabIndicatorRect(_selection.index) : Rect.zero;
_updateScrollBehavior(); _updateScrollBehavior();
}); });
} }
...@@ -981,7 +982,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect ...@@ -981,7 +982,7 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
// render object via our return value. // render object via our return value.
_viewportSize = dimensions.containerSize; _viewportSize = dimensions.containerSize;
_updateScrollBehavior(); _updateScrollBehavior();
if (config.isScrollable) if (config.isScrollable && _selection != null)
scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll); scrollTo(_centeredTabScrollOffset(_selection.index), duration: _kTabBarScroll);
return scrollOffsetToPixelDelta(scrollOffset); return scrollOffsetToPixelDelta(scrollOffset);
} }
......
...@@ -419,7 +419,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -419,7 +419,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
if (focused) { if (focused) {
_selectionOverlay.update(config.value); _selectionOverlay.update(config.value);
} else { } else {
_selectionOverlay.hide(); _selectionOverlay?.hide();
_selectionOverlay = null; _selectionOverlay = null;
} }
}); });
......
...@@ -554,7 +554,7 @@ class HeroController extends NavigatorObserver { ...@@ -554,7 +554,7 @@ class HeroController extends NavigatorObserver {
_to.offstage = false; _to.offstage = false;
Animation<double> animation = _animation; Animation<double> animation = _animation;
Curve curve = Curves.ease; Curve curve = Curves.fastOutSlowIn;
if (animation.status == AnimationStatus.reverse) { if (animation.status == AnimationStatus.reverse) {
animation = new ReverseAnimation(animation); animation = new ReverseAnimation(animation);
curve = new Interval(animation.value, 1.0, curve: curve); curve = new Interval(animation.value, 1.0, curve: curve);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment