ink_decoration.dart 13 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
///         child: const Center(
63
///           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
///   child: Center(
///     child: Ink.image(
81
///       image: const 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: const Align(
Ian Hickson's avatar
Ian Hickson committed
88
///           alignment: Alignment.topLeft,
89
///           child: Padding(
90 91 92 93 94 95 96 97
///             padding: EdgeInsets.all(10.0),
///             child: Text(
///               'KITTEN',
///               style: TextStyle(
///                 fontWeight: FontWeight.w900,
///                 color: Colors.white,
///               ),
///             ),
Ian Hickson's avatar
Ian Hickson committed
98 99 100 101 102 103 104
///           ),
///         )
///       ),
///     ),
///   ),
/// )
/// ```
105
/// {@end-tool}
Ian Hickson's avatar
Ian Hickson committed
106 107 108 109
///
/// See also:
///
///  * [Container], a more generic form of this widget which paints itself,
Josh Soref's avatar
Josh Soref committed
110
///    rather that deferring to the nearest [Material] widget.
Ian Hickson's avatar
Ian Hickson committed
111 112 113 114 115 116 117 118
///  * [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].
  ///
119 120 121 122 123 124 125 126
  /// 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
127
  Ink({
128
    Key? key,
Ian Hickson's avatar
Ian Hickson committed
129
    this.padding,
130 131
    Color? color,
    Decoration? decoration,
Ian Hickson's avatar
Ian Hickson committed
132 133 134 135 136 137 138
    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'
139
         'The color argument is just a shorthand for "decoration: BoxDecoration(color: color)".',
Ian Hickson's avatar
Ian Hickson committed
140
       ),
141
       decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
Ian Hickson's avatar
Ian Hickson committed
142 143 144 145 146 147
       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
148
  /// its [BoxDecoration.image] property set to the [Ink] constructor. The
Ian Hickson's avatar
Ian Hickson committed
149 150 151
  /// properties of the [DecorationImage] of that [BoxDecoration] are set
  /// according to the arguments passed to this method.
  ///
152 153
  /// The `image` argument must not be null. If there is no
  /// intention to render anything on this image, consider using a
154 155
  /// [Container] with a [BoxDecoration.image] instead. The `onImageError`
  /// argument may be provided to listen for errors when resolving the image.
156 157 158
  ///
  /// The `alignment`, `repeat`, and `matchTextDirection` arguments must not
  /// be null either, but they have default values.
Ian Hickson's avatar
Ian Hickson committed
159 160 161
  ///
  /// See [paintImage] for a description of the meaning of these arguments.
  Ink.image({
162
    Key? key,
Ian Hickson's avatar
Ian Hickson committed
163
    this.padding,
164 165 166 167
    required ImageProvider image,
    ImageErrorListener? onImageError,
    ColorFilter? colorFilter,
    BoxFit? fit,
168
    AlignmentGeometry alignment = Alignment.center,
169
    Rect? centerSlice,
170 171
    ImageRepeat repeat = ImageRepeat.noRepeat,
    bool matchTextDirection = false,
Ian Hickson's avatar
Ian Hickson committed
172 173 174 175 176 177 178 179
    this.width,
    this.height,
    this.child,
  }) : assert(padding == null || padding.isNonNegative),
       assert(image != null),
       assert(alignment != null),
       assert(repeat != null),
       assert(matchTextDirection != null),
180 181
       decoration = BoxDecoration(
         image: DecorationImage(
Ian Hickson's avatar
Ian Hickson committed
182
           image: image,
183
           onError: onImageError,
Ian Hickson's avatar
Ian Hickson committed
184 185 186 187 188 189 190 191 192 193 194 195
           colorFilter: colorFilter,
           fit: fit,
           alignment: alignment,
           centerSlice: centerSlice,
           repeat: repeat,
           matchTextDirection: matchTextDirection,
         ),
       ),
       super(key: key);

  /// The [child] contained by the container.
  ///
196
  /// {@macro flutter.widgets.ProxyWidget.child}
197
  final Widget? child;
Ian Hickson's avatar
Ian Hickson committed
198 199 200 201 202 203

  /// 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].
204
  final EdgeInsetsGeometry? padding;
Ian Hickson's avatar
Ian Hickson committed
205 206 207 208 209 210 211

  /// 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.
  ///
212 213
  /// A shorthand for specifying just an image is also available using the
  /// [Ink.image] constructor.
214
  final Decoration? decoration;
Ian Hickson's avatar
Ian Hickson committed
215 216 217

  /// A width to apply to the [decoration] and the [child]. The width includes
  /// any [padding].
218
  final double? width;
Ian Hickson's avatar
Ian Hickson committed
219 220 221

  /// A height to apply to the [decoration] and the [child]. The height includes
  /// any [padding].
222
  final double? height;
Ian Hickson's avatar
Ian Hickson committed
223

224
  EdgeInsetsGeometry get _paddingIncludingDecoration {
225
    if (decoration == null || decoration!.padding == null)
226 227
      return padding ?? EdgeInsets.zero;
    final EdgeInsetsGeometry decorationPadding = decoration!.padding!;
Ian Hickson's avatar
Ian Hickson committed
228 229
    if (padding == null)
      return decorationPadding;
230
    return padding!.add(decorationPadding);
Ian Hickson's avatar
Ian Hickson committed
231 232 233
  }

  @override
234 235
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
236 237
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
    properties.add(DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null));
Ian Hickson's avatar
Ian Hickson committed
238 239 240
  }

  @override
241
  _InkState createState() => _InkState();
Ian Hickson's avatar
Ian Hickson committed
242 243 244
}

class _InkState extends State<Ink> {
245
  final GlobalKey _boxKey = GlobalKey();
246
  InkDecoration? _ink;
Ian Hickson's avatar
Ian Hickson committed
247 248 249 250 251 252 253

  void _handleRemoved() {
    _ink = null;
  }

  @override
  void deactivate() {
254
    _ink?.dispose();
Ian Hickson's avatar
Ian Hickson committed
255
    assert(_ink == null);
256
    super.deactivate();
Ian Hickson's avatar
Ian Hickson committed
257 258
  }

259 260 261
  Widget _build(BuildContext context) {
    // By creating the InkDecoration from within a Builder widget, we can
    // use the RenderBox of the Padding widget.
Ian Hickson's avatar
Ian Hickson committed
262
    if (_ink == null) {
263
      _ink = InkDecoration(
Ian Hickson's avatar
Ian Hickson committed
264 265
        decoration: widget.decoration,
        configuration: createLocalImageConfiguration(context),
266
        controller: Material.of(context)!,
267
        referenceBox: _boxKey.currentContext!.findRenderObject()! as RenderBox,
Ian Hickson's avatar
Ian Hickson committed
268 269 270
        onRemoved: _handleRemoved,
      );
    } else {
271 272
      _ink!.decoration = widget.decoration;
      _ink!.configuration = createLocalImageConfiguration(context);
Ian Hickson's avatar
Ian Hickson committed
273
    }
274
    return widget.child ?? Container();
Ian Hickson's avatar
Ian Hickson committed
275 276 277 278 279
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
280 281 282 283
    Widget result = Padding(
      key: _boxKey,
      padding: widget._paddingIncludingDecoration,
      child: Builder(builder: _build),
Ian Hickson's avatar
Ian Hickson committed
284 285
    );
    if (widget.width != null || widget.height != null) {
286
      result = SizedBox(
Ian Hickson's avatar
Ian Hickson committed
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
        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({
314 315 316 317 318
    required Decoration? decoration,
    required ImageConfiguration configuration,
    required MaterialInkController controller,
    required RenderBox referenceBox,
    VoidCallback? onRemoved,
Ian Hickson's avatar
Ian Hickson committed
319 320 321 322 323 324 325
  }) : assert(configuration != null),
       _configuration = configuration,
       super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
    this.decoration = decoration;
    controller.addInkFeature(this);
  }

326
  BoxPainter? _painter;
Ian Hickson's avatar
Ian Hickson committed
327 328 329 330 331

  /// 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].
332 333 334
  Decoration? get decoration => _decoration;
  Decoration? _decoration;
  set decoration(Decoration? value) {
Ian Hickson's avatar
Ian Hickson committed
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
    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;
372
    final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
Ian Hickson's avatar
Ian Hickson committed
373 374 375 376 377 378
    final ImageConfiguration sizedConfiguration = configuration.copyWith(
      size: referenceBox.size,
    );
    if (originOffset == null) {
      canvas.save();
      canvas.transform(transform.storage);
379
      _painter!.paint(canvas, Offset.zero, sizedConfiguration);
Ian Hickson's avatar
Ian Hickson committed
380 381
      canvas.restore();
    } else {
382
      _painter!.paint(canvas, originOffset, sizedConfiguration);
Ian Hickson's avatar
Ian Hickson committed
383 384 385
    }
  }
}