floating_action_button_location.dart 19.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
// 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/widgets.dart';

import 'scaffold.dart';

// TODO(hmuller): should be device dependent.
/// The margin that a [FloatingActionButton] should leave between it and the
/// edge of the screen.
14
///
15 16
/// [FloatingActionButtonLocation.endFloat] uses this to set the appropriate margin
/// between the [FloatingActionButton] and the end of the screen.
17
const double kFloatingActionButtonMargin = 16.0;
18 19

/// The amount of time the [FloatingActionButton] takes to transition in or out.
20 21
///
/// The [Scaffold] uses this to set the duration of [FloatingActionButton]
22
/// motion, entrance, and exit animations.
23
const Duration kFloatingActionButtonSegue = Duration(milliseconds: 200);
24 25

/// The fraction of a circle the [FloatingActionButton] should turn when it enters.
26
///
27 28 29 30 31
/// Its value corresponds to 0.125 of a full circle, equivalent to 45 degrees or pi/4 radians.
const double kFloatingActionButtonTurnInterval = 0.125;

/// An object that defines a position for the [FloatingActionButton]
/// based on the [Scaffold]'s [ScaffoldPrelayoutGeometry].
32
///
33 34 35
/// Flutter provides [FloatingActionButtonLocation]s for the common
/// [FloatingActionButton] placements in Material Design applications. These
/// locations are available as static members of this class.
36
///
37
/// See also:
38
///
39 40 41
///  * [FloatingActionButton], which is a circular button typically shown in the
///    bottom right corner of the app.
///  * [FloatingActionButtonAnimator], which is used to animate the
42
///    [Scaffold.floatingActionButton] from one [FloatingActionButtonLocation] to
43
///    another.
44
///  * [ScaffoldPrelayoutGeometry], the geometry that
45 46 47 48 49 50 51
///    [FloatingActionButtonLocation]s use to position the [FloatingActionButton].
abstract class FloatingActionButtonLocation {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const FloatingActionButtonLocation();

  /// End-aligned [FloatingActionButton], floating at the bottom of the screen.
52
  ///
53
  /// This is the default alignment of [FloatingActionButton]s in Material applications.
54
  static const FloatingActionButtonLocation endFloat = _EndFloatFloatingActionButtonLocation();
55 56

  /// Centered [FloatingActionButton], floating at the bottom of the screen.
57
  static const FloatingActionButtonLocation centerFloat = _CenterFloatFloatingActionButtonLocation();
58

59 60 61 62 63 64 65 66 67 68
  /// End-aligned [FloatingActionButton], floating over the
  /// [Scaffold.bottomNavigationBar] so that the center of the floating
  /// action button lines up with the top of the bottom navigation bar.
  ///
  /// If the value of [Scaffold.bottomNavigationBar] is a [BottomAppBar],
  /// the bottom app bar can include a "notch" in its shape that accommodates
  /// the overlapping floating action button.
  ///
  /// This is unlikely to be a useful location for apps that lack a bottom
  /// navigation bar.
69
  static const FloatingActionButtonLocation endDocked = _EndDockedFloatingActionButtonLocation();
70 71 72 73 74 75

  /// Center-aligned [FloatingActionButton], floating over the
  /// [Scaffold.bottomNavigationBar] so that the center of the floating
  /// action button lines up with the top of the bottom navigation bar.
  ///
  /// If the value of [Scaffold.bottomNavigationBar] is a [BottomAppBar],
David Shuckerow's avatar
David Shuckerow committed
76
  /// the bottom app bar can include a "notch" in its shape that accommodates
77 78 79 80
  /// the overlapping floating action button.
  ///
  /// This is unlikely to be a useful location for apps that lack a bottom
  /// navigation bar.
81
  static const FloatingActionButtonLocation centerDocked = _CenterDockedFloatingActionButtonLocation();
82

83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
  /// Start-aligned [FloatingActionButton], floating over the transition between
  /// the [Scaffold.appBar] and the [Scaffold.body].
  ///
  /// To align a floating action button with [FloatingActionButton.mini] set to
  /// true with [CircleAvatar]s in the [ListTile.leading] slots of [ListTile]s
  /// in a [ListView] in the [Scaffold.body], consider using [miniStartTop].
  ///
  /// This is unlikely to be a useful location for apps that lack a top [AppBar]
  /// or that use a [SliverAppBar] in the scaffold body itself.
  static const FloatingActionButtonLocation startTop = _StartTopFloatingActionButtonLocation();

  /// Start-aligned [FloatingActionButton], floating over the transition between
  /// the [Scaffold.appBar] and the [Scaffold.body], optimized for mini floating
  /// action buttons.
  ///
  /// This is intended to be used with [FloatingActionButton.mini] set to true,
  /// so that the floating action button appears to align with [CircleAvatar]s
  /// in the [ListTile.leading] slot of a [ListTile] in a [ListView] in the
  /// [Scaffold.body].
  ///
  /// This is unlikely to be a useful location for apps that lack a top [AppBar]
  /// or that use a [SliverAppBar] in the scaffold body itself.
  static const FloatingActionButtonLocation miniStartTop = _MiniStartTopFloatingActionButtonLocation();

  /// End-aligned [FloatingActionButton], floating over the transition between
  /// the [Scaffold.appBar] and the [Scaffold.body].
  ///
  /// This is unlikely to be a useful location for apps that lack a top [AppBar]
  /// or that use a [SliverAppBar] in the scaffold body itself.
  static const FloatingActionButtonLocation endTop = _EndTopFloatingActionButtonLocation();

114
  /// Places the [FloatingActionButton] based on the [Scaffold]'s layout.
115
  ///
116 117 118 119 120 121 122 123 124 125 126
  /// This uses a [ScaffoldPrelayoutGeometry], which the [Scaffold] constructs
  /// during its layout phase after it has laid out every widget it can lay out
  /// except the [FloatingActionButton]. The [Scaffold] uses the [Offset]
  /// returned from this method to position the [FloatingActionButton] and
  /// complete its layout.
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry);

  @override
  String toString() => '$runtimeType';
}

127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
double _leftOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) {
  return kFloatingActionButtonMargin
       + scaffoldGeometry.minInsets.left
       - offset;
}

double _rightOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) {
  return scaffoldGeometry.scaffoldSize.width
       - kFloatingActionButtonMargin
       - scaffoldGeometry.minInsets.right
       - scaffoldGeometry.floatingActionButtonSize.width
       + offset;
}

double _endOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) {
  assert(scaffoldGeometry.textDirection != null);
  switch (scaffoldGeometry.textDirection) {
    case TextDirection.rtl:
      return _leftOffset(scaffoldGeometry, offset: offset);
    case TextDirection.ltr:
      return _rightOffset(scaffoldGeometry, offset: offset);
  }
  return null;
}

double _startOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) {
  assert(scaffoldGeometry.textDirection != null);
  switch (scaffoldGeometry.textDirection) {
    case TextDirection.rtl:
      return _rightOffset(scaffoldGeometry, offset: offset);
    case TextDirection.ltr:
      return _leftOffset(scaffoldGeometry, offset: offset);
  }
  return null;
}

class _CenterFloatFloatingActionButtonLocation extends FloatingActionButtonLocation {
  const _CenterFloatFloatingActionButtonLocation();
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    // Compute the x-axis offset.
    final double fabX = (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0;

    // Compute the y-axis offset.
    final double contentBottom = scaffoldGeometry.contentBottom;
    final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height;
    final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height;
    final double snackBarHeight = scaffoldGeometry.snackBarSize.height;
    double fabY = contentBottom - fabHeight - kFloatingActionButtonMargin;
    if (snackBarHeight > 0.0)
      fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
    if (bottomSheetHeight > 0.0)
      fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0);

182
    return Offset(fabX, fabY);
183
  }
184 185 186

  @override
  String toString() => 'FloatingActionButtonLocation.centerFloat';
187 188
}

189 190
class _EndFloatFloatingActionButtonLocation extends FloatingActionButtonLocation {
  const _EndFloatFloatingActionButtonLocation();
191 192 193 194

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    // Compute the x-axis offset.
195
    final double fabX = _endOffset(scaffoldGeometry);
196 197 198 199 200 201 202 203 204 205 206 207 208

    // Compute the y-axis offset.
    final double contentBottom = scaffoldGeometry.contentBottom;
    final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height;
    final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height;
    final double snackBarHeight = scaffoldGeometry.snackBarSize.height;

    double fabY = contentBottom - fabHeight - kFloatingActionButtonMargin;
    if (snackBarHeight > 0.0)
      fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
    if (bottomSheetHeight > 0.0)
      fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0);

209
    return Offset(fabX, fabY);
210
  }
211 212 213

  @override
  String toString() => 'FloatingActionButtonLocation.endFloat';
214 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
// Provider of common logic for [FloatingActionButtonLocation]s that
// dock to the [BottomAppBar].
abstract class _DockedFloatingActionButtonLocation extends FloatingActionButtonLocation {
  const _DockedFloatingActionButtonLocation();

  // Positions the Y coordinate of the [FloatingActionButton] at a height
  // where it docks to the [BottomAppBar].
  @protected
  double getDockedY(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    final double contentBottom = scaffoldGeometry.contentBottom;
    final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height;
    final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height;
    final double snackBarHeight = scaffoldGeometry.snackBarSize.height;

    double fabY = contentBottom - fabHeight / 2.0;
    // The FAB should sit with a margin between it and the snack bar.
    if (snackBarHeight > 0.0)
      fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
    // The FAB should sit with its center in front of the top of the bottom sheet.
    if (bottomSheetHeight > 0.0)
      fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0);

    final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight;
    return math.min(maxFabY, fabY);
  }
}

class _EndDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation {
  const _EndDockedFloatingActionButtonLocation();

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
248
    final double fabX = _endOffset(scaffoldGeometry);
249
    return Offset(fabX, getDockedY(scaffoldGeometry));
250
  }
251 252 253

  @override
  String toString() => 'FloatingActionButtonLocation.endDocked';
254 255 256 257 258 259 260 261
}

class _CenterDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation {
  const _CenterDockedFloatingActionButtonLocation();

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    final double fabX = (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0;
262
    return Offset(fabX, getDockedY(scaffoldGeometry));
263
  }
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310

  @override
  String toString() => 'FloatingActionButtonLocation.centerDocked';
}

double _straddleAppBar(ScaffoldPrelayoutGeometry scaffoldGeometry) {
  final double fabHalfHeight = scaffoldGeometry.floatingActionButtonSize.height / 2.0;
  return scaffoldGeometry.contentTop - fabHalfHeight;
}

class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
  const _StartTopFloatingActionButtonLocation();

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    return Offset(_startOffset(scaffoldGeometry), _straddleAppBar(scaffoldGeometry));
  }

  @override
  String toString() => 'FloatingActionButtonLocation.startTop';
}

class _MiniStartTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
  const _MiniStartTopFloatingActionButtonLocation();

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    // We have to offset the FAB by four pixels because the FAB itself _adds_
    // four pixels in every direction in order to have a hit target area of 48
    // pixels in each dimension, despite being a circle of radius 40.
    return Offset(_startOffset(scaffoldGeometry, offset: 4.0), _straddleAppBar(scaffoldGeometry));
  }

  @override
  String toString() => 'FloatingActionButtonLocation.miniStartTop';
}

class _EndTopFloatingActionButtonLocation extends FloatingActionButtonLocation {
  const _EndTopFloatingActionButtonLocation();

  @override
  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
    return Offset(_endOffset(scaffoldGeometry), _straddleAppBar(scaffoldGeometry));
  }

  @override
  String toString() => 'FloatingActionButtonLocation.endTop';
311 312
}

313
/// Provider of animations to move the [FloatingActionButton] between [FloatingActionButtonLocation]s.
314
///
315 316 317 318 319 320 321
/// The [Scaffold] uses [Scaffold.floatingActionButtonAnimator] to define:
///
///  * The [Offset] of the [FloatingActionButton] between the old and new
///    [FloatingActionButtonLocation]s as part of the transition animation.
///  * An [Animation] to scale the [FloatingActionButton] during the transition.
///  * An [Animation] to rotate the [FloatingActionButton] during the transition.
///  * Where to start a new animation from if an animation is interrupted.
322
///
323
/// See also:
324
///
325 326
///  * [FloatingActionButton], which is a circular button typically shown in the
///    bottom right corner of the app.
327 328
///  * [FloatingActionButtonLocation], which the [Scaffold] uses to place the
///    [Scaffold.floatingActionButton] within the [Scaffold]'s layout.
329 330 331 332 333
abstract class FloatingActionButtonAnimator {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const FloatingActionButtonAnimator();

334
  /// Moves the [FloatingActionButton] by scaling out and then in at a new
335
  /// [FloatingActionButtonLocation].
336
  ///
337 338
  /// This animator shrinks the [FloatingActionButton] down until it disappears, then
  /// grows it back to full size at its new [FloatingActionButtonLocation].
339
  ///
340
  /// This is the default [FloatingActionButton] motion animation.
341
  static const FloatingActionButtonAnimator scaling = _ScalingFabMotionAnimator();
342 343 344

  /// Gets the [FloatingActionButton]'s position relative to the origin of the
  /// [Scaffold] based on [progress].
345 346
  ///
  /// [begin] is the [Offset] provided by the previous
347
  /// [FloatingActionButtonLocation].
348 349
  ///
  /// [end] is the [Offset] provided by the new
350
  /// [FloatingActionButtonLocation].
351
  ///
352 353 354
  /// [progress] is the current progress of the transition animation.
  /// When [progress] is 0.0, the returned [Offset] should be equal to [begin].
  /// when [progress] is 1.0, the returned [Offset] should be equal to [end].
355
  Offset getOffset({ @required Offset begin, @required Offset end, @required double progress });
356 357

  /// Animates the scale of the [FloatingActionButton].
358
  ///
359
  /// The animation should both start and end with a value of 1.0.
360
  ///
361 362
  /// For example, to create an animation that linearly scales out and then back in,
  /// you could join animations that pass each other:
363
  ///
364 365 366 367
  /// ```dart
  ///   @override
  ///   Animation<double> getScaleAnimation({@required Animation<double> parent}) {
  ///     // The animations will cross at value 0, and the train will return to 1.0.
368
  ///     return TrainHoppingAnimation(
369 370 371 372 373
  ///       Tween<double>(begin: 1.0, end: -1.0).animate(parent),
  ///       Tween<double>(begin: -1.0, end: 1.0).animate(parent),
  ///     );
  ///   }
  /// ```
374
  Animation<double> getScaleAnimation({ @required Animation<double> parent });
375 376

  /// Animates the rotation of [Scaffold.floatingActionButton].
377
  ///
378
  /// The animation should both start and end with a value of 0.0 or 1.0.
379
  ///
380 381
  /// The animation values are a fraction of a full circle, with 0.0 and 1.0
  /// corresponding to 0 and 360 degrees, while 0.5 corresponds to 180 degrees.
382 383
  ///
  /// For example, to create a rotation animation that rotates the
384
  /// [FloatingActionButton] through a full circle:
385
  ///
386
  /// ```dart
387 388 389 390
  /// @override
  /// Animation<double> getRotationAnimation({@required Animation<double> parent}) {
  ///   return Tween<double>(begin: 0.0, end: 1.0).animate(parent);
  /// }
391
  /// ```
392
  Animation<double> getRotationAnimation({ @required Animation<double> parent });
393 394

  /// Gets the progress value to restart a motion animation from when the animation is interrupted.
395
  ///
396
  /// [previousValue] is the value of the animation before it was interrupted.
397
  ///
398 399
  /// The restart of the animation will affect all three parts of the motion animation:
  /// offset animation, scale animation, and rotation animation.
400
  ///
401 402
  /// An interruption triggers if the [Scaffold] is given a new [FloatingActionButtonLocation]
  /// while it is still animating a transition between two previous [FloatingActionButtonLocation]s.
403
  ///
404 405 406 407 408 409 410 411 412 413 414 415
  /// A sensible default is usually 0.0, which is the same as restarting
  /// the animation from the beginning, regardless of the original state of the animation.
  double getAnimationRestart(double previousValue) => 0.0;

  @override
  String toString() => '$runtimeType';
}

class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator {
  const _ScalingFabMotionAnimator();

  @override
416
  Offset getOffset({ Offset begin, Offset end, double progress }) {
417 418 419 420 421 422 423 424
    if (progress < 0.5) {
      return begin;
    } else {
      return end;
    }
  }

  @override
425
  Animation<double> getScaleAnimation({ Animation<double> parent }) {
426 427
    // Animate the scale down from 1 to 0 in the first half of the animation
    // then from 0 back to 1 in the second half.
428
    const Curve curve = Interval(0.5, 1.0, curve: Curves.ease);
429
    return _AnimationSwap<double>(
430 431
      ReverseAnimation(parent.drive(CurveTween(curve: curve.flipped))),
      parent.drive(CurveTween(curve: curve)),
432 433 434 435 436
      parent,
      0.5,
    );
  }

437 438 439 440 441 442 443 444 445
  // Because we only see the last half of the rotation tween,
  // it needs to go twice as far.
  static final Animatable<double> _rotationTween = Tween<double>(
    begin: 1.0 - kFloatingActionButtonTurnInterval * 2.0,
    end: 1.0,
  );

  static final Animatable<double> _thresholdCenterTween = CurveTween(curve: const Threshold(0.5));

446
  @override
447
  Animation<double> getRotationAnimation({ Animation<double> parent }) {
448
    // This rotation will turn on the way in, but not on the way out.
449
    return _AnimationSwap<double>(
450 451
      parent.drive(_rotationTween),
      ReverseAnimation(parent.drive(_thresholdCenterTween)),
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
      parent,
      0.5,
    );
  }

  // If the animation was just starting, we'll continue from where we left off.
  // If the animation was finishing, we'll treat it as if we were starting at that point in reverse.
  // This avoids a size jump during the animation.
  @override
  double getAnimationRestart(double previousValue) => math.min(1.0 - previousValue, previousValue);
}

/// An animation that swaps from one animation to the next when the [parent] passes [swapThreshold].
///
/// The [value] of this animation is the value of [first] when [parent.value] < [swapThreshold]
/// and the value of [next] otherwise.
class _AnimationSwap<T> extends CompoundAnimation<T> {
  /// Creates an [_AnimationSwap].
  ///
471
  /// Both arguments must be non-null. Either can be an [_AnimationSwap] itself
472
  /// to combine multiple animations.
473
  _AnimationSwap(Animation<T> first, Animation<T> next, this.parent, this.swapThreshold) : super(first: first, next: next);
474 475 476 477 478 479

  final Animation<double> parent;
  final double swapThreshold;

  @override
  T get value => parent.value < swapThreshold ? first.value : next.value;
480
}