material.dart 13.3 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 90 91 92 93 94 95
///
/// 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.
///
96 97 98 99
/// 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.
///
100 101
/// See also:
///
102 103
/// * [MergeableMaterial], a piece of material that can split and remerge.
/// * [Card], a wrapper for a [Material] of [type] [MaterialType.card].
104
/// * <https://material.google.com/>
105
class Material extends StatefulWidget {
106 107
  /// Creates a piece of material.
  ///
108
  /// The [type] and the [elevation] arguments must not be null.
109
  Material({
110
    Key key,
111
    this.type: MaterialType.canvas,
Hans Muller's avatar
Hans Muller committed
112
    this.elevation: 0,
113
    this.color,
114
    this.textStyle,
115
    this.borderRadius,
116
    this.child
117
  }) : super(key: key) {
118
    assert(type != null);
Hans Muller's avatar
Hans Muller committed
119
    assert(elevation != null);
120
    assert(type != MaterialType.circle || borderRadius == null);
121 122
  }

123
  /// The widget below this widget in the tree.
124
  final Widget child;
125

126 127 128
  /// 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.
129
  final MaterialType type;
130

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

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

146
  /// The typographical style to use for text within this material.
147
  final TextStyle textStyle;
148

149 150 151 152 153 154 155
  /// 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;

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

169
  @override
170
  _MaterialState createState() => new _MaterialState();
171

172
  @override
173 174 175 176 177 178
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$type');
    description.add('elevation: $elevation');
    if (color != null)
      description.add('color: $color');
179 180 181 182
    if (textStyle != null)
      description.add('textStyle: $textStyle');
    if (borderRadius != null)
      description.add('borderRadius: $borderRadius');
183
  }
184 185 186

  /// The default radius of an ink splash in logical pixels.
  static const double defaultSplashRadius = 35.0;
187 188
}

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

192
  Color _getBackgroundColor(BuildContext context) {
193 194 195
    if (config.color != null)
      return config.color;
    switch (config.type) {
196
      case MaterialType.canvas:
197
        return Theme.of(context).canvasColor;
198
      case MaterialType.card:
199
        return Theme.of(context).cardColor;
200 201
      default:
        return null;
202 203 204
    }
  }

205
  @override
206
  Widget build(BuildContext context) {
207
    final Color backgroundColor = _getBackgroundColor(context);
208
    assert(backgroundColor != null || config.type == MaterialType.transparency);
209
    Widget contents = config.child;
210
    final BorderRadius radius = config.borderRadius ?? kMaterialEdges[config.type];
211
    if (contents != null) {
212
      contents = new AnimatedDefaultTextStyle(
213
        style: config.textStyle ?? Theme.of(context).textTheme.body1,
214
        duration: kThemeChangeDuration,
215 216 217
        child: contents
      );
    }
218 219
    contents = new NotificationListener<LayoutChangedNotification>(
      onNotification: (LayoutChangedNotification notification) {
220
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
221
        renderer._didChangeLayout();
222
        return true;
223
      },
224
      child: new _InkFeatures(
225 226
        key: _inkFeatureRenderer,
        color: backgroundColor,
227 228
        child: contents,
        vsync: this,
229
      )
230
    );
231
    if (config.type == MaterialType.circle) {
232 233 234 235 236
      contents = new ClipOval(child: contents);
    } else if (kMaterialEdges[config.type] != null) {
      contents = new ClipRRect(
        borderRadius: radius,
        child: contents
237 238
      );
    }
239 240
    if (config.type != MaterialType.transparency) {
      contents = new AnimatedContainer(
241
        curve: Curves.fastOutSlowIn,
242 243
        duration: kThemeChangeDuration,
        decoration: new BoxDecoration(
244
          borderRadius: radius,
245
          shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
246
        ),
247 248
        child: new Container(
          decoration: new BoxDecoration(
249
            borderRadius: radius,
250
            boxShadow: config.elevation == 0 ? null : kElevationToShadow[config.elevation],
251 252 253 254 255
            backgroundColor: backgroundColor,
            shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
          ),
          child: contents
        )
256 257
      );
    }
258 259 260 261
    return contents;
  }
}

262
const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200);
263

264
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
265 266 267 268 269 270 271
  _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.
272
  @override
273
  final TickerProvider vsync;
274 275 276 277

  // This is here to satisfy the MaterialInkController contract.
  // The actual painting of this color is done by a Container in the
  // MaterialState build method.
278
  @override
279 280 281 282
  Color color;

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

283
  @override
284 285
  void addInkFeature(InkFeature feature) {
    assert(!feature._debugDisposed);
286
    assert(feature._controller == this);
287 288 289 290 291 292 293 294 295 296
    assert(!_inkFeatures.contains(feature));
    _inkFeatures.add(feature);
    markNeedsPaint();
  }

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

297 298 299 300 301
  void _didChangeLayout() {
    if (_inkFeatures.isNotEmpty)
      markNeedsPaint();
  }

302
  @override
303 304
  bool hitTestSelf(Point position) => true;

305
  @override
306 307 308 309 310 311 312 313 314 315 316 317 318 319
  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);
  }
}

320
class _InkFeatures extends SingleChildRenderObjectWidget {
321 322 323 324
  _InkFeatures({ Key key, this.color, Widget child, @required this.vsync }) : super(key: key, child: child);

  // 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.
325 326 327

  final Color color;

328 329
  final TickerProvider vsync;

330
  @override
331 332 333 334 335 336
  _RenderInkFeatures createRenderObject(BuildContext context) {
    return new _RenderInkFeatures(
      color: color,
      vsync: vsync
    );
  }
337

338
  @override
339
  void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) {
340
    renderObject.color = color;
341
    assert(vsync == renderObject.vsync);
342 343 344
  }
}

345 346
/// A visual reaction on a piece of [Material].
///
347 348 349
/// To add an ink feature to a piece of [Material], obtain the
/// [MaterialInkController] via [Material.of] and call
/// [MaterialInkController.addInkFeature].
350
abstract class InkFeature {
351
  /// Initializes fields for subclasses.
352
  InkFeature({
353 354
    @required MaterialInkController controller,
    @required this.referenceBox,
355
    this.onRemoved
356 357 358 359
  }) : _controller = controller {
    assert(_controller != null);
    assert(referenceBox != null);
  }
360

361 362 363 364
  /// The [MaterialInkController] associated with this [InkFeature].
  ///
  /// Typically used by subclasses to call
  /// [MaterialInkController.markNeedsPaint] when they need to repaint.
365
  MaterialInkController get controller => _controller;
366
  _RenderInkFeatures _controller;
367 368

  /// The render box whose visual position defines the frame of reference for this ink feature.
369
  final RenderBox referenceBox;
370 371

  /// Called when the ink feature is no longer visible on the material.
372 373 374 375
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

376
  /// Free up the resources associated with this ink feature.
377
  @mustCallSuper
378 379 380
  void dispose() {
    assert(!_debugDisposed);
    assert(() { _debugDisposed = true; return true; });
381
    _controller._removeFeature(this);
382 383 384 385 386 387 388 389
    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
390 391
    final List<RenderObject> descendants = <RenderObject>[referenceBox];
    RenderObject node = referenceBox;
392
    while (node != _controller) {
393 394
      node = node.parent;
      assert(node != null);
395
      descendants.add(node);
396 397
    }
    // determine the transform that gets our coordinate system to be like theirs
398
    final Matrix4 transform = new Matrix4.identity();
399 400 401
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
402 403 404
    paintFeature(canvas, transform);
  }

405 406 407 408
  /// 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].
409
  @protected
410 411
  void paintFeature(Canvas canvas, Matrix4 transform);

412
  @override
413
  String toString() => '$runtimeType#$hashCode';
414
}