material.dart 19.4 KB
Newer Older
1 2 3 4
// Copyright 2015 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.

5 6 7
import 'dart:math' as math;

import 'package:flutter/rendering.dart';
8
import 'package:flutter/widgets.dart';
9
import 'package:vector_math/vector_math_64.dart';
10

11 12
import 'constants.dart';
import 'shadows.dart';
13
import 'theme.dart';
14

15 16 17 18 19
/// Signature for callback used by ink effects to obtain the rectangle for the effect.
typedef Rect RectCallback();

/// The various kinds of material in material design. Used to
/// configure the default behavior of [Material] widgets.
20 21
///
/// See also:
22 23
///
///  * [Material], in particular [Material.type]
24
///  * [kMaterialEdges]
25 26 27 28 29 30
enum MaterialType {
  /// Infinite extent using default theme canvas color.
  canvas,

  /// Rounded edges, card theme color.
  card,
31

32 33 34
  /// A circle, no color by default (used for floating action buttons).
  circle,

35
  /// Rounded edges, no color by default (used for [MaterialButton] buttons).
36 37 38 39
  button,

  /// A transparent piece of material that draws ink splashes and highlights.
  transparency
40 41
}

42 43 44 45 46 47
/// The border radii used by the various kinds of material in material design.
///
/// See also:
///
///  * [MaterialType]
///  * [Material]
48
const Map<MaterialType, double> kMaterialEdges = const <MaterialType, double>{
49 50 51 52
  MaterialType.canvas: null,
  MaterialType.card: 2.0,
  MaterialType.circle: null,
  MaterialType.button: 2.0,
53
  MaterialType.transparency: null,
54 55
};

56 57 58
/// A visual reaction on a piece of [Material] to user input.
///
/// Typically created by [MaterialInkController.splashAt].
59
abstract class InkSplash {
60 61 62
  /// The user input is confirmed.
  ///
  /// Causes the reaction to propagate faster across the material.
63
  void confirm();
64 65 66 67

  /// The user input was cancelled.
  ///
  /// Causes the reaction to gradually disappear.
68
  void cancel();
69 70

  /// Free up the resources associated with this reaction.
71 72 73
  void dispose();
}

74 75 76
/// A visual emphasis on a part of a [Material] receiving user interaction.
///
/// Typically created by [MaterialInkController.highlightAt].
77
abstract class InkHighlight {
78
  /// Start visually emphasizing this part of the material.
79
  void activate();
80 81

  /// Stop visually emphasizing this part of the material.
82
  void deactivate();
83 84

  /// Free up the resources associated with this highlight.
85
  void dispose();
86 87

  /// Whether this part of the material is being visually emphasized.
88
  bool get active;
89 90

  /// The color use to visually represent the emphasis.
91 92 93 94
  Color get color;
  void set color(Color value);
}

95 96 97
/// An interface for creating [InkSplash]s and [InkHighlight]s on a material.
///
/// Typically obtained via [Material.of].
98
abstract class MaterialInkController {
99
  /// The color of the material.
100 101 102
  Color get color;

  /// Begin a splash, centered at position relative to referenceBox.
103 104 105 106 107 108 109 110 111 112
  ///
  /// If containedInkWell is true, then the splash will be sized to fit
  /// the well rectangle, then clipped to it when drawn. The well
  /// rectangle is the box returned by rectCallback, if provided, or
  /// otherwise is the bounds of the referenceBox.
  ///
  /// If containedInkWell is false, then rectCallback should be null.
  /// The ink splash is clipped only to the edges of the [Material].
  /// This is the default.
  ///
113
  /// When the splash is removed, onRemoved will be invoked.
114 115 116 117 118 119 120 121
  InkSplash splashAt({
    RenderBox referenceBox,
    Point position,
    Color color,
    bool containedInkWell: false,
    RectCallback rectCallback,
    VoidCallback onRemoved
  });
122

123 124 125 126 127 128 129 130 131 132
  /// Begin a highlight animation. If a rectCallback is given, then it
  /// provides the highlight rectangle, otherwise, the highlight
  /// rectangle is coincident with the referenceBox.
  InkHighlight highlightAt({
    RenderBox referenceBox,
    Color color,
    BoxShape shape: BoxShape.rectangle,
    RectCallback rectCallback,
    VoidCallback onRemoved
  });
133 134 135 136 137

  /// Add an arbitrary InkFeature to this InkController.
  void addInkFeature(InkFeature feature);
}

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
/// A piece of material.
///
/// Material is the central metaphor in material design. Each piece of material
/// exists at a given elevation, which influences how that piece of material
/// visually relates to other pieces of material and how that material casts
/// shadows on other pieces of material.
///
/// Most user interface elements are either conceptually printed on a piece of
/// material or themselves made of material. Material reacts to user input using
/// [InkSplash] and [InkHighlight] effects. To trigger a reaction on the
/// material, use a [MaterialInkController] obtained via [Material.of].
///
/// If the layout changes (e.g. because there's a list on the paper, and it's
/// been scrolled), a LayoutChangedNotification must be dispatched at the
/// relevant subtree. (This in particular means that Transitions should not be
/// placed inside Material.) Otherwise, in-progress ink features (e.g., ink
/// splashes and ink highlights) won't move to account for the new layout.
///
/// See also:
///
/// * <https://www.google.com/design/spec/material-design/introduction.html>
159
class Material extends StatefulWidget {
160 161 162
  /// Creates a piece of material.
  ///
  /// Both the type and the elevation arguments are required.
163
  Material({
164
    Key key,
165
    this.child,
166
    this.type: MaterialType.canvas,
Hans Muller's avatar
Hans Muller committed
167
    this.elevation: 0,
168 169
    this.color,
    this.textStyle
170
  }) : super(key: key) {
171
    assert(type != null);
Hans Muller's avatar
Hans Muller committed
172
    assert(elevation != null);
173 174
  }

175
  /// The widget below this widget in the tree.
176
  final Widget child;
177

178 179 180
  /// The kind of material to show (e.g., card or canvas). This
  /// affects the shape of the widget, the roundness of its corners if
  /// the shape is rectangular, and the default color.
181
  final MaterialType type;
182

183
  /// The z-coordinate at which to place this material.
Hans Muller's avatar
Hans Muller committed
184
  final int elevation;
185

186
  /// The color to paint the material.
187 188 189
  ///
  /// Must be opaque. To create a transparent piece of material, use
  /// [MaterialType.transparency].
190 191
  ///
  /// By default, the color is derived from the [type] of material.
192
  final Color color;
193

194
  /// The typographical style to use for text within this material.
195
  final TextStyle textStyle;
196

197 198
  /// The ink controller from the closest instance of this class that
  /// encloses the given context.
199
  static MaterialInkController of(BuildContext context) {
200
    final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
201 202 203
    return result;
  }

204
  @override
205
  _MaterialState createState() => new _MaterialState();
206

207
  @override
208 209 210 211 212 213 214
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$type');
    description.add('elevation: $elevation');
    if (color != null)
      description.add('color: $color');
  }
215 216 217 218 219
}

class _MaterialState extends State<Material> {
  final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer');

220
  Color _getBackgroundColor(BuildContext context) {
221 222 223
    if (config.color != null)
      return config.color;
    switch (config.type) {
224
      case MaterialType.canvas:
225
        return Theme.of(context).canvasColor;
226
      case MaterialType.card:
227
        return Theme.of(context).cardColor;
228 229
      default:
        return null;
230 231 232
    }
  }

233
  @override
234
  Widget build(BuildContext context) {
235 236 237
    Color backgroundColor = _getBackgroundColor(context);
    Widget contents = config.child;
    if (contents != null) {
238
      contents = new AnimatedDefaultTextStyle(
239
        style: config.textStyle ?? Theme.of(context).textTheme.body1,
240
        duration: kThemeChangeDuration,
241 242 243
        child: contents
      );
    }
244 245 246 247
    contents = new NotificationListener<LayoutChangedNotification>(
      onNotification: (LayoutChangedNotification notification) {
        _inkFeatureRenderer.currentContext.findRenderObject().markNeedsPaint();
      },
248
      child: new _InkFeatures(
249 250
        key: _inkFeatureRenderer,
        color: backgroundColor,
251 252
        child: contents
      )
253
    );
254 255 256 257 258 259 260 261 262
    if (config.type == MaterialType.circle) {
      contents = new ClipOval(child: contents);
    } else if (kMaterialEdges[config.type] != null) {
      contents = new ClipRRect(
        xRadius: kMaterialEdges[config.type],
        yRadius: kMaterialEdges[config.type],
        child: contents
      );
    }
263 264 265 266 267 268 269
    if (config.type != MaterialType.transparency) {
      contents = new AnimatedContainer(
        curve: Curves.ease,
        duration: kThemeChangeDuration,
        decoration: new BoxDecoration(
          borderRadius: kMaterialEdges[config.type],
          boxShadow: config.elevation == 0 ? null : elevationToShadow[config.elevation],
270
          shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
271
        ),
272 273 274 275 276 277 278 279
        child: new Container(
          decoration: new BoxDecoration(
            borderRadius: kMaterialEdges[config.type],
            backgroundColor: backgroundColor,
            shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
          ),
          child: contents
        )
280 281
      );
    }
282 283 284 285
    return contents;
  }
}

286 287
const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200);
const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1);
288 289

const double _kDefaultSplashRadius = 35.0; // logical pixels
290
const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond
291 292
const double _kSplashInitialSize = 0.0; // logical pixels

293 294
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
  _RenderInkFeatures({ RenderBox child, this.color }) : super(child);
295 296 297 298

  // This is here to satisfy the MaterialInkController contract.
  // The actual painting of this color is done by a Container in the
  // MaterialState build method.
299
  @override
300 301 302 303
  Color color;

  final List<InkFeature> _inkFeatures = <InkFeature>[];

304
  @override
305 306 307 308
  InkSplash splashAt({
    RenderBox referenceBox,
    Point position,
    Color color,
309 310
    bool containedInkWell: false,
    RectCallback rectCallback,
311 312
    VoidCallback onRemoved
  }) {
313
    double radius;
314 315 316 317 318 319 320 321 322 323 324
    RectCallback clipCallback;
    if (containedInkWell) {
      Size size;
      if (rectCallback != null) {
        size = rectCallback().size;
        clipCallback = rectCallback;
      } else {
        size = referenceBox.size;
        clipCallback = () => Point.origin & size;
      }
      radius = _getSplashTargetSize(size, position);
325
    } else {
326
      assert(rectCallback == null);
327 328 329 330 331 332
      radius = _kDefaultSplashRadius;
    }
    _InkSplash splash = new _InkSplash(
      renderer: this,
      referenceBox: referenceBox,
      position: position,
333
      color: color,
334
      targetRadius: radius,
335 336
      clipCallback: clipCallback,
      repositionToReferenceBox: !containedInkWell,
337 338 339 340 341 342 343 344 345 346 347 348 349 350
      onRemoved: onRemoved
    );
    addInkFeature(splash);
    return splash;
  }

  double _getSplashTargetSize(Size bounds, Point position) {
    double d1 = (position - bounds.topLeft(Point.origin)).distance;
    double d2 = (position - bounds.topRight(Point.origin)).distance;
    double d3 = (position - bounds.bottomLeft(Point.origin)).distance;
    double d4 = (position - bounds.bottomRight(Point.origin)).distance;
    return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
  }

351
  @override
352 353 354
  InkHighlight highlightAt({
    RenderBox referenceBox,
    Color color,
355
    BoxShape shape: BoxShape.rectangle,
356
    RectCallback rectCallback,
357 358
    VoidCallback onRemoved
  }) {
359 360 361 362
    _InkHighlight highlight = new _InkHighlight(
      renderer: this,
      referenceBox: referenceBox,
      color: color,
363
      shape: shape,
364
      rectCallback: rectCallback,
365 366 367 368 369 370
      onRemoved: onRemoved
    );
    addInkFeature(highlight);
    return highlight;
  }

371
  @override
372 373 374 375 376 377 378 379 380 381 382 383 384
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
    assert(feature.renderer == this);
    assert(!_inkFeatures.contains(feature));
    _inkFeatures.add(feature);
    markNeedsPaint();
  }

  void _removeFeature(InkFeature feature) {
    _inkFeatures.remove(feature);
    markNeedsPaint();
  }

385
  @override
386 387
  bool hitTestSelf(Point position) => true;

388
  @override
389 390 391 392 393 394 395 396 397 398 399 400 401 402
  void paint(PaintingContext context, Offset offset) {
    if (_inkFeatures.isNotEmpty) {
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
      canvas.clipRect(Point.origin & size);
      for (InkFeature inkFeature in _inkFeatures)
        inkFeature._paint(canvas);
      canvas.restore();
    }
    super.paint(context, offset);
  }
}

403 404
class _InkFeatures extends SingleChildRenderObjectWidget {
  _InkFeatures({ Key key, this.color, Widget child }) : super(key: key, child: child);
405 406 407

  final Color color;

408
  @override
409
  _RenderInkFeatures createRenderObject(BuildContext context) => new _RenderInkFeatures(color: color);
410

411
  @override
412
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
413 414 415 416
    renderObject.color = color;
  }
}

417 418 419
/// A visual reaction on a piece of [Material].
///
/// Typically used with [MaterialInkController].
420
abstract class InkFeature {
421 422 423
  /// To add an ink feature to a piece of Material, obtain the
  /// [MaterialInkController] via [Material.of] and call
  /// [MaterialInkController.addInkFeature].
424 425 426 427 428 429
  InkFeature({
    this.renderer,
    this.referenceBox,
    this.onRemoved
  });

430 431 432
  final _RenderInkFeatures renderer;

  /// The render box whose visual position defines the frame of reference for this ink feature.
433
  final RenderBox referenceBox;
434 435

  /// Called when the ink feature is no longer visible on the material.
436 437 438 439
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

440
  /// Free up the resources associated with this ink feature.
441 442 443 444 445 446 447 448 449 450 451 452
  void dispose() {
    assert(!_debugDisposed);
    assert(() { _debugDisposed = true; return true; });
    renderer._removeFeature(this);
    if (onRemoved != null)
      onRemoved();
  }

  void _paint(Canvas canvas) {
    assert(referenceBox.attached);
    assert(!_debugDisposed);
    // find the chain of renderers from us to the feature's referenceBox
453
    List<RenderBox> descendants = <RenderBox>[referenceBox];
454 455 456 457
    RenderBox node = referenceBox;
    while (node != renderer) {
      node = node.parent;
      assert(node != null);
458
      descendants.add(node);
459 460 461
    }
    // determine the transform that gets our coordinate system to be like theirs
    Matrix4 transform = new Matrix4.identity();
462 463 464
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
465 466 467
    paintFeature(canvas, transform);
  }

468 469 470 471
  /// Override this method to paint the ink feature.
  ///
  /// The transform argument gives the coordinate conversion from the coordinate
  /// system of the canvas to the coodinate system of the [referenceBox].
472 473
  void paintFeature(Canvas canvas, Matrix4 transform);

474
  @override
475 476 477 478 479
  String toString() => "$runtimeType@$hashCode";
}

class _InkSplash extends InkFeature implements InkSplash {
  _InkSplash({
480
    _RenderInkFeatures renderer,
481 482
    RenderBox referenceBox,
    this.position,
483
    this.color,
484
    this.targetRadius,
485
    this.clipCallback,
486
    this.repositionToReferenceBox,
487 488
    VoidCallback onRemoved
  }) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
    _radiusController = new AnimationController(duration: _kUnconfirmedSplashDuration)
      ..addListener(renderer.markNeedsPaint)
      ..forward();
    _radius = new Tween<double>(
      begin: _kSplashInitialSize,
      end: targetRadius
    ).animate(_radiusController);

    _alphaController = new AnimationController(duration: _kHighlightFadeDuration)
      ..addListener(renderer.markNeedsPaint)
      ..addStatusListener(_handleAlphaStatusChanged);
    _alpha = new IntTween(
      begin: color.alpha,
      end: 0
    ).animate(_alphaController);
504
  }
505 506

  final Point position;
507
  final Color color;
508
  final double targetRadius;
509
  final RectCallback clipCallback;
510
  final bool repositionToReferenceBox;
511

512
  Animation<double> _radius;
513 514
  AnimationController _radiusController;

515
  Animation<int> _alpha;
516
  AnimationController _alphaController;
517

518
  @override
519
  void confirm() {
520
    int duration = (targetRadius / _kSplashConfirmedVelocity).floor();
521 522 523 524
    _radiusController
      ..duration = new Duration(milliseconds: duration)
      ..forward();
    _alphaController.forward();
525 526
  }

527
  @override
528
  void cancel() {
529
    _alphaController.forward();
530 531
  }

532 533
  void _handleAlphaStatusChanged(AnimationStatus status) {
    if (status == AnimationStatus.completed)
534 535 536
      dispose();
  }

537
  @override
538
  void dispose() {
539 540
    _radiusController.stop();
    _alphaController.stop();
541 542 543
    super.dispose();
  }

544
  @override
545
  void paintFeature(Canvas canvas, Matrix4 transform) {
546 547
    Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
    Point center = position;
548 549
    if (repositionToReferenceBox)
      center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
550 551 552
    Offset originOffset = MatrixUtils.getAsTranslation(transform);
    if (originOffset == null) {
      canvas.save();
553
      canvas.transform(transform.storage);
554 555
      if (clipCallback != null)
        canvas.clipRect(clipCallback());
556
      canvas.drawCircle(center, _radius.value, paint);
557 558
      canvas.restore();
    } else {
559
      if (clipCallback != null) {
560
        canvas.save();
561
        canvas.clipRect(clipCallback().shift(originOffset));
562
      }
563
      canvas.drawCircle(center + originOffset, _radius.value, paint);
564
      if (clipCallback != null)
565 566 567 568 569 570 571
        canvas.restore();
    }
  }
}

class _InkHighlight extends InkFeature implements InkHighlight {
  _InkHighlight({
572
    _RenderInkFeatures renderer,
573
    RenderBox referenceBox,
574
    this.rectCallback,
575
    Color color,
576
    this.shape,
577 578 579
    VoidCallback onRemoved
  }) : _color = color,
       super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
580 581 582 583 584 585 586 587
    _alphaController = new AnimationController(duration: _kHighlightFadeDuration)
      ..addListener(renderer.markNeedsPaint)
      ..addStatusListener(_handleAlphaStatusChanged)
      ..forward();
    _alpha = new IntTween(
      begin: 0,
      end: color.alpha
    ).animate(_alphaController);
588 589
  }

590 591
  final RectCallback rectCallback;

592
  @override
593 594
  Color get color => _color;
  Color _color;
595 596

  @override
597 598 599 600 601 602 603
  void set color(Color value) {
    if (value == _color)
      return;
    _color = value;
    renderer.markNeedsPaint();
  }

604
  final BoxShape shape;
605

606
  @override
607 608
  bool get active => _active;
  bool _active = true;
609

610
  Animation<int> _alpha;
611
  AnimationController _alphaController;
612

613
  @override
614 615
  void activate() {
    _active = true;
616
    _alphaController.forward();
617 618
  }

619
  @override
620 621
  void deactivate() {
    _active = false;
622
    _alphaController.reverse();
623 624
  }

625 626
  void _handleAlphaStatusChanged(AnimationStatus status) {
    if (status == AnimationStatus.dismissed && !_active)
627 628 629
      dispose();
  }

630
  @override
631
  void dispose() {
632
    _alphaController.stop();
633 634 635
    super.dispose();
  }

636
  void _paintHighlight(Canvas canvas, Rect rect, Paint paint) {
637
    if (shape == BoxShape.rectangle)
638 639 640 641 642
      canvas.drawRect(rect, paint);
    else
      canvas.drawCircle(rect.center, _kDefaultSplashRadius, paint);
  }

643
  @override
644 645 646
  void paintFeature(Canvas canvas, Matrix4 transform) {
    Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
    Offset originOffset = MatrixUtils.getAsTranslation(transform);
647
    final Rect rect = (rectCallback != null ? rectCallback() : Point.origin & referenceBox.size);
648 649
    if (originOffset == null) {
      canvas.save();
650
      canvas.transform(transform.storage);
651
      _paintHighlight(canvas, rect, paint);
652 653
      canvas.restore();
    } else {
654
      _paintHighlight(canvas, rect.shift(originOffset), paint);
655 656 657
    }
  }

658
}