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 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 20
enum MaterialType {
  /// Infinite extent using default theme canvas color.
  canvas,

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

22 23 24 25
  /// A circle, no color by default (used for floating action buttons).
  circle,

  /// Rounded edges, no color by default (used for MaterialButton buttons).
26 27 28 29
  button,

  /// A transparent piece of material that draws ink splashes and highlights.
  transparency
30 31 32
}

const Map<MaterialType, double> kMaterialEdges = const <MaterialType, double>{
33 34 35 36
  MaterialType.canvas: null,
  MaterialType.card: 2.0,
  MaterialType.circle: null,
  MaterialType.button: 2.0,
37
  MaterialType.transparency: null,
38 39
};

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
abstract class InkSplash {
  void confirm();
  void cancel();
  void dispose();
}

abstract class InkHighlight {
  void activate();
  void deactivate();
  void dispose();
  bool get active;
  Color get color;
  void set color(Color value);
}

abstract class MaterialInkController {
  /// The color of the material
  Color get color;

  /// Begin a splash, centered at position relative to referenceBox.
  /// If containedInWell is true, then the splash will be sized to fit
  /// the referenceBox, then clipped to it when drawn.
  /// When the splash is removed, onRemoved will be invoked.
63
  InkSplash splashAt({ RenderBox referenceBox, Point position, Color color, bool containedInWell, VoidCallback onRemoved });
64 65

  /// Begin a highlight, coincident with the referenceBox.
66
  InkHighlight highlightAt({ RenderBox referenceBox, Color color, BoxShape shape: BoxShape.rectangle, VoidCallback onRemoved });
67 68 69 70 71 72 73 74 75 76

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

/// Describes a sheet of Material. 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.)
class Material extends StatefulComponent {
77
  Material({
78
    Key key,
79
    this.child,
80
    this.type: MaterialType.canvas,
Hans Muller's avatar
Hans Muller committed
81
    this.elevation: 0,
82 83
    this.color,
    this.textStyle
84
  }) : super(key: key) {
85
    assert(type != null);
Hans Muller's avatar
Hans Muller committed
86
    assert(elevation != null);
87 88
  }

89 90
  final Widget child;
  final MaterialType type;
Hans Muller's avatar
Hans Muller committed
91
  final int elevation;
92
  final Color color;
93
  final TextStyle textStyle;
94

95
  /// The ink controller from the closest instance of this class that encloses the given context.
96
  static MaterialInkController of(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
97
    final RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<RenderInkFeatures>());
98 99 100 101
    return result;
  }

  _MaterialState createState() => new _MaterialState();
102 103 104 105 106 107 108 109

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$type');
    description.add('elevation: $elevation');
    if (color != null)
      description.add('color: $color');
  }
110 111 112 113 114
}

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

115
  Color _getBackgroundColor(BuildContext context) {
116 117 118
    if (config.color != null)
      return config.color;
    switch (config.type) {
119
      case MaterialType.canvas:
120
        return Theme.of(context).canvasColor;
121
      case MaterialType.card:
122
        return Theme.of(context).cardColor;
123 124
      default:
        return null;
125 126 127
    }
  }

128
  Widget build(BuildContext context) {
129 130 131
    Color backgroundColor = _getBackgroundColor(context);
    Widget contents = config.child;
    if (contents != null) {
132
      contents = new DefaultTextStyle(
133
        style: config.textStyle ?? Theme.of(context).text.body1,
134 135 136
        child: contents
      );
    }
137 138 139 140 141 142 143
    contents = new NotificationListener<LayoutChangedNotification>(
      onNotification: (LayoutChangedNotification notification) {
        _inkFeatureRenderer.currentContext.findRenderObject().markNeedsPaint();
      },
      child: new InkFeatures(
        key: _inkFeatureRenderer,
        color: backgroundColor,
144 145
        child: contents
      )
146
    );
147 148 149 150 151 152 153 154 155
    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
      );
    }
156 157 158 159 160 161 162
    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],
163
          shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
164
        ),
165 166 167 168 169 170 171 172
        child: new Container(
          decoration: new BoxDecoration(
            borderRadius: kMaterialEdges[config.type],
            backgroundColor: backgroundColor,
            shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
          ),
          child: contents
        )
173 174
      );
    }
175 176 177 178
    return contents;
  }
}

179 180
const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200);
const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1);
181 182

const double _kDefaultSplashRadius = 35.0; // logical pixels
183
const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond
184 185 186 187 188 189 190 191 192 193 194 195
const double _kSplashInitialSize = 0.0; // logical pixels

class RenderInkFeatures extends RenderProxyBox implements MaterialInkController {
  RenderInkFeatures({ RenderBox child, this.color }) : super(child);

  // This is here to satisfy the MaterialInkController contract.
  // The actual painting of this color is done by a Container in the
  // MaterialState build method.
  Color color;

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

196 197 198 199 200 201 202
  InkSplash splashAt({
    RenderBox referenceBox,
    Point position,
    Color color,
    bool containedInWell,
    VoidCallback onRemoved
  }) {
203 204 205 206 207 208 209 210 211 212
    double radius;
    if (containedInWell) {
      radius = _getSplashTargetSize(referenceBox.size, position);
    } else {
      radius = _kDefaultSplashRadius;
    }
    _InkSplash splash = new _InkSplash(
      renderer: this,
      referenceBox: referenceBox,
      position: position,
213
      color: color,
214 215
      targetRadius: radius,
      clipToReferenceBox: containedInWell,
216
      repositionToReferenceBox: !containedInWell,
217 218 219 220 221 222 223 224 225 226 227 228 229 230
      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();
  }

231 232 233
  InkHighlight highlightAt({
    RenderBox referenceBox,
    Color color,
234
    BoxShape shape: BoxShape.rectangle,
235 236
    VoidCallback onRemoved
  }) {
237 238 239 240
    _InkHighlight highlight = new _InkHighlight(
      renderer: this,
      referenceBox: referenceBox,
      color: color,
241
      shape: shape,
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 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 311 312 313
      onRemoved: onRemoved
    );
    addInkFeature(highlight);
    return highlight;
  }

  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();
  }

  bool hitTestSelf(Point position) => true;

  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);
  }
}

class InkFeatures extends OneChildRenderObjectWidget {
  InkFeatures({ Key key, this.color, Widget child }) : super(key: key, child: child);

  final Color color;

  RenderInkFeatures createRenderObject() => new RenderInkFeatures(color: color);

  void updateRenderObject(RenderInkFeatures renderObject, InkFeatures oldWidget) {
    renderObject.color = color;
  }
}

abstract class InkFeature {
  InkFeature({
    this.renderer,
    this.referenceBox,
    this.onRemoved
  });

  final RenderInkFeatures renderer;
  final RenderBox referenceBox;
  final VoidCallback onRemoved;

  bool _debugDisposed = false;

  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
314
    List<RenderBox> descendants = <RenderBox>[referenceBox];
315 316 317 318
    RenderBox node = referenceBox;
    while (node != renderer) {
      node = node.parent;
      assert(node != null);
319
      descendants.add(node);
320 321 322
    }
    // determine the transform that gets our coordinate system to be like theirs
    Matrix4 transform = new Matrix4.identity();
323 324 325
    assert(descendants.length >= 2);
    for (int index = descendants.length - 1; index > 0; index -= 1)
      descendants[index].applyPaintTransform(descendants[index - 1], transform);
326 327 328 329 330 331 332 333 334 335 336 337 338
    paintFeature(canvas, transform);
  }

  void paintFeature(Canvas canvas, Matrix4 transform);

  String toString() => "$runtimeType@$hashCode";
}

class _InkSplash extends InkFeature implements InkSplash {
  _InkSplash({
    RenderInkFeatures renderer,
    RenderBox referenceBox,
    this.position,
339
    this.color,
340 341
    this.targetRadius,
    this.clipToReferenceBox,
342
    this.repositionToReferenceBox,
343 344
    VoidCallback onRemoved
  }) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
    _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);
360
  }
361 362

  final Point position;
363
  final Color color;
364 365
  final double targetRadius;
  final bool clipToReferenceBox;
366
  final bool repositionToReferenceBox;
367

368
  Animation<double> _radius;
369 370
  AnimationController _radiusController;

371
  Animation<int> _alpha;
372
  AnimationController _alphaController;
373 374

  void confirm() {
375
    int duration = (targetRadius / _kSplashConfirmedVelocity).floor();
376 377 378 379
    _radiusController
      ..duration = new Duration(milliseconds: duration)
      ..forward();
    _alphaController.forward();
380 381 382
  }

  void cancel() {
383
    _alphaController.forward();
384 385
  }

386 387
  void _handleAlphaStatusChanged(AnimationStatus status) {
    if (status == AnimationStatus.completed)
388 389 390 391
      dispose();
  }

  void dispose() {
392 393
    _radiusController.stop();
    _alphaController.stop();
394 395 396 397
    super.dispose();
  }

  void paintFeature(Canvas canvas, Matrix4 transform) {
398 399
    Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
    Point center = position;
400 401 402
    Offset originOffset = MatrixUtils.getAsTranslation(transform);
    if (originOffset == null) {
      canvas.save();
403
      canvas.transform(transform.storage);
404 405
      if (clipToReferenceBox)
        canvas.clipRect(Point.origin & referenceBox.size);
406
      if (repositionToReferenceBox)
407
        center = Point.lerp(center, Point.origin, _radiusController.value);
408
      canvas.drawCircle(center, _radius.value, paint);
409 410 411 412 413 414
      canvas.restore();
    } else {
      if (clipToReferenceBox) {
        canvas.save();
        canvas.clipRect(originOffset.toPoint() & referenceBox.size);
      }
415
      if (repositionToReferenceBox)
416
        center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value);
417
      canvas.drawCircle(center + originOffset, _radius.value, paint);
418 419 420 421 422 423 424 425 426 427 428
      if (clipToReferenceBox)
        canvas.restore();
    }
  }
}

class _InkHighlight extends InkFeature implements InkHighlight {
  _InkHighlight({
    RenderInkFeatures renderer,
    RenderBox referenceBox,
    Color color,
429
    this.shape,
430 431 432
    VoidCallback onRemoved
  }) : _color = color,
       super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) {
433 434 435 436 437 438 439 440
    _alphaController = new AnimationController(duration: _kHighlightFadeDuration)
      ..addListener(renderer.markNeedsPaint)
      ..addStatusListener(_handleAlphaStatusChanged)
      ..forward();
    _alpha = new IntTween(
      begin: 0,
      end: color.alpha
    ).animate(_alphaController);
441 442 443 444 445 446 447 448 449 450 451
  }

  Color get color => _color;
  Color _color;
  void set color(Color value) {
    if (value == _color)
      return;
    _color = value;
    renderer.markNeedsPaint();
  }

452
  final BoxShape shape;
453

454 455
  bool get active => _active;
  bool _active = true;
456

457
  Animation<int> _alpha;
458
  AnimationController _alphaController;
459 460 461

  void activate() {
    _active = true;
462
    _alphaController.forward();
463 464 465 466
  }

  void deactivate() {
    _active = false;
467
    _alphaController.reverse();
468 469
  }

470 471
  void _handleAlphaStatusChanged(AnimationStatus status) {
    if (status == AnimationStatus.dismissed && !_active)
472 473 474 475
      dispose();
  }

  void dispose() {
476
    _alphaController.stop();
477 478 479
    super.dispose();
  }

480
  void _paintHighlight(Canvas canvas, Rect rect, paint) {
481
    if (shape == BoxShape.rectangle)
482 483 484 485 486
      canvas.drawRect(rect, paint);
    else
      canvas.drawCircle(rect.center, _kDefaultSplashRadius, paint);
  }

487 488 489 490 491
  void paintFeature(Canvas canvas, Matrix4 transform) {
    Paint paint = new Paint()..color = color.withAlpha(_alpha.value);
    Offset originOffset = MatrixUtils.getAsTranslation(transform);
    if (originOffset == null) {
      canvas.save();
492
      canvas.transform(transform.storage);
493
      _paintHighlight(canvas, Point.origin & referenceBox.size, paint);
494 495
      canvas.restore();
    } else {
496
      _paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint);
497 498 499
    }
  }

500
}