// 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. import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; import 'constants.dart'; import 'shadows.dart'; import 'theme.dart'; enum MaterialType { /// Infinite extent using default theme canvas color. canvas, /// Rounded edges, card theme color. card, /// A circle, no color by default (used for floating action buttons). circle, /// Rounded edges, no color by default (used for MaterialButton buttons). button, /// A transparent piece of material that draws ink splashes and highlights. transparency } const Map<MaterialType, double> kMaterialEdges = const <MaterialType, double>{ MaterialType.canvas: null, MaterialType.card: 2.0, MaterialType.circle: null, MaterialType.button: 2.0, MaterialType.transparency: null, }; 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. InkSplash splashAt({ RenderBox referenceBox, Point position, Color color, bool containedInWell, VoidCallback onRemoved }); /// Begin a highlight, coincident with the referenceBox. InkHighlight highlightAt({ RenderBox referenceBox, Color color, BoxShape shape: BoxShape.rectangle, VoidCallback onRemoved }); /// 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 { Material({ Key key, this.child, this.type: MaterialType.canvas, this.elevation: 0, this.color, this.textStyle }) : super(key: key) { assert(type != null); assert(elevation != null); } final Widget child; final MaterialType type; final int elevation; final Color color; final TextStyle textStyle; /// The ink controller from the closest instance of this class that encloses the given context. static MaterialInkController of(BuildContext context) { final RenderInkFeatures result = context.ancestorRenderObjectOfType(const TypeMatcher<RenderInkFeatures>()); return result; } _MaterialState createState() => new _MaterialState(); void debugFillDescription(List<String> description) { super.debugFillDescription(description); description.add('$type'); description.add('elevation: $elevation'); if (color != null) description.add('color: $color'); } } class _MaterialState extends State<Material> { final GlobalKey _inkFeatureRenderer = new GlobalKey(debugLabel: 'ink renderer'); Color _getBackgroundColor(BuildContext context) { if (config.color != null) return config.color; switch (config.type) { case MaterialType.canvas: return Theme.of(context).canvasColor; case MaterialType.card: return Theme.of(context).cardColor; default: return null; } } Widget build(BuildContext context) { Color backgroundColor = _getBackgroundColor(context); Widget contents = config.child; if (contents != null) { contents = new DefaultTextStyle( style: config.textStyle ?? Theme.of(context).text.body1, child: contents ); } contents = new NotificationListener<LayoutChangedNotification>( onNotification: (LayoutChangedNotification notification) { _inkFeatureRenderer.currentContext.findRenderObject().markNeedsPaint(); }, child: new InkFeatures( key: _inkFeatureRenderer, color: backgroundColor, child: contents ) ); 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 ); } 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], shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle ), child: new Container( decoration: new BoxDecoration( borderRadius: kMaterialEdges[config.type], backgroundColor: backgroundColor, shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle ), child: contents ) ); } return contents; } } const Duration _kHighlightFadeDuration = const Duration(milliseconds: 200); const Duration _kUnconfirmedSplashDuration = const Duration(seconds: 1); const double _kDefaultSplashRadius = 35.0; // logical pixels const double _kSplashConfirmedVelocity = 1.0; // logical pixels per millisecond 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>[]; InkSplash splashAt({ RenderBox referenceBox, Point position, Color color, bool containedInWell, VoidCallback onRemoved }) { double radius; if (containedInWell) { radius = _getSplashTargetSize(referenceBox.size, position); } else { radius = _kDefaultSplashRadius; } _InkSplash splash = new _InkSplash( renderer: this, referenceBox: referenceBox, position: position, color: color, targetRadius: radius, clipToReferenceBox: containedInWell, repositionToReferenceBox: !containedInWell, 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(); } InkHighlight highlightAt({ RenderBox referenceBox, Color color, BoxShape shape: BoxShape.rectangle, VoidCallback onRemoved }) { _InkHighlight highlight = new _InkHighlight( renderer: this, referenceBox: referenceBox, color: color, shape: shape, 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 List<RenderBox> descendants = <RenderBox>[referenceBox]; RenderBox node = referenceBox; while (node != renderer) { node = node.parent; assert(node != null); descendants.add(node); } // determine the transform that gets our coordinate system to be like theirs Matrix4 transform = new Matrix4.identity(); assert(descendants.length >= 2); for (int index = descendants.length - 1; index > 0; index -= 1) descendants[index].applyPaintTransform(descendants[index - 1], transform); 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, this.color, this.targetRadius, this.clipToReferenceBox, this.repositionToReferenceBox, VoidCallback onRemoved }) : super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { _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); } final Point position; final Color color; final double targetRadius; final bool clipToReferenceBox; final bool repositionToReferenceBox; Animation<double> _radius; AnimationController _radiusController; Animation<int> _alpha; AnimationController _alphaController; void confirm() { int duration = (targetRadius / _kSplashConfirmedVelocity).floor(); _radiusController ..duration = new Duration(milliseconds: duration) ..forward(); _alphaController.forward(); } void cancel() { _alphaController.forward(); } void _handleAlphaStatusChanged(AnimationStatus status) { if (status == AnimationStatus.completed) dispose(); } void dispose() { _radiusController.stop(); _alphaController.stop(); super.dispose(); } void paintFeature(Canvas canvas, Matrix4 transform) { Paint paint = new Paint()..color = color.withAlpha(_alpha.value); Point center = position; Offset originOffset = MatrixUtils.getAsTranslation(transform); if (originOffset == null) { canvas.save(); canvas.transform(transform.storage); if (clipToReferenceBox) canvas.clipRect(Point.origin & referenceBox.size); if (repositionToReferenceBox) center = Point.lerp(center, Point.origin, _radiusController.value); canvas.drawCircle(center, _radius.value, paint); canvas.restore(); } else { if (clipToReferenceBox) { canvas.save(); canvas.clipRect(originOffset.toPoint() & referenceBox.size); } if (repositionToReferenceBox) center = Point.lerp(center, referenceBox.size.center(Point.origin), _radiusController.value); canvas.drawCircle(center + originOffset, _radius.value, paint); if (clipToReferenceBox) canvas.restore(); } } } class _InkHighlight extends InkFeature implements InkHighlight { _InkHighlight({ RenderInkFeatures renderer, RenderBox referenceBox, Color color, this.shape, VoidCallback onRemoved }) : _color = color, super(renderer: renderer, referenceBox: referenceBox, onRemoved: onRemoved) { _alphaController = new AnimationController(duration: _kHighlightFadeDuration) ..addListener(renderer.markNeedsPaint) ..addStatusListener(_handleAlphaStatusChanged) ..forward(); _alpha = new IntTween( begin: 0, end: color.alpha ).animate(_alphaController); } Color get color => _color; Color _color; void set color(Color value) { if (value == _color) return; _color = value; renderer.markNeedsPaint(); } final BoxShape shape; bool get active => _active; bool _active = true; Animation<int> _alpha; AnimationController _alphaController; void activate() { _active = true; _alphaController.forward(); } void deactivate() { _active = false; _alphaController.reverse(); } void _handleAlphaStatusChanged(AnimationStatus status) { if (status == AnimationStatus.dismissed && !_active) dispose(); } void dispose() { _alphaController.stop(); super.dispose(); } void _paintHighlight(Canvas canvas, Rect rect, paint) { if (shape == BoxShape.rectangle) canvas.drawRect(rect, paint); else canvas.drawCircle(rect.center, _kDefaultSplashRadius, paint); } 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(); canvas.transform(transform.storage); _paintHighlight(canvas, Point.origin & referenceBox.size, paint); canvas.restore(); } else { _paintHighlight(canvas, originOffset.toPoint() & referenceBox.size, paint); } } }