// 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 '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) { return AbsorbPointer( absorbing: !_active, child: widget.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; 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 List<Widget> children = <Widget>[ Container( alignment: Alignment.center, width: 56.0, child: leading, ), Expanded( child: title, ), ]; if (trailing != null) { children.add( Container( alignment: Alignment.center, width: 56.0, child: trailing, ), ); } final ThemeData theme = Theme.of(context); return IconTheme.merge( data: theme.primaryIconTheme, child: DefaultTextStyle( style: theme.primaryTextTheme.title, child: SizedBox( height: _kBackAppBarHeight, child: Row(children: children), ), ), ); } } 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(); 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), )); final List<Widget> layers = <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: 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) { layers.add( PositionedTransition( rect: frontRelativeRect, child: ExcludeSemantics( child: Container( alignment: Alignment.topLeft, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _toggleFrontLayer, onVerticalDragUpdate: _handleDragUpdate, onVerticalDragEnd: _handleDragEnd, child: widget.frontHeading, ), ), ), ), ); } return Stack( key: _backdropKey, children: layers, ); } @override Widget build(BuildContext context) { return LayoutBuilder(builder: _buildStack); } }