| // 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/material.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import 'package:flutter_gallery/demo/shrine/login.dart'; |
| |
| const Cubic _kAccelerateCurve = Cubic(0.548, 0.0, 0.757, 0.464); |
| const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0); |
| const double _kPeakVelocityTime = 0.248210; |
| const double _kPeakVelocityProgress = 0.379146; |
| |
| class _TappableWhileStatusIs extends StatefulWidget { |
| const _TappableWhileStatusIs( |
| this.status, { |
| Key key, |
| this.controller, |
| this.child, |
| }) : super(key: key); |
| |
| final AnimationController controller; |
| final AnimationStatus status; |
| final Widget child; |
| |
| @override |
| _TappableWhileStatusIsState createState() => _TappableWhileStatusIsState(); |
| } |
| |
| class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> { |
| bool _active; |
| |
| @override |
| void initState() { |
| super.initState(); |
| widget.controller.addStatusListener(_handleStatusChange); |
| _active = widget.controller.status == widget.status; |
| } |
| |
| @override |
| void dispose() { |
| widget.controller.removeStatusListener(_handleStatusChange); |
| super.dispose(); |
| } |
| |
| void _handleStatusChange(AnimationStatus status) { |
| final bool value = widget.controller.status == widget.status; |
| if (_active != value) { |
| setState(() { |
| _active = value; |
| }); |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| Widget child = AbsorbPointer( |
| absorbing: !_active, |
| child: widget.child, |
| ); |
| |
| if (!_active) { |
| child = FocusScope( |
| canRequestFocus: false, |
| debugLabel: '$_TappableWhileStatusIs', |
| child: child, |
| ); |
| } |
| return child; |
| } |
| } |
| |
| class _FrontLayer extends StatelessWidget { |
| const _FrontLayer({ |
| Key key, |
| this.onTap, |
| this.child, |
| }) : super(key: key); |
| |
| final VoidCallback onTap; |
| final Widget child; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Material( |
| elevation: 16.0, |
| shape: const BeveledRectangleBorder( |
| borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)), |
| ), |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.stretch, |
| children: <Widget>[ |
| GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: onTap, |
| child: Container( |
| height: 40.0, |
| alignment: AlignmentDirectional.centerStart, |
| ), |
| ), |
| Expanded( |
| child: child, |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class _BackdropTitle extends AnimatedWidget { |
| const _BackdropTitle({ |
| Key key, |
| Listenable listenable, |
| this.onPress, |
| @required this.frontTitle, |
| @required this.backTitle, |
| }) : assert(frontTitle != null), |
| assert(backTitle != null), |
| super(key: key, listenable: listenable); |
| |
| final Function onPress; |
| final Widget frontTitle; |
| final Widget backTitle; |
| |
| @override |
| Widget build(BuildContext context) { |
| final Animation<double> animation = CurvedAnimation( |
| parent: listenable, |
| curve: const Interval(0.0, 0.78), |
| ); |
| |
| return DefaultTextStyle( |
| style: Theme.of(context).primaryTextTheme.title, |
| softWrap: false, |
| overflow: TextOverflow.ellipsis, |
| child: Row(children: <Widget>[ |
| // branded icon |
| SizedBox( |
| width: 72.0, |
| child: IconButton( |
| padding: const EdgeInsets.only(right: 8.0), |
| onPressed: onPress, |
| icon: Stack(children: <Widget>[ |
| Opacity( |
| opacity: animation.value, |
| child: const ImageIcon(AssetImage('packages/shrine_images/slanted_menu.png')), |
| ), |
| FractionalTranslation( |
| translation: Tween<Offset>( |
| begin: Offset.zero, |
| end: const Offset(1.0, 0.0), |
| ).evaluate(animation), |
| child: const ImageIcon(AssetImage('packages/shrine_images/diamond.png')), |
| ), |
| ]), |
| ), |
| ), |
| // Here, we do a custom cross fade between backTitle and frontTitle. |
| // This makes a smooth animation between the two texts. |
| Stack( |
| children: <Widget>[ |
| Opacity( |
| opacity: CurvedAnimation( |
| parent: ReverseAnimation(animation), |
| curve: const Interval(0.5, 1.0), |
| ).value, |
| child: FractionalTranslation( |
| translation: Tween<Offset>( |
| begin: Offset.zero, |
| end: const Offset(0.5, 0.0), |
| ).evaluate(animation), |
| child: backTitle, |
| ), |
| ), |
| Opacity( |
| opacity: CurvedAnimation( |
| parent: animation, |
| curve: const Interval(0.5, 1.0), |
| ).value, |
| child: FractionalTranslation( |
| translation: Tween<Offset>( |
| begin: const Offset(-0.25, 0.0), |
| end: Offset.zero, |
| ).evaluate(animation), |
| child: frontTitle, |
| ), |
| ), |
| ], |
| ), |
| ]), |
| ); |
| } |
| } |
| |
| /// Builds a Backdrop. |
| /// |
| /// A Backdrop widget has two layers, front and back. The front layer is shown |
| /// by default, and slides down to show the back layer, from which a user |
| /// can make a selection. The user can also configure the titles for when the |
| /// front or back layer is showing. |
| class Backdrop extends StatefulWidget { |
| const Backdrop({ |
| @required this.frontLayer, |
| @required this.backLayer, |
| @required this.frontTitle, |
| @required this.backTitle, |
| @required this.controller, |
| }) : assert(frontLayer != null), |
| assert(backLayer != null), |
| assert(frontTitle != null), |
| assert(backTitle != null), |
| assert(controller != null); |
| |
| final Widget frontLayer; |
| final Widget backLayer; |
| final Widget frontTitle; |
| final Widget backTitle; |
| final AnimationController controller; |
| |
| @override |
| _BackdropState createState() => _BackdropState(); |
| } |
| |
| class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin { |
| final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); |
| AnimationController _controller; |
| Animation<RelativeRect> _layerAnimation; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _controller = widget.controller; |
| } |
| |
| @override |
| void dispose() { |
| _controller.dispose(); |
| super.dispose(); |
| } |
| |
| bool get _frontLayerVisible { |
| final AnimationStatus status = _controller.status; |
| return status == AnimationStatus.completed || status == AnimationStatus.forward; |
| } |
| |
| void _toggleBackdropLayerVisibility() { |
| // Call setState here to update layerAnimation if that's necessary |
| setState(() { |
| _frontLayerVisible ? _controller.reverse() : _controller.forward(); |
| }); |
| } |
| |
| // _layerAnimation animates the front layer between open and close. |
| // _getLayerAnimation adjusts the values in the TweenSequence so the |
| // curve and timing are correct in both directions. |
| Animation<RelativeRect> _getLayerAnimation(Size layerSize, double layerTop) { |
| Curve firstCurve; // Curve for first TweenSequenceItem |
| Curve secondCurve; // Curve for second TweenSequenceItem |
| double firstWeight; // Weight of first TweenSequenceItem |
| double secondWeight; // Weight of second TweenSequenceItem |
| Animation<double> animation; // Animation on which TweenSequence runs |
| |
| if (_frontLayerVisible) { |
| firstCurve = _kAccelerateCurve; |
| secondCurve = _kDecelerateCurve; |
| firstWeight = _kPeakVelocityTime; |
| secondWeight = 1.0 - _kPeakVelocityTime; |
| animation = CurvedAnimation( |
| parent: _controller.view, |
| curve: const Interval(0.0, 0.78), |
| ); |
| } else { |
| // These values are only used when the controller runs from t=1.0 to t=0.0 |
| firstCurve = _kDecelerateCurve.flipped; |
| secondCurve = _kAccelerateCurve.flipped; |
| firstWeight = 1.0 - _kPeakVelocityTime; |
| secondWeight = _kPeakVelocityTime; |
| animation = _controller.view; |
| } |
| |
| return TweenSequence<RelativeRect>( |
| <TweenSequenceItem<RelativeRect>>[ |
| TweenSequenceItem<RelativeRect>( |
| tween: RelativeRectTween( |
| begin: RelativeRect.fromLTRB( |
| 0.0, |
| layerTop, |
| 0.0, |
| layerTop - layerSize.height, |
| ), |
| end: RelativeRect.fromLTRB( |
| 0.0, |
| layerTop * _kPeakVelocityProgress, |
| 0.0, |
| (layerTop - layerSize.height) * _kPeakVelocityProgress, |
| ), |
| ).chain(CurveTween(curve: firstCurve)), |
| weight: firstWeight, |
| ), |
| TweenSequenceItem<RelativeRect>( |
| tween: RelativeRectTween( |
| begin: RelativeRect.fromLTRB( |
| 0.0, |
| layerTop * _kPeakVelocityProgress, |
| 0.0, |
| (layerTop - layerSize.height) * _kPeakVelocityProgress, |
| ), |
| end: RelativeRect.fill, |
| ).chain(CurveTween(curve: secondCurve)), |
| weight: secondWeight, |
| ), |
| ], |
| ).animate(animation); |
| } |
| |
| Widget _buildStack(BuildContext context, BoxConstraints constraints) { |
| const double layerTitleHeight = 48.0; |
| final Size layerSize = constraints.biggest; |
| final double layerTop = layerSize.height - layerTitleHeight; |
| |
| _layerAnimation = _getLayerAnimation(layerSize, layerTop); |
| |
| return Stack( |
| key: _backdropKey, |
| children: <Widget>[ |
| _TappableWhileStatusIs( |
| AnimationStatus.dismissed, |
| controller: _controller, |
| child: widget.backLayer, |
| ), |
| PositionedTransition( |
| rect: _layerAnimation, |
| child: _FrontLayer( |
| onTap: _toggleBackdropLayerVisibility, |
| child: _TappableWhileStatusIs( |
| AnimationStatus.completed, |
| controller: _controller, |
| child: widget.frontLayer, |
| ), |
| ), |
| ), |
| ], |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final AppBar appBar = AppBar( |
| brightness: Brightness.light, |
| elevation: 0.0, |
| titleSpacing: 0.0, |
| title: _BackdropTitle( |
| listenable: _controller.view, |
| onPress: _toggleBackdropLayerVisibility, |
| frontTitle: widget.frontTitle, |
| backTitle: widget.backTitle, |
| ), |
| actions: <Widget>[ |
| IconButton( |
| icon: const Icon(Icons.search, semanticLabel: 'login'), |
| onPressed: () { |
| Navigator.push<void>( |
| context, |
| MaterialPageRoute<void>(builder: (BuildContext context) => LoginPage()), |
| ); |
| }, |
| ), |
| IconButton( |
| icon: const Icon(Icons.tune, semanticLabel: 'login'), |
| onPressed: () { |
| Navigator.push<void>( |
| context, |
| MaterialPageRoute<void>(builder: (BuildContext context) => LoginPage()), |
| ); |
| }, |
| ), |
| ], |
| ); |
| return Scaffold( |
| appBar: appBar, |
| body: LayoutBuilder( |
| builder: _buildStack, |
| ), |
| ); |
| } |
| } |