backdrop.dart 9.87 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// 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
11
const double _kFrontClosedHeight = 92.0; // front layer height when closed
12 13 14
const double _kBackAppBarHeight = 56.0; // back layer (options) appbar height

// The size of the front layer heading's left and right beveled corners.
15
final Animatable<BorderRadius> _kFrontHeadingBevelRadius = BorderRadiusTween(
16
  begin: const BorderRadius.only(
17 18
    topLeft: Radius.circular(12.0),
    topRight: Radius.circular(12.0),
19 20
  ),
  end: const BorderRadius.only(
21 22
    topLeft: Radius.circular(_kFrontHeadingHeight),
    topRight: Radius.circular(_kFrontHeadingHeight),
23 24 25
  ),
);

26
class _TappableWhileStatusIs extends StatefulWidget {
27 28
  const _TappableWhileStatusIs(
    this.status, {
29 30 31 32 33 34 35 36 37 38
    Key key,
    this.controller,
    this.child,
  }) : super(key: key);

  final AnimationController controller;
  final AnimationStatus status;
  final Widget child;

  @override
39
  _TappableWhileStatusIsState createState() => _TappableWhileStatusIsState();
40 41
}

42 43
class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
  bool _active;
44 45 46 47 48

  @override
  void initState() {
    super.initState();
    widget.controller.addStatusListener(_handleStatusChange);
49
    _active = widget.controller.status == widget.status;
50 51 52 53 54 55 56 57
  }

  @override
  void dispose() {
    widget.controller.removeStatusListener(_handleStatusChange);
    super.dispose();
  }

58 59 60
  void _handleStatusChange(AnimationStatus status) {
    final bool value = widget.controller.status == widget.status;
    if (_active != value) {
61
      setState(() {
62
        _active = value;
63 64 65 66 67 68
      });
    }
  }

  @override
  Widget build(BuildContext context) {
69
    return AbsorbPointer(
70
      absorbing: !_active,
71
      child: widget.child,
72 73 74 75 76 77 78
    );
  }
}

class _CrossFadeTransition extends AnimatedWidget {
  const _CrossFadeTransition({
    Key key,
79
    this.alignment = Alignment.center,
80 81 82 83 84 85 86 87 88 89 90 91 92
    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;

93 94
    final double opacity1 = CurvedAnimation(
      parent: ReverseAnimation(progress),
95 96 97
      curve: const Interval(0.5, 1.0),
    ).value;

98
    final double opacity2 = CurvedAnimation(
99 100 101 102
      parent: progress,
      curve: const Interval(0.5, 1.0),
    ).value;

103
    return Stack(
104 105
      alignment: alignment,
      children: <Widget>[
106
        Opacity(
107
          opacity: opacity1,
108
          child: Semantics(
109 110 111
            scopesRoute: true,
            explicitChildNodes: true,
            child: child1,
112 113
          ),
        ),
114
        Opacity(
115
          opacity: opacity2,
116
          child: Semantics(
117 118 119
            scopesRoute: true,
            explicitChildNodes: true,
            child: child0,
120 121 122 123 124 125 126 127 128 129
          ),
        ),
      ],
    );
  }
}

class _BackAppBar extends StatelessWidget {
  const _BackAppBar({
    Key key,
130
    this.leading = const SizedBox(width: 56.0),
131 132 133 134 135 136 137 138 139 140 141
    @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>[
142
      Container(
143 144 145 146
        alignment: Alignment.center,
        width: 56.0,
        child: leading,
      ),
147
      Expanded(
148 149 150 151 152 153
        child: title,
      ),
    ];

    if (trailing != null) {
      children.add(
154
        Container(
155 156 157 158 159 160 161 162 163 164 165
          alignment: Alignment.center,
          width: 56.0,
          child: trailing,
        ),
      );
    }

    final ThemeData theme = Theme.of(context);

    return IconTheme.merge(
      data: theme.primaryIconTheme,
166
      child: DefaultTextStyle(
167
        style: theme.primaryTextTheme.title,
168
        child: SizedBox(
169
          height: _kBackAppBarHeight,
170
          child: Row(children: children),
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
        ),
      ),
    );
  }
}

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
195
  _BackdropState createState() => _BackdropState();
196 197 198
}

class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin {
199
  final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
200
  AnimationController _controller;
201
  Animation<double> _frontOpacity;
202

203 204 205
  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)));

206 207 208
  @override
  void initState() {
    super.initState();
209
    _controller = AnimationController(
210 211 212 213
      duration: const Duration(milliseconds: 300),
      value: 1.0,
      vsync: this,
    );
214
    _frontOpacity = _controller.drive(_frontOpacityTween);
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
  }

  @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) {
254
    final Animation<RelativeRect> frontRelativeRect = _controller.drive(RelativeRectTween(
255
      begin: RelativeRect.fromLTRB(0.0, constraints.biggest.height - _kFrontClosedHeight, 0.0, 0.0),
256
      end: const RelativeRect.fromLTRB(0.0, _kBackAppBarHeight, 0.0, 0.0),
257
    ));
258

259 260
    final List<Widget> layers = <Widget>[
      // Back layer
261
      Column(
262 263
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
264
          _BackAppBar(
265
            leading: widget.frontAction,
266
            title: _CrossFadeTransition(
267 268
              progress: _controller,
              alignment: AlignmentDirectional.centerStart,
269 270
              child0: Semantics(namesRoute: true, child: widget.frontTitle),
              child1: Semantics(namesRoute: true, child: widget.backTitle),
271
            ),
272
            trailing: IconButton(
273 274
              onPressed: _toggleFrontLayer,
              tooltip: 'Toggle options page',
275
              icon: AnimatedIcon(
276 277
                icon: AnimatedIcons.close_menu,
                progress: _controller,
278 279
              ),
            ),
280
          ),
281 282
          Expanded(
            child: Visibility(
283
              child: widget.backLayer,
284 285
              visible: _controller.status != AnimationStatus.completed,
              maintainState: true,
286
            ),
287 288 289 290
          ),
        ],
      ),
      // Front layer
291
      PositionedTransition(
292
        rect: frontRelativeRect,
293
        child: AnimatedBuilder(
294 295
          animation: _controller,
          builder: (BuildContext context, Widget child) {
296
            return PhysicalShape(
297 298
              elevation: 12.0,
              color: Theme.of(context).canvasColor,
299 300
              clipper: ShapeBorderClipper(
                shape: BeveledRectangleBorder(
301
                  borderRadius: _kFrontHeadingBevelRadius.transform(_controller.value),
302
                ),
303
              ),
304
              clipBehavior: Clip.antiAlias,
305 306 307
              child: child,
            );
          },
308
          child: _TappableWhileStatusIs(
309 310
            AnimationStatus.completed,
            controller: _controller,
311
            child: FadeTransition(
312 313
              opacity: _frontOpacity,
              child: widget.frontLayer,
314 315 316
            ),
          ),
        ),
317 318 319 320 321 322 323 324 325
      ),
    ];

    // 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(
326
        PositionedTransition(
327
          rect: frontRelativeRect,
328 329
          child: ExcludeSemantics(
            child: Container(
330
              alignment: Alignment.topLeft,
331
              child: GestureDetector(
332 333 334 335 336 337
                behavior: HitTestBehavior.opaque,
                onTap: _toggleFrontLayer,
                onVerticalDragUpdate: _handleDragUpdate,
                onVerticalDragEnd: _handleDragEnd,
                child: widget.frontHeading,
              ),
338 339 340
            ),
          ),
        ),
341 342 343
      );
    }

344
    return Stack(
345 346
      key: _backdropKey,
      children: layers,
347 348 349 350 351
    );
  }

  @override
  Widget build(BuildContext context) {
352
    return LayoutBuilder(builder: _buildStack);
353 354
  }
}