material.dart 14.6 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
import 'package:flutter/foundation.dart';
6
import 'package:flutter/rendering.dart';
7
import 'package:flutter/widgets.dart';
8

9
import 'constants.dart';
10
import 'shadows.dart';
11
import 'theme.dart';
12

13 14
/// Signature for the callback used by ink effects to obtain the rectangle for the effect.
///
15
/// Used by [InkHighlight] and [InkSplash], for example.
16 17 18 19
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
final Map<MaterialType, BorderRadius> kMaterialEdges = <MaterialType, BorderRadius> {
49
  MaterialType.canvas: null,
50
  MaterialType.card: new BorderRadius.circular(2.0),
51
  MaterialType.circle: null,
52
  MaterialType.button: new BorderRadius.circular(2.0),
53
  MaterialType.transparency: null,
54 55
};

56 57 58
/// An interface for creating [InkSplash]s and [InkHighlight]s on a material.
///
/// Typically obtained via [Material.of].
59
abstract class MaterialInkController {
60
  /// The color of the material.
61 62
  Color get color;

63
  /// The ticker provider used by the controller.
64
  ///
65 66 67 68 69
  /// Ink features that are added to this controller with [addInkFeature] should
  /// use this vsync to drive their animations.
  TickerProvider get vsync;

  /// Add an [InkFeature], such as an [InkSplash] or an [InkHighlight].
70
  ///
71
  /// The ink feature will paint as part of this controller.
72
  void addInkFeature(InkFeature feature);
73 74 75

  /// Notifies the controller that one of its ink features needs to repaint.
  void markNeedsPaint();
76 77
}

78 79 80 81 82
/// 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
83
/// shadows.
84 85 86 87 88 89
///
/// 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].
///
90 91 92 93
/// If a material has a non-zero [elevation], then the material will clip its
/// contents because content that is conceptually printing on a separate piece
/// of material cannot be printed beyond the bounds of the material.
///
94 95 96 97 98 99
/// 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.
///
100 101 102 103
/// In general, the features of a [Material] should not change over time (e.g. a
/// [Material] should not change its [color] or [type]). The one exception is
/// the [elevation], changes to which will be animated.
///
104 105
/// See also:
///
106 107
/// * [MergeableMaterial], a piece of material that can split and remerge.
/// * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
108
/// * <https://material.google.com/>
109
class Material extends StatefulWidget {
110 111
  /// Creates a piece of material.
  ///
112
  /// The [type] and the [elevation] arguments must not be null.
113
  const Material({
114
    Key key,
115
    this.type: MaterialType.canvas,
Hans Muller's avatar
Hans Muller committed
116
    this.elevation: 0,
117
    this.color,
118
    this.textStyle,
119
    this.borderRadius,
120
    this.child,
121 122 123 124
  }) : assert(type != null),
       assert(elevation != null),
       assert(!(identical(type, MaterialType.circle) && borderRadius != null)),
       super(key: key);
125

126
  /// The widget below this widget in the tree.
127
  final Widget child;
128

129 130 131
  /// 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.
132
  final MaterialType type;
133

134
  /// The z-coordinate at which to place this material.
135 136
  ///
  /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
137 138
  ///
  /// Defaults to 0.
Hans Muller's avatar
Hans Muller committed
139
  final int elevation;
140

141
  /// The color to paint the material.
142 143 144
  ///
  /// Must be opaque. To create a transparent piece of material, use
  /// [MaterialType.transparency].
145 146
  ///
  /// By default, the color is derived from the [type] of material.
147
  final Color color;
148

149
  /// The typographical style to use for text within this material.
150
  final TextStyle textStyle;
151

152 153 154 155 156 157 158
  /// If non-null, the corners of this box are rounded by this [BorderRadius].
  /// Otherwise, the corners specified for the current [type] of material are
  /// used.
  ///
  /// Must be null if [type] is [MaterialType.circle].
  final BorderRadius borderRadius;

159 160
  /// The ink controller from the closest instance of this class that
  /// encloses the given context.
161 162 163 164 165 166
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
  /// MaterialInkController inkController = Material.of(context);
  /// ```
167
  static MaterialInkController of(BuildContext context) {
168
    final _RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderInkFeatures>());
169 170 171
    return result;
  }

172
  @override
173
  _MaterialState createState() => new _MaterialState();
174

175
  @override
176 177 178 179 180 181
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$type');
    description.add('elevation: $elevation');
    if (color != null)
      description.add('color: $color');
182 183 184 185
    if (textStyle != null) {
      for (String entry in '$textStyle'.split('\n'))
        description.add('textStyle.$entry');
    }
186 187
    if (borderRadius != null)
      description.add('borderRadius: $borderRadius');
188
  }
189 190 191

  /// The default radius of an ink splash in logical pixels.
  static const double defaultSplashRadius = 35.0;
192 193 194

  // Temporary flag used to enable the PhysicalModel shadow implementation.
  static bool debugEnablePhysicalModel = false;
195 196
}

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

200
  Color _getBackgroundColor(BuildContext context) {
201 202 203
    if (widget.color != null)
      return widget.color;
    switch (widget.type) {
204
      case MaterialType.canvas:
205
        return Theme.of(context).canvasColor;
206
      case MaterialType.card:
207
        return Theme.of(context).cardColor;
208 209
      default:
        return null;
210 211 212
    }
  }

213
  @override
214
  Widget build(BuildContext context) {
215
    final Color backgroundColor = _getBackgroundColor(context);
216 217 218
    assert(backgroundColor != null || widget.type == MaterialType.transparency);
    Widget contents = widget.child;
    final BorderRadius radius = widget.borderRadius ?? kMaterialEdges[widget.type];
219
    if (contents != null) {
220
      contents = new AnimatedDefaultTextStyle(
221
        style: widget.textStyle ?? Theme.of(context).textTheme.body1,
222
        duration: kThemeChangeDuration,
223 224 225
        child: contents
      );
    }
226 227
    contents = new NotificationListener<LayoutChangedNotification>(
      onNotification: (LayoutChangedNotification notification) {
228
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
229
        renderer._didChangeLayout();
230
        return true;
231
      },
232
      child: new _InkFeatures(
233 234
        key: _inkFeatureRenderer,
        color: backgroundColor,
235 236
        child: contents,
        vsync: this,
237
      )
238
    );
239

240
    if (Material.debugEnablePhysicalModel) {
241
      if (widget.type == MaterialType.circle) {
242 243
        contents = new PhysicalModel(
          shape: BoxShape.circle,
244
          elevation: widget.elevation,
245 246 247
          color: backgroundColor,
          child: contents,
        );
248
      } else if (widget.type == MaterialType.transparency) {
249 250 251 252 253 254 255 256
        if (radius == null) {
          contents = new ClipRect(child: contents);
        } else {
          contents = new ClipRRect(
            borderRadius: radius,
            child: contents
          );
        }
257
      } else {
258 259 260
        contents = new PhysicalModel(
          shape: BoxShape.rectangle,
          borderRadius: radius ?? BorderRadius.zero,
261
          elevation: widget.elevation,
262 263 264 265 266
          color: backgroundColor,
          child: contents,
        );
      }
    } else {
267
      if (widget.type == MaterialType.circle) {
268
        contents = new ClipOval(child: contents);
269
      } else if (kMaterialEdges[widget.type] != null) {
270 271 272 273 274
        contents = new ClipRRect(
          borderRadius: radius,
          child: contents
        );
      }
275
    }
276

277
    if (widget.type != MaterialType.transparency) {
278
      contents = new AnimatedContainer(
279
        curve: Curves.fastOutSlowIn,
280 281
        duration: kThemeChangeDuration,
        decoration: new BoxDecoration(
282
          borderRadius: radius,
283 284 285
          boxShadow: widget.elevation == 0 || Material.debugEnablePhysicalModel ?
              null : kElevationToShadow[widget.elevation],
          shape: widget.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
286
        ),
287 288
        child: new Container(
          decoration: new BoxDecoration(
289
            borderRadius: radius,
290
            backgroundColor: backgroundColor,
291
            shape: widget.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
292 293 294
          ),
          child: contents
        )
295 296
      );
    }
297 298 299 300
    return contents;
  }
}

301
const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200);
302

303
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
304 305 306 307 308 309 310
  _RenderInkFeatures({ RenderBox child, @required this.vsync, this.color }) : super(child) {
    assert(vsync != null);
  }

  // This class should exist in a 1:1 relationship with a MaterialState object,
  // since there's no current support for dynamically changing the ticker
  // provider.
311
  @override
312
  final TickerProvider vsync;
313 314 315 316

  // This is here to satisfy the MaterialInkController contract.
  // The actual painting of this color is done by a Container in the
  // MaterialState build method.
317
  @override
318 319 320 321
  Color color;

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

322
  @override
323 324
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
325
    assert(feature._controller == this);
326 327 328 329 330 331 332 333 334 335
    assert(!_inkFeatures.contains(feature));
    _inkFeatures.add(feature);
    markNeedsPaint();
  }

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

336 337 338 339 340
  void _didChangeLayout() {
    if (_inkFeatures.isNotEmpty)
      markNeedsPaint();
  }

341
  @override
342
  bool hitTestSelf(Offset position) => true;
343

344
  @override
345 346 347 348 349
  void paint(PaintingContext context, Offset offset) {
    if (_inkFeatures.isNotEmpty) {
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
350
      canvas.clipRect(Offset.zero & size);
351 352 353 354 355 356 357 358
      for (InkFeature inkFeature in _inkFeatures)
        inkFeature._paint(canvas);
      canvas.restore();
    }
    super.paint(context, offset);
  }
}

359
class _InkFeatures extends SingleChildRenderObjectWidget {
360
  const _InkFeatures({ Key key, this.color, Widget child, @required this.vsync }) : super(key: key, child: child);
361 362 363

  // This widget must be owned by a MaterialState, which must be provided as the vsync.
  // This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
364 365 366

  final Color color;

367 368
  final TickerProvider vsync;

369
  @override
370 371 372 373 374 375
  _RenderInkFeatures createRenderObject(BuildContext context) {
    return new _RenderInkFeatures(
      color: color,
      vsync: vsync
    );
  }
376

377
  @override
378
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
379
    renderObject.color = color;
380
    assert(vsync == renderObject.vsync);
381 382 383
  }
}

384 385
/// A visual reaction on a piece of [Material].
///
386 387 388
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
389
abstract class InkFeature {
390
  /// Initializes fields for subclasses.
391
  InkFeature({
392 393
    @required MaterialInkController controller,
    @required this.referenceBox,
394
    this.onRemoved
395 396 397 398
  }) : _controller = controller {
    assert(_controller != null);
    assert(referenceBox != null);
  }
399

400 401 402 403
  /// The [MaterialInkController] associated with this [InkFeature].
  ///
  /// Typically used by subclasses to call
  /// [MaterialInkController.markNeedsPaint] when they need to repaint.
404
  MaterialInkController get controller => _controller;
405
  _RenderInkFeatures _controller;
406 407

  /// The render box whose visual position defines the frame of reference for this ink feature.
408
  final RenderBox referenceBox;
409 410

  /// Called when the ink feature is no longer visible on the material.
411 412 413 414
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

415
  /// Free up the resources associated with this ink feature.
416
  @mustCallSuper
417 418 419
  void dispose() {
    assert(!_debugDisposed);
    assert(() { _debugDisposed = true; return true; });
420
    _controller._removeFeature(this);
421 422 423 424 425 426 427 428
    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
429 430
    final List<RenderObject> descendants = <RenderObject>[referenceBox];
    RenderObject node = referenceBox;
431
    while (node != _controller) {
432 433
      node = node.parent;
      assert(node != null);
434
      descendants.add(node);
435 436
    }
    // determine the transform that gets our coordinate system to be like theirs
437
    final Matrix4 transform = new Matrix4.identity();
438 439 440
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
441 442 443
    paintFeature(canvas, transform);
  }

444 445 446 447
  /// 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].
448
  @protected
449 450
  void paintFeature(Canvas canvas, Matrix4 transform);

451
  @override
452
  String toString() => '$runtimeType#$hashCode';
453
}