ink_decoration.dart 12.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
// Copyright 2017 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 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'debug.dart';
import 'material.dart';

/// A convenience widget for drawing images and other decorations on [Material]
/// widgets, so that [InkWell] and [InkResponse] splashes will render over them.
///
/// Ink splashes and highlights, as rendered by [InkWell] and [InkResponse],
/// draw on the actual underlying [Material], under whatever widgets are drawn
/// over the material (such as [Text] and [Icon]s). If an opaque image is drawn
/// over the [Material] (maybe using a [Container] or [DecoratedBox]), these ink
/// effects will not be visible, as they will be entirely obscured by the opaque
/// graphics drawn above the [Material].
///
/// This widget draws the given [Decoration] directly on the [Material], in the
/// same way that [InkWell] and [InkResponse] draw there. This allows the
/// splashes to be drawn above the otherwise opaque graphics.
///
/// An alternative solution is to use a [MaterialType.transparency] material
/// above the opaque graphics, so that the ink responses from [InkWell]s and
/// [InkResponse]s will be drawn on the transparent material on top of the
/// opaque graphics, rather than under the opaque graphics on the underlying
/// [Material].
///
/// ## Limitations
///
/// This widget is subject to the same limitations as other ink effects, as
/// described in the documentation for [Material]. Most notably, the position of
/// an [Ink] widget must not change during the lifetime of the [Material] object
/// unless a [LayoutChangedNotification] is dispatched each frame that the
/// position changes. This is done automatically for [ListView] and other
/// scrolling widgets, but is not done for animated transitions such as
/// [SlideTransition].
///
/// Additionally, if multiple [Ink] widgets paint on the same [Material] in the
/// same location, their relative order is not guaranteed. The decorations will
/// be painted in the order that they were added to the material, which
/// generally speaking will match the order they are given in the widget tree,
/// but this order may appear to be somewhat random in more dynamic situations.
///
47
/// {@tool sample}
Ian Hickson's avatar
Ian Hickson committed
48 49 50 51 52
///
/// This example shows how a [Material] widget can have a yellow rectangle drawn
/// on it using [Ink], while still having ink effects over the yellow rectangle:
///
/// ```dart
53
/// Material(
Ian Hickson's avatar
Ian Hickson committed
54
///   color: Colors.teal[900],
55 56
///   child: Center(
///     child: Ink(
Ian Hickson's avatar
Ian Hickson committed
57 58 59
///       color: Colors.yellow,
///       width: 200.0,
///       height: 100.0,
60
///       child: InkWell(
Ian Hickson's avatar
Ian Hickson committed
61
///         onTap: () { /* ... */ },
62 63
///         child: Center(
///           child: Text('YELLOW'),
Ian Hickson's avatar
Ian Hickson committed
64 65 66 67 68 69
///         )
///       ),
///     ),
///   ),
/// )
/// ```
70 71
/// {@end-tool}
/// {@tool sample}
Ian Hickson's avatar
Ian Hickson committed
72 73 74 75 76
///
/// The following example shows how an image can be printed on a [Material]
/// widget with an [InkWell] above it:
///
/// ```dart
77
/// Material(
Ian Hickson's avatar
Ian Hickson committed
78
///   color: Colors.grey[800],
79 80 81
///   child: Center(
///     child: Ink.image(
///       image: AssetImage('cat.jpeg'),
Ian Hickson's avatar
Ian Hickson committed
82 83 84
///       fit: BoxFit.cover,
///       width: 300.0,
///       height: 200.0,
85
///       child: InkWell(
Ian Hickson's avatar
Ian Hickson committed
86
///         onTap: () { /* ... */ },
87
///         child: Align(
Ian Hickson's avatar
Ian Hickson committed
88
///           alignment: Alignment.topLeft,
89
///           child: Padding(
Ian Hickson's avatar
Ian Hickson committed
90
///             padding: const EdgeInsets.all(10.0),
91
///             child: Text('KITTEN', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.white)),
Ian Hickson's avatar
Ian Hickson committed
92 93 94 95 96 97 98
///           ),
///         )
///       ),
///     ),
///   ),
/// )
/// ```
99
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
100 101 102 103
///
/// See also:
///
///  * [Container], a more generic form of this widget which paints itself,
Josh Soref's avatar
Josh Soref committed
104
///    rather that deferring to the nearest [Material] widget.
Ian Hickson's avatar
Ian Hickson committed
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
///  * [InkDecoration], the [InkFeature] subclass used by this widget to paint
///    on [Material] widgets.
///  * [InkWell] and [InkResponse], which also draw on [Material] widgets.
class Ink extends StatefulWidget {
  /// Paints a decoration (which can be a simple color) on a [Material].
  ///
  /// The [height] and [width] values include the [padding].
  ///
  /// The `color` argument is a shorthand for `decoration: new
  /// BoxDecoration(color: color)`, which means you cannot supply both a `color`
  /// and a `decoration` argument. If you want to have both a `color` and a
  /// `decoration`, you can pass the color as the `color` argument to the
  /// `BoxDecoration`.
  Ink({
    Key key,
    this.padding,
    Color color,
    Decoration decoration,
    this.width,
    this.height,
    this.child,
  }) : assert(padding == null || padding.isNonNegative),
       assert(decoration == null || decoration.debugAssertIsValid()),
       assert(color == null || decoration == null,
         'Cannot provide both a color and a decoration\n'
         'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".'
       ),
132
       decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
Ian Hickson's avatar
Ian Hickson committed
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
       super(key: key);

  /// Creates a widget that shows an image (obtained from an [ImageProvider]) on
  /// a [Material].
  ///
  /// This argument is a shorthand for passing a [BoxDecoration] that has only
  /// its [BoxDecoration.image] property set to the [new Ink] constructor. The
  /// properties of the [DecorationImage] of that [BoxDecoration] are set
  /// according to the arguments passed to this method.
  ///
  /// The `image` argument must not be null. The `alignment`, `repeat`, and
  /// `matchTextDirection` arguments must not be null either, but they have
  /// default values.
  ///
  /// See [paintImage] for a description of the meaning of these arguments.
  Ink.image({
    Key key,
    this.padding,
    @required ImageProvider image,
    ColorFilter colorFilter,
    BoxFit fit,
154
    AlignmentGeometry alignment = Alignment.center,
Ian Hickson's avatar
Ian Hickson committed
155
    Rect centerSlice,
156 157
    ImageRepeat repeat = ImageRepeat.noRepeat,
    bool matchTextDirection = false,
Ian Hickson's avatar
Ian Hickson committed
158 159 160 161 162 163 164 165
    this.width,
    this.height,
    this.child,
  }) : assert(padding == null || padding.isNonNegative),
       assert(image != null),
       assert(alignment != null),
       assert(repeat != null),
       assert(matchTextDirection != null),
166 167
       decoration = BoxDecoration(
         image: DecorationImage(
Ian Hickson's avatar
Ian Hickson committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
           image: image,
           colorFilter: colorFilter,
           fit: fit,
           alignment: alignment,
           centerSlice: centerSlice,
           repeat: repeat,
           matchTextDirection: matchTextDirection,
         ),
       ),
       super(key: key);

  /// The [child] contained by the container.
  ///
  /// {@macro flutter.widgets.child}
  final Widget child;

  /// Empty space to inscribe inside the [decoration]. The [child], if any, is
  /// placed inside this padding.
  ///
  /// This padding is in addition to any padding inherent in the [decoration];
  /// see [Decoration.padding].
  final EdgeInsetsGeometry padding;

  /// The decoration to paint on the nearest ancestor [Material] widget.
  ///
  /// A shorthand for specifying just a solid color is available in the
  /// constructor: set the `color` argument instead of the `decoration`
  /// argument.
  ///
  /// A shorthand for specifying just an image is also available using the [new
  /// Ink.image] constructor.
  final Decoration decoration;

  /// A width to apply to the [decoration] and the [child]. The width includes
  /// any [padding].
  final double width;

  /// A height to apply to the [decoration] and the [child]. The height includes
  /// any [padding].
  final double height;

  EdgeInsetsGeometry get _paddingIncludingDecoration {
    if (decoration == null || decoration.padding == null)
      return padding;
    final EdgeInsetsGeometry decorationPadding = decoration.padding;
    if (padding == null)
      return decorationPadding;
    return padding.add(decorationPadding);
  }

  @override
219 220
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
221 222
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
    properties.add(DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null));
Ian Hickson's avatar
Ian Hickson committed
223 224 225
  }

  @override
226
  _InkState createState() => _InkState();
Ian Hickson's avatar
Ian Hickson committed
227 228 229 230 231 232 233 234 235 236 237
}

class _InkState extends State<Ink> {
  InkDecoration _ink;

  void _handleRemoved() {
    _ink = null;
  }

  @override
  void deactivate() {
238
    _ink?.dispose();
Ian Hickson's avatar
Ian Hickson committed
239 240 241 242 243 244
    assert(_ink == null);
    super.deactivate();
  }

  Widget _build(BuildContext context, BoxConstraints constraints) {
    if (_ink == null) {
245
      _ink = InkDecoration(
Ian Hickson's avatar
Ian Hickson committed
246 247 248 249 250 251 252 253 254 255 256 257 258
        decoration: widget.decoration,
        configuration: createLocalImageConfiguration(context),
        controller: Material.of(context),
        referenceBox: context.findRenderObject(),
        onRemoved: _handleRemoved,
      );
    } else {
      _ink.decoration = widget.decoration;
      _ink.configuration = createLocalImageConfiguration(context);
    }
    Widget current = widget.child;
    final EdgeInsetsGeometry effectivePadding = widget._paddingIncludingDecoration;
    if (effectivePadding != null)
259
      current = Padding(padding: effectivePadding, child: current);
Ian Hickson's avatar
Ian Hickson committed
260 261 262 263 264 265
    return current;
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
266
    Widget result = LayoutBuilder(
Ian Hickson's avatar
Ian Hickson committed
267 268 269
      builder: _build,
    );
    if (widget.width != null || widget.height != null) {
270
      result = SizedBox(
Ian Hickson's avatar
Ian Hickson committed
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 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
        width: widget.width,
        height: widget.height,
        child: result,
      );
    }
    return result;
  }
}

/// A decoration on a part of a [Material].
///
/// This object is rarely created directly. Instead of creating an ink
/// decoration directly, consider using an [Ink] widget, which uses this class
/// in combination with [Padding] and [ConstrainedBox] to draw a decoration on a
/// [Material].
///
/// See also:
///
///  * [Ink], the corresponding widget.
///  * [InkResponse], which uses gestures to trigger ink highlights and ink
///    splashes in the parent [Material].
///  * [InkWell], which is a rectangular [InkResponse] (the most common type of
///    ink response).
///  * [Material], which is the widget on which the ink is painted.
class InkDecoration extends InkFeature {
  /// Draws a decoration on a [Material].
  InkDecoration({
    @required Decoration decoration,
    @required ImageConfiguration configuration,
    @required MaterialInkController controller,
    @required RenderBox referenceBox,
    VoidCallback onRemoved,
  }) : assert(configuration != null),
       _configuration = configuration,
       super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
    this.decoration = decoration;
    controller.addInkFeature(this);
  }

  BoxPainter _painter;

  /// What to paint on the [Material].
  ///
  /// The decoration is painted at the position and size of the [referenceBox],
  /// on the [Material] that owns the [controller].
  Decoration get decoration => _decoration;
  Decoration _decoration;
  set decoration(Decoration value) {
    if (value == _decoration)
      return;
    _decoration = value;
    _painter?.dispose();
    _painter = _decoration?.createBoxPainter(_handleChanged);
    controller.markNeedsPaint();
  }

  /// The configuration to pass to the [BoxPainter] obtained from the
  /// [decoration], when painting.
  ///
  /// The [ImageConfiguration.size] field is ignored (and replaced by the size
  /// of the [referenceBox], at paint time).
  ImageConfiguration get configuration => _configuration;
  ImageConfiguration _configuration;
  set configuration(ImageConfiguration value) {
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
    controller.markNeedsPaint();
  }

  void _handleChanged() {
    controller.markNeedsPaint();
  }

  @override
  void dispose() {
    _painter?.dispose();
    super.dispose();
  }

  @override
  void paintFeature(Canvas canvas, Matrix4 transform) {
    if (_painter == null)
      return;
    final Offset originOffset = MatrixUtils.getAsTranslation(transform);
    final ImageConfiguration sizedConfiguration = configuration.copyWith(
      size: referenceBox.size,
    );
    if (originOffset == null) {
      canvas.save();
      canvas.transform(transform.storage);
      _painter.paint(canvas, Offset.zero, sizedConfiguration);
      canvas.restore();
    } else {
      _painter.paint(canvas, originOffset, sizedConfiguration);
    }
  }
}