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
  Ink({
122
    Key? key,
Ian Hickson's avatar
Ian Hickson committed
123
    this.padding,
124 125
    Color? color,
    Decoration? decoration,
Ian Hickson's avatar
Ian Hickson committed
126 127 128 129 130 131 132
    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
  ///
  /// See [paintImage] for a description of the meaning of these arguments.
  Ink.image({
156
    Key? key,
Ian Hickson's avatar
Ian Hickson committed
157
    this.padding,
158 159 160 161
    required ImageProvider image,
    ImageErrorListener? onImageError,
    ColorFilter? colorFilter,
    BoxFit? fit,
162
    AlignmentGeometry alignment = Alignment.center,
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
           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}
191
  final Widget? child;
Ian Hickson's avatar
Ian Hickson committed
192 193 194 195 196 197

  /// 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].
198
  final EdgeInsetsGeometry? padding;
Ian Hickson's avatar
Ian Hickson committed
199 200 201 202 203 204 205

  /// 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.
208
  final Decoration? decoration;
Ian Hickson's avatar
Ian Hickson committed
209 210 211

  /// A width to apply to the [decoration] and the [child]. The width includes
  /// any [padding].
212
  final double? width;
Ian Hickson's avatar
Ian Hickson committed
213 214 215

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

218 219
  EdgeInsetsGeometry? get _paddingIncludingDecoration {
    if (decoration == null || decoration!.padding == null)
Ian Hickson's avatar
Ian Hickson committed
220
      return padding;
221
    final EdgeInsetsGeometry? decorationPadding = decoration!.padding;
Ian Hickson's avatar
Ian Hickson committed
222 223
    if (padding == null)
      return decorationPadding;
224
    return padding!.add(decorationPadding!);
Ian Hickson's avatar
Ian Hickson committed
225 226 227
  }

  @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
}

class _InkState extends State<Ink> {
239
  InkDecoration? _ink;
Ian Hickson's avatar
Ian Hickson committed
240 241 242 243 244 245 246

  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
        decoration: widget.decoration,
        configuration: createLocalImageConfiguration(context),
257
        controller: Material.of(context)!,
258
        referenceBox: context.findRenderObject()! as RenderBox,
Ian Hickson's avatar
Ian Hickson committed
259 260 261
        onRemoved: _handleRemoved,
      );
    } else {
262 263
      _ink!.decoration = widget.decoration;
      _ink!.configuration = createLocalImageConfiguration(context);
Ian Hickson's avatar
Ian Hickson committed
264
    }
265 266
    Widget? current = widget.child;
    final EdgeInsetsGeometry? effectivePadding = widget._paddingIncludingDecoration;
Ian Hickson's avatar
Ian Hickson committed
267
    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
        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({
307 308 309 310 311
    required Decoration? decoration,
    required ImageConfiguration configuration,
    required MaterialInkController controller,
    required RenderBox referenceBox,
    VoidCallback? onRemoved,
Ian Hickson's avatar
Ian Hickson committed
312 313 314 315 316 317 318
  }) : assert(configuration != null),
       _configuration = configuration,
       super(controller: controller, referenceBox: referenceBox, onRemoved: onRemoved) {
    this.decoration = decoration;
    controller.addInkFeature(this);
  }

319
  BoxPainter? _painter;
Ian Hickson's avatar
Ian Hickson committed
320 321 322 323 324

  /// 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].
325 326 327
  Decoration? get decoration => _decoration;
  Decoration? _decoration;
  set decoration(Decoration? value) {
Ian Hickson's avatar
Ian Hickson committed
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
    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;
365
    final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
Ian Hickson's avatar
Ian Hickson committed
366 367 368 369 370 371
    final ImageConfiguration sizedConfiguration = configuration.copyWith(
      size: referenceBox.size,
    );
    if (originOffset == null) {
      canvas.save();
      canvas.transform(transform.storage);
372
      _painter!.paint(canvas, Offset.zero, sizedConfiguration);
Ian Hickson's avatar
Ian Hickson committed
373 374
      canvas.restore();
    } else {
375
      _painter!.paint(canvas, originOffset, sizedConfiguration);
Ian Hickson's avatar
Ian Hickson committed
376 377 378
    }
  }
}