| // Copyright 2014 The Flutter 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, |
| ); |
| }, |
| ), |
| ); |
| }, |
| ); |
| } |
| } |
| |
| // Zooms and fades a new page in, zooming out the previous page. This transition |
| // is designed to match the Android 10 activity transition. |
| class _ZoomPageTransition extends StatefulWidget { |
| const _ZoomPageTransition({ |
| Key key, |
| this.animation, |
| this.secondaryAnimation, |
| this.child, |
| }) : super(key: key); |
| |
| // The scrim obscures the old page by becoming increasingly opaque. |
| static final Tween<double> _scrimOpacityTween = Tween<double>( |
| begin: 0.0, |
| end: 0.60, |
| ); |
| |
| // A curve sequence that is similar to the 'fastOutExtraSlowIn' curve used in |
| // the native transition. |
| static final List<TweenSequenceItem<double>> fastOutExtraSlowInTweenSequenceItems = <TweenSequenceItem<double>>[ |
| TweenSequenceItem<double>( |
| tween: Tween<double>(begin: 0.0, end: 0.4) |
| .chain(CurveTween(curve: const Cubic(0.05, 0.0, 0.133333, 0.06))), |
| weight: 0.166666, |
| ), |
| TweenSequenceItem<double>( |
| tween: Tween<double>(begin: 0.4, end: 1.0) |
| .chain(CurveTween(curve: const Cubic(0.208333, 0.82, 0.25, 1.0))), |
| weight: 1.0 - 0.166666, |
| ), |
| ]; |
| static final TweenSequence<double> _scaleCurveSequence = TweenSequence<double>(fastOutExtraSlowInTweenSequenceItems); |
| static final FlippedTweenSequence _flippedScaleCurveSequence = FlippedTweenSequence(fastOutExtraSlowInTweenSequenceItems); |
| |
| final Animation<double> animation; |
| final Animation<double> secondaryAnimation; |
| final Widget child; |
| |
| @override |
| __ZoomPageTransitionState createState() => __ZoomPageTransitionState(); |
| } |
| |
| class __ZoomPageTransitionState extends State<_ZoomPageTransition> { |
| AnimationStatus _currentAnimationStatus; |
| AnimationStatus _lastAnimationStatus; |
| |
| @override |
| void initState() { |
| super.initState(); |
| widget.animation.addStatusListener((AnimationStatus animationStatus) { |
| _lastAnimationStatus = _currentAnimationStatus; |
| _currentAnimationStatus = animationStatus; |
| }); |
| } |
| |
| // This check ensures that the animation reverses the original animation if |
| // the transition were interruped midway. This prevents a disjointed |
| // experience since the reverse animation uses different fade and scaling |
| // curves. |
| bool get _transitionWasInterrupted { |
| bool wasInProgress = false; |
| bool isInProgress = false; |
| |
| switch (_currentAnimationStatus) { |
| case AnimationStatus.completed: |
| case AnimationStatus.dismissed: |
| isInProgress = false; |
| break; |
| case AnimationStatus.forward: |
| case AnimationStatus.reverse: |
| isInProgress = true; |
| break; |
| } |
| switch (_lastAnimationStatus) { |
| case AnimationStatus.completed: |
| case AnimationStatus.dismissed: |
| wasInProgress = false; |
| break; |
| case AnimationStatus.forward: |
| case AnimationStatus.reverse: |
| wasInProgress = true; |
| break; |
| } |
| return wasInProgress && isInProgress; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final Animation<double> _forwardScrimOpacityAnimation = widget.animation.drive( |
| _ZoomPageTransition._scrimOpacityTween |
| .chain(CurveTween(curve: const Interval(0.2075, 0.4175)))); |
| |
| final Animation<double> _forwardEndScreenScaleTransition = widget.animation.drive( |
| Tween<double>(begin: 0.85, end: 1.00) |
| .chain(_ZoomPageTransition._scaleCurveSequence)); |
| |
| final Animation<double> _forwardStartScreenScaleTransition = widget.secondaryAnimation.drive( |
| Tween<double>(begin: 1.00, end: 1.05) |
| .chain(_ZoomPageTransition._scaleCurveSequence)); |
| |
| final Animation<double> _forwardEndScreenFadeTransition = widget.animation.drive( |
| Tween<double>(begin: 0.0, end: 1.00) |
| .chain(CurveTween(curve: const Interval(0.125, 0.250)))); |
| |
| final Animation<double> _reverseEndScreenScaleTransition = widget.secondaryAnimation.drive( |
| Tween<double>(begin: 1.00, end: 1.10) |
| .chain(_ZoomPageTransition._flippedScaleCurveSequence)); |
| |
| final Animation<double> _reverseStartScreenScaleTransition = widget.animation.drive( |
| Tween<double>(begin: 0.9, end: 1.0) |
| .chain(_ZoomPageTransition._flippedScaleCurveSequence)); |
| |
| final Animation<double> _reverseStartScreenFadeTransition = widget.animation.drive( |
| Tween<double>(begin: 0.0, end: 1.00) |
| .chain(CurveTween(curve: const Interval(1 - 0.2075, 1 - 0.0825)))); |
| |
| return AnimatedBuilder( |
| animation: widget.animation, |
| builder: (BuildContext context, Widget child) { |
| if (widget.animation.status == AnimationStatus.forward || _transitionWasInterrupted) { |
| return Container( |
| color: Colors.black.withOpacity(_forwardScrimOpacityAnimation.value), |
| child: FadeTransition( |
| opacity: _forwardEndScreenFadeTransition, |
| child: ScaleTransition( |
| scale: _forwardEndScreenScaleTransition, |
| child: child, |
| ), |
| ), |
| ); |
| } else if (widget.animation.status == AnimationStatus.reverse) { |
| return ScaleTransition( |
| scale: _reverseStartScreenScaleTransition, |
| child: FadeTransition( |
| opacity: _reverseStartScreenFadeTransition, |
| child: child, |
| ), |
| ); |
| } |
| return child; |
| }, |
| child: AnimatedBuilder( |
| animation: widget.secondaryAnimation, |
| builder: (BuildContext context, Widget child) { |
| if (widget.secondaryAnimation.status == AnimationStatus.forward || _transitionWasInterrupted) { |
| return ScaleTransition( |
| scale: _forwardStartScreenScaleTransition, |
| child: child, |
| ); |
| } else if (widget.secondaryAnimation.status == AnimationStatus.reverse) { |
| return ScaleTransition( |
| scale: _reverseEndScreenScaleTransition, |
| child: child, |
| ); |
| } |
| return child; |
| }, |
| child: widget.child, |
| ), |
| ); |
| } |
| } |
| |
| /// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page |
| /// transition animation. |
| /// |
| /// Apps can configure the map of builders for [ThemeData.pageTransitionsTheme] |
| /// 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. |
| /// * [ZoomPageTransitionsBuilder], which defines a page transition similar |
| /// to the one provided in Android 10. |
| /// * [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. |
| /// * [ZoomPageTransitionsBuilder], which defines a page transition similar |
| /// to the one provided in Android 10. |
| /// * [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. |
| /// * [ZoomPageTransitionsBuilder], which defines a page transition similar |
| /// to the one provided in Android 10. |
| /// * [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 zooming [MaterialPageRoute] page |
| /// transition animation that looks like the default page transition used on |
| /// Android 10. |
| /// |
| /// See also: |
| /// |
| /// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition. |
| /// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition |
| /// similar to the one provided by Android P. |
| /// * [CupertinoPageTransitionsBuilder], which defines a horizontal page |
| /// transition that matches native iOS page transitions. |
| class ZoomPageTransitionsBuilder extends PageTransitionsBuilder { |
| /// Construct a [ZoomPageTransitionsBuilder]. |
| const ZoomPageTransitionsBuilder(); |
| |
| @override |
| Widget buildTransitions<T>( |
| PageRoute<T> route, |
| BuildContext context, |
| Animation<double> animation, |
| Animation<double> secondaryAnimation, |
| Widget child, |
| ) { |
| return _ZoomPageTransition( |
| 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. |
| /// * [ZoomPageTransitionsBuilder], which defines a page transition similar |
| /// to the one provided in Android 10. |
| 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 with Diagnosticable { |
| /// Construct a PageTransitionsTheme. |
| /// |
| /// By default the list of builders is: [FadeUpwardsPageTransitionsBuilder] |
| /// for [TargetPlatform.android], and [CupertinoPageTransitionsBuilder] for |
| /// [TargetPlatform.iOS] and [TargetPlatform.macOS]. |
| const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders }) : _builders = builders; |
| |
| static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{ |
| TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), |
| TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), |
| TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(), |
| TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), |
| TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), |
| }; |
| |
| /// 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 ==(Object other) { |
| if (identical(this, other)) |
| return true; |
| if (other.runtimeType != runtimeType) |
| return false; |
| if (other is PageTransitionsTheme && identical(builders, other.builders)) |
| return true; |
| return other is PageTransitionsTheme |
| && listEquals<PageTransitionsBuilder>(_all(other.builders), _all(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, |
| ), |
| ); |
| } |
| } |