ink_decoration.dart 12.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
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
// 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 snippet}
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
/// {@end-tool}
71
/// {@tool snippet}
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
///  * [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].
  ///
113 114 115 116 117 118 119 120
  /// The `color` argument is a shorthand for
  /// `decoration: 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`.
  ///
  /// If there is no intention to render anything on this decoration, consider
  /// using a [Container] with a [BoxDecoration] instead.
Ian Hickson's avatar
Ian Hickson committed
121 122 123 124 125 126 127 128 129 130 131 132
  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'
133
         'The color argument is just a shorthand for "decoration: BoxDecoration(color: color)".'
Ian Hickson's avatar
Ian Hickson committed
134
       ),
135
       decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
Ian Hickson's avatar
Ian Hickson committed
136 137 138 139 140 141
       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
142
  /// its [BoxDecoration.image] property set to the [Ink] constructor. The
Ian Hickson's avatar
Ian Hickson committed
143 144 145
  /// properties of the [DecorationImage] of that [BoxDecoration] are set
  /// according to the arguments passed to this method.
  ///
146 147
  /// The `image` argument must not be null. If there is no
  /// intention to render anything on this image, consider using a
148 149
  /// [Container] with a [BoxDecoration.image] instead. The `onImageError`
  /// argument may be provided to listen for errors when resolving the image.
150 151 152
  ///
  /// The `alignment`, `repeat`, and `matchTextDirection` arguments must not
  /// be null either, but they have default values.
Ian Hickson's avatar
Ian Hickson committed
153 154 155 156 157 158
  ///
  /// See [paintImage] for a description of the meaning of these arguments.
  Ink.image({
    Key key,
    this.padding,
    @required ImageProvider image,
159
    ImageErrorListener onImageError,
Ian Hickson's avatar
Ian Hickson committed
160 161
    ColorFilter colorFilter,
    BoxFit fit,
162
    AlignmentGeometry alignment = Alignment.center,
Ian Hickson's avatar
Ian Hickson committed
163
    Rect centerSlice,
164 165
    ImageRepeat repeat = ImageRepeat.noRepeat,
    bool matchTextDirection = false,
Ian Hickson's avatar
Ian Hickson committed
166 167 168 169 170 171 172 173
    this.width,
    this.height,
    this.child,
  }) : assert(padding == null || padding.isNonNegative),
       assert(image != null),
       assert(alignment != null),
       assert(repeat != null),
       assert(matchTextDirection != null),
174 175
       decoration = BoxDecoration(
         image: DecorationImage(
Ian Hickson's avatar
Ian Hickson committed
176
           image: image,
177
           onError: onImageError,
Ian Hickson's avatar
Ian Hickson committed
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
           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.
  ///
206 207
  /// A shorthand for specifying just an image is also available using the
  /// [Ink.image] constructor.
Ian Hickson's avatar
Ian Hickson committed
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  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
228 229
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
230 231
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
    properties.add(DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null));
Ian Hickson's avatar
Ian Hickson committed
232 233 234
  }

  @override
235
  _InkState createState() => _InkState();
Ian Hickson's avatar
Ian Hickson committed
236 237 238 239 240 241 242 243 244 245 246
}

class _InkState extends State<Ink> {
  InkDecoration _ink;

  void _handleRemoved() {
    _ink = null;
  }

  @override
  void deactivate() {
247
    _ink?.dispose();
Ian Hickson's avatar
Ian Hickson committed
248 249 250 251 252 253
    assert(_ink == null);
    super.deactivate();
  }

  Widget _build(BuildContext context, BoxConstraints constraints) {
    if (_ink == null) {
254
      _ink = InkDecoration(
Ian Hickson's avatar
Ian Hickson committed
255 256 257
        decoration: widget.decoration,
        configuration: createLocalImageConfiguration(context),
        controller: Material.of(context),
258
        referenceBox: context.findRenderObject() as RenderBox,
Ian Hickson's avatar
Ian Hickson committed
259 260 261 262 263 264 265 266 267
        onRemoved: _handleRemoved,
      );
    } else {
      _ink.decoration = widget.decoration;
      _ink.configuration = createLocalImageConfiguration(context);
    }
    Widget current = widget.child;
    final EdgeInsetsGeometry effectivePadding = widget._paddingIncludingDecoration;
    if (effectivePadding != null)
268
      current = Padding(padding: effectivePadding, child: current);
269
    return current ?? Container();
Ian Hickson's avatar
Ian Hickson committed
270 271 272 273 274
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
275
    Widget result = LayoutBuilder(
Ian Hickson's avatar
Ian Hickson committed
276 277 278
      builder: _build,
    );
    if (widget.width != null || widget.height != null) {
279
      result = SizedBox(
Ian Hickson's avatar
Ian Hickson committed
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 370 371 372 373 374 375 376 377 378
        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);
    }
  }
}