// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'theme.dart'; // Slides the page upwards and fades it in, starting from 1/4 screen // below the top. class _FadeUpwardsPageTransition extends StatelessWidget { _FadeUpwardsPageTransition({ Key key, @required Animation<double> routeAnimation, // The route's linear 0.0 - 1.0 animation. @required this.child, }) : _positionAnimation = routeAnimation.drive(_bottomUpTween.chain(_fastOutSlowInTween)), _opacityAnimation = routeAnimation.drive(_easeInTween), super(key: key); // Fractional offset from 1/4 screen below the top to fully on screen. static final Tween<Offset> _bottomUpTween = Tween<Offset>( begin: const Offset(0.0, 0.25), end: Offset.zero, ); static final Animatable<double> _fastOutSlowInTween = CurveTween(curve: Curves.fastOutSlowIn); static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn); final Animation<Offset> _positionAnimation; final Animation<double> _opacityAnimation; final Widget child; @override Widget build(BuildContext context) { return SlideTransition( position: _positionAnimation, // TODO(ianh): tell the transform to be un-transformed for hit testing child: FadeTransition( opacity: _opacityAnimation, child: child, ), ); } } // This transition is intended to match the default for Android P. class _OpenUpwardsPageTransition extends StatelessWidget { const _OpenUpwardsPageTransition({ Key key, this.animation, this.secondaryAnimation, this.child, }) : super(key: key); // The new page slides upwards just a little as its clip // rectangle exposes the page from bottom to top. static final Tween<Offset> _primaryTranslationTween = Tween<Offset>( begin: const Offset(0.0, 0.05), end: Offset.zero, ); // The old page slides upwards a little as the new page appears. static final Tween<Offset> _secondaryTranslationTween = Tween<Offset>( begin: Offset.zero, end: const Offset(0.0, -0.025), ); // The scrim obscures the old page by becoming increasingly opaque. static final Tween<double> _scrimOpacityTween = Tween<double>( begin: 0.0, end: 0.25, ); // Used by all of the transition animations. static const Curve _transitionCurve = Cubic(0.20, 0.00, 0.00, 1.00); final Animation<double> animation; final Animation<double> secondaryAnimation; final Widget child; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final Size size = constraints.biggest; final CurvedAnimation primaryAnimation = CurvedAnimation( parent: animation, curve: _transitionCurve, reverseCurve: _transitionCurve.flipped, ); // Gradually expose the new page from bottom to top. final Animation<double> clipAnimation = Tween<double>( begin: 0.0, end: size.height, ).animate(primaryAnimation); final Animation<double> opacityAnimation = _scrimOpacityTween.animate(primaryAnimation); final Animation<Offset> primaryTranslationAnimation = _primaryTranslationTween.animate(primaryAnimation); final Animation<Offset> secondaryTranslationAnimation = _secondaryTranslationTween.animate( CurvedAnimation( parent: secondaryAnimation, curve: _transitionCurve, reverseCurve: _transitionCurve.flipped, ), ); return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget child) { return Container( color: Colors.black.withOpacity(opacityAnimation.value), alignment: Alignment.bottomLeft, child: ClipRect( child: SizedBox( height: clipAnimation.value, child: OverflowBox( alignment: Alignment.bottomLeft, maxHeight: size.height, child: child, ), ), ), ); }, child: AnimatedBuilder( animation: secondaryAnimation, child: FractionalTranslation( translation: primaryTranslationAnimation.value, child: child, ), builder: (BuildContext context, Widget child) { return FractionalTranslation( translation: secondaryTranslationAnimation.value, child: child, ); }, ), ); }, ); } } /// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page /// transition animation. /// /// Apps can configure the map of builders for [ThemeData.platformTheme] /// to customize the default [MaterialPageRoute] page transition animation /// for different platforms. /// /// See also: /// /// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition. /// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition /// that's similar to the one provided by Android P. /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page /// transition that matches native iOS page transitions. abstract class PageTransitionsBuilder { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const PageTransitionsBuilder(); /// Wraps the child with one or more transition widgets which define how [route] /// arrives on and leaves the screen. /// /// The [MaterialPageRoute.buildTransitions] method looks up the current /// current [PageTransitionsTheme] with `Theme.of(context).pageTransitionsTheme` /// and delegates to this method with a [PageTransitionsBuilder] based /// on the theme's [ThemeData.platform]. Widget buildTransitions<T>( PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ); } /// Used by [PageTransitionsTheme] to define a default [MaterialPageRoute] page /// transition animation. /// /// The default animation fades the new page in while translating it upwards, /// starting from about 25% below the top of the screen. /// /// See also: /// /// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition /// that's similar to the one provided by Android P. /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page /// transition that matches native iOS page transitions. class FadeUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { /// Construct a [FadeUpwardsPageTransitionsBuilder]. const FadeUpwardsPageTransitionsBuilder(); @override Widget buildTransitions<T>( PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return _FadeUpwardsPageTransition(routeAnimation: animation, child: child); } } /// Used by [PageTransitionsTheme] to define a vertical [MaterialPageRoute] page /// transition animation that looks like the default page transition /// used on Android P. /// /// See also: /// /// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition. /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page /// transition that matches native iOS page transitions. class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder { /// Construct a [OpenUpwardsPageTransitionsBuilder]. const OpenUpwardsPageTransitionsBuilder(); @override Widget buildTransitions<T>( PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return _OpenUpwardsPageTransition( animation: animation, secondaryAnimation: secondaryAnimation, child: child, ); } } /// Used by [PageTransitionsTheme] to define a horizontal [MaterialPageRoute] /// page transition animation that matches native iOS page transitions. /// /// See also: /// /// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition. /// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition /// that's similar to the one provided by Android P. class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder { /// Construct a [CupertinoPageTransitionsBuilder]. const CupertinoPageTransitionsBuilder(); @override Widget buildTransitions<T>( PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return CupertinoPageRoute.buildPageTransitions<T>(route, context, animation, secondaryAnimation, child); } } /// Defines the page transition animations used by [MaterialPageRoute] /// for different [TargetPlatform]s. /// /// The [MaterialPageRoute.buildTransitions] method looks up the current /// current [PageTransitionsTheme] with `Theme.of(context).pageTransitionsTheme` /// and delegates to [buildTransitions]. /// /// If a builder with a matching platform is not found, then the /// [FadeUpwardsPageTransitionsBuilder] is used. /// /// See also: /// /// * [ThemeData.pageTransitionsTheme], which defines the default page /// transitions for the overall theme. /// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition. /// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition /// that's similar to the one provided by Android P. /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page /// transition that matches native iOS page transitions. @immutable class PageTransitionsTheme extends Diagnosticable { /// Construct a PageTransitionsTheme. /// /// By default the list of builders is: [FadeUpwardsPageTransitionsBuilder], /// [CupertinoPageTransitionsBuilder] for [TargetPlatform.android] /// and [TargetPlatform.iOS] respectively. const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders }) : _builders = builders; static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{ TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), }; /// The [PageTransitionsBuilder]s supported by this theme. Map<TargetPlatform, PageTransitionsBuilder> get builders => _builders ?? _defaultBuilders; final Map<TargetPlatform, PageTransitionsBuilder> _builders; /// Delegates to the builder for the current [ThemeData.platform] /// or [FadeUpwardsPageTransitionsBuilder]. /// /// [MaterialPageRoute.buildTransitions] delegates to this method. Widget buildTransitions<T>( PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { TargetPlatform platform = Theme.of(context).platform; if (CupertinoPageRoute.isPopGestureInProgress(route)) platform = TargetPlatform.iOS; final PageTransitionsBuilder matchingBuilder = builders[platform] ?? const FadeUpwardsPageTransitionsBuilder(); return matchingBuilder.buildTransitions<T>(route, context, animation, secondaryAnimation, child); } // Just used to the builders Map to a list with one PageTransitionsBuilder per platform // for the operator == overload. List<PageTransitionsBuilder> _all(Map<TargetPlatform, PageTransitionsBuilder> builders) { return TargetPlatform.values.map((TargetPlatform platform) => builders[platform]).toList(); } @override bool operator ==(dynamic other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; final PageTransitionsTheme typedOther = other; if (identical(builders, other.builders)) return true; return listEquals<PageTransitionsBuilder>(_all(builders), _all(typedOther.builders)); } @override int get hashCode => hashList(_all(builders)); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add( DiagnosticsProperty<Map<TargetPlatform, PageTransitionsBuilder>>( 'builders', builders, defaultValue: PageTransitionsTheme._defaultBuilders, ), ); } }