| // 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 'dart:math' as math; | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/material.dart'; | |
| const double _kFrontHeadingHeight = 32.0; // front layer beveled rectangle | |
| const double _kFrontClosedHeight = 92.0; // front layer height when closed | |
| const double _kBackAppBarHeight = 56.0; // back layer (options) appbar height | |
| // The size of the front layer heading's left and right beveled corners. | |
| final Animatable<BorderRadius> _kFrontHeadingBevelRadius = BorderRadiusTween( | |
| begin: const BorderRadius.only( | |
| topLeft: Radius.circular(12.0), | |
| topRight: Radius.circular(12.0), | |
| ), | |
| end: const BorderRadius.only( | |
| topLeft: Radius.circular(_kFrontHeadingHeight), | |
| topRight: Radius.circular(_kFrontHeadingHeight), | |
| ), | |
| ); | |
| 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 _CrossFadeTransition extends AnimatedWidget { | |
| const _CrossFadeTransition({ | |
| Key key, | |
| this.alignment = Alignment.center, | |
| Animation<double> progress, | |
| this.child0, | |
| this.child1, | |
| }) : super(key: key, listenable: progress); | |
| final AlignmentGeometry alignment; | |
| final Widget child0; | |
| final Widget child1; | |
| @override | |
| Widget build(BuildContext context) { | |
| final Animation<double> progress = listenable as Animation<double>; | |
| final double opacity1 = CurvedAnimation( | |
| parent: ReverseAnimation(progress), | |
| curve: const Interval(0.5, 1.0), | |
| ).value; | |
| final double opacity2 = CurvedAnimation( | |
| parent: progress, | |
| curve: const Interval(0.5, 1.0), | |
| ).value; | |
| return Stack( | |
| alignment: alignment, | |
| children: <Widget>[ | |
| Opacity( | |
| opacity: opacity1, | |
| child: Semantics( | |
| scopesRoute: true, | |
| explicitChildNodes: true, | |
| child: child1, | |
| ), | |
| ), | |
| Opacity( | |
| opacity: opacity2, | |
| child: Semantics( | |
| scopesRoute: true, | |
| explicitChildNodes: true, | |
| child: child0, | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| class _BackAppBar extends StatelessWidget { | |
| const _BackAppBar({ | |
| Key key, | |
| this.leading = const SizedBox(width: 56.0), | |
| @required this.title, | |
| this.trailing, | |
| }) : assert(leading != null), assert(title != null), super(key: key); | |
| final Widget leading; | |
| final Widget title; | |
| final Widget trailing; | |
| @override | |
| Widget build(BuildContext context) { | |
| final ThemeData theme = Theme.of(context); | |
| return IconTheme.merge( | |
| data: theme.primaryIconTheme, | |
| child: DefaultTextStyle( | |
| style: theme.primaryTextTheme.headline6, | |
| child: SizedBox( | |
| height: _kBackAppBarHeight, | |
| child: Row( | |
| children: <Widget>[ | |
| Container( | |
| alignment: Alignment.center, | |
| width: 56.0, | |
| child: leading, | |
| ), | |
| Expanded( | |
| child: title, | |
| ), | |
| if (trailing != null) | |
| Container( | |
| alignment: Alignment.center, | |
| width: 56.0, | |
| child: trailing, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class Backdrop extends StatefulWidget { | |
| const Backdrop({ | |
| this.frontAction, | |
| this.frontTitle, | |
| this.frontHeading, | |
| this.frontLayer, | |
| this.backTitle, | |
| this.backLayer, | |
| }); | |
| final Widget frontAction; | |
| final Widget frontTitle; | |
| final Widget frontLayer; | |
| final Widget frontHeading; | |
| final Widget backTitle; | |
| final Widget backLayer; | |
| @override | |
| _BackdropState createState() => _BackdropState(); | |
| } | |
| class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin { | |
| final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); | |
| AnimationController _controller; | |
| Animation<double> _frontOpacity; | |
| static final Animatable<double> _frontOpacityTween = Tween<double>(begin: 0.2, end: 1.0) | |
| .chain(CurveTween(curve: const Interval(0.0, 0.4, curve: Curves.easeInOut))); | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController( | |
| duration: const Duration(milliseconds: 300), | |
| value: 1.0, | |
| vsync: this, | |
| ); | |
| _frontOpacity = _controller.drive(_frontOpacityTween); | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| double get _backdropHeight { | |
| // Warning: this can be safely called from the event handlers but it may | |
| // not be called at build time. | |
| final RenderBox renderBox = _backdropKey.currentContext.findRenderObject() as RenderBox; | |
| return math.max(0.0, renderBox.size.height - _kBackAppBarHeight - _kFrontClosedHeight); | |
| } | |
| void _handleDragUpdate(DragUpdateDetails details) { | |
| _controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta); | |
| } | |
| void _handleDragEnd(DragEndDetails details) { | |
| if (_controller.isAnimating || _controller.status == AnimationStatus.completed) | |
| return; | |
| final double flingVelocity = details.velocity.pixelsPerSecond.dy / _backdropHeight; | |
| if (flingVelocity < 0.0) | |
| _controller.fling(velocity: math.max(2.0, -flingVelocity)); | |
| else if (flingVelocity > 0.0) | |
| _controller.fling(velocity: math.min(-2.0, -flingVelocity)); | |
| else | |
| _controller.fling(velocity: _controller.value < 0.5 ? -2.0 : 2.0); | |
| } | |
| void _toggleFrontLayer() { | |
| final AnimationStatus status = _controller.status; | |
| final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward; | |
| _controller.fling(velocity: isOpen ? -2.0 : 2.0); | |
| } | |
| Widget _buildStack(BuildContext context, BoxConstraints constraints) { | |
| final Animation<RelativeRect> frontRelativeRect = _controller.drive(RelativeRectTween( | |
| begin: RelativeRect.fromLTRB(0.0, constraints.biggest.height - _kFrontClosedHeight, 0.0, 0.0), | |
| end: const RelativeRect.fromLTRB(0.0, _kBackAppBarHeight, 0.0, 0.0), | |
| )); | |
| return Stack( | |
| key: _backdropKey, | |
| children: <Widget>[ | |
| // Back layer | |
| Column( | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: <Widget>[ | |
| _BackAppBar( | |
| leading: widget.frontAction, | |
| title: _CrossFadeTransition( | |
| progress: _controller, | |
| alignment: AlignmentDirectional.centerStart, | |
| child0: Semantics(namesRoute: true, child: widget.frontTitle), | |
| child1: Semantics(namesRoute: true, child: widget.backTitle), | |
| ), | |
| trailing: IconButton( | |
| onPressed: _toggleFrontLayer, | |
| tooltip: 'Toggle options page', | |
| icon: AnimatedIcon( | |
| icon: AnimatedIcons.close_menu, | |
| progress: _controller, | |
| ), | |
| ), | |
| ), | |
| Expanded( | |
| child: _TappableWhileStatusIs( | |
| AnimationStatus.dismissed, | |
| controller: _controller, | |
| child: Visibility( | |
| child: widget.backLayer, | |
| visible: _controller.status != AnimationStatus.completed, | |
| maintainState: true, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| // Front layer | |
| PositionedTransition( | |
| rect: frontRelativeRect, | |
| child: AnimatedBuilder( | |
| animation: _controller, | |
| builder: (BuildContext context, Widget child) { | |
| return PhysicalShape( | |
| elevation: 12.0, | |
| color: Theme.of(context).canvasColor, | |
| clipper: ShapeBorderClipper( | |
| shape: BeveledRectangleBorder( | |
| borderRadius: _kFrontHeadingBevelRadius.transform(_controller.value), | |
| ), | |
| ), | |
| clipBehavior: Clip.antiAlias, | |
| child: child, | |
| ); | |
| }, | |
| child: _TappableWhileStatusIs( | |
| AnimationStatus.completed, | |
| controller: _controller, | |
| child: FadeTransition( | |
| opacity: _frontOpacity, | |
| child: widget.frontLayer, | |
| ), | |
| ), | |
| ), | |
| ), | |
| // The front "heading" is a (typically transparent) widget that's stacked on | |
| // top of, and at the top of, the front layer. It adds support for dragging | |
| // the front layer up and down and for opening and closing the front layer | |
| // with a tap. It may obscure part of the front layer's topmost child. | |
| if (widget.frontHeading != null) | |
| PositionedTransition( | |
| rect: frontRelativeRect, | |
| child: ExcludeSemantics( | |
| child: Container( | |
| alignment: Alignment.topLeft, | |
| child: GestureDetector( | |
| behavior: HitTestBehavior.opaque, | |
| onTap: _toggleFrontLayer, | |
| onVerticalDragUpdate: _handleDragUpdate, | |
| onVerticalDragEnd: _handleDragEnd, | |
| child: widget.frontHeading, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return LayoutBuilder(builder: _buildStack); | |
| } | |
| } |