| // Copyright 2014 The Flutter 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. |
| /// |
| /// {@tool snippet} |
| /// |
| /// 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 |
| /// Material( |
| /// color: Colors.teal[900], |
| /// child: Center( |
| /// child: Ink( |
| /// color: Colors.yellow, |
| /// width: 200.0, |
| /// height: 100.0, |
| /// child: InkWell( |
| /// onTap: () { /* ... */ }, |
| /// child: Center( |
| /// child: Text('YELLOW'), |
| /// ) |
| /// ), |
| /// ), |
| /// ), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// {@tool snippet} |
| /// |
| /// The following example shows how an image can be printed on a [Material] |
| /// widget with an [InkWell] above it: |
| /// |
| /// ```dart |
| /// Material( |
| /// color: Colors.grey[800], |
| /// child: Center( |
| /// child: Ink.image( |
| /// image: AssetImage('cat.jpeg'), |
| /// fit: BoxFit.cover, |
| /// width: 300.0, |
| /// height: 200.0, |
| /// child: InkWell( |
| /// onTap: () { /* ... */ }, |
| /// child: Align( |
| /// alignment: Alignment.topLeft, |
| /// child: Padding( |
| /// padding: const EdgeInsets.all(10.0), |
| /// child: Text('KITTEN', style: TextStyle(fontWeight: FontWeight.w900, color: Colors.white)), |
| /// ), |
| /// ) |
| /// ), |
| /// ), |
| /// ), |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [Container], a more generic form of this widget which paints itself, |
| /// rather that deferring to the nearest [Material] widget. |
| /// * [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: 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. |
| 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: BoxDecoration(color: color)".' |
| ), |
| decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null), |
| 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 [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. If there is no |
| /// intention to render anything on this image, consider using a |
| /// [Container] with a [BoxDecoration.image] instead. The `onImageError` |
| /// argument may be provided to listen for errors when resolving the image. |
| /// |
| /// 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, |
| ImageErrorListener? onImageError, |
| ColorFilter? colorFilter, |
| BoxFit? fit, |
| AlignmentGeometry alignment = Alignment.center, |
| Rect? centerSlice, |
| ImageRepeat repeat = ImageRepeat.noRepeat, |
| bool matchTextDirection = false, |
| this.width, |
| this.height, |
| this.child, |
| }) : assert(padding == null || padding.isNonNegative), |
| assert(image != null), |
| assert(alignment != null), |
| assert(repeat != null), |
| assert(matchTextDirection != null), |
| decoration = BoxDecoration( |
| image: DecorationImage( |
| image: image, |
| onError: onImageError, |
| colorFilter: colorFilter, |
| fit: fit, |
| alignment: alignment, |
| centerSlice: centerSlice, |
| repeat: repeat, |
| matchTextDirection: matchTextDirection, |
| ), |
| ), |
| super(key: key); |
| |
| /// The [child] contained by the container. |
| /// |
| /// {@macro flutter.widgets.ProxyWidget.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 |
| /// [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 |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null)); |
| properties.add(DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null)); |
| } |
| |
| @override |
| _InkState createState() => _InkState(); |
| } |
| |
| class _InkState extends State<Ink> { |
| InkDecoration? _ink; |
| |
| void _handleRemoved() { |
| _ink = null; |
| } |
| |
| @override |
| void deactivate() { |
| _ink?.dispose(); |
| assert(_ink == null); |
| super.deactivate(); |
| } |
| |
| Widget _build(BuildContext context, BoxConstraints constraints) { |
| if (_ink == null) { |
| _ink = InkDecoration( |
| decoration: widget.decoration, |
| configuration: createLocalImageConfiguration(context), |
| controller: Material.of(context)!, |
| referenceBox: context.findRenderObject()! as RenderBox, |
| onRemoved: _handleRemoved, |
| ); |
| } else { |
| _ink!.decoration = widget.decoration; |
| _ink!.configuration = createLocalImageConfiguration(context); |
| } |
| Widget? current = widget.child; |
| final EdgeInsetsGeometry? effectivePadding = widget._paddingIncludingDecoration; |
| if (effectivePadding != null) |
| current = Padding(padding: effectivePadding, child: current); |
| return current ?? Container(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterial(context)); |
| Widget result = LayoutBuilder( |
| builder: _build, |
| ); |
| if (widget.width != null || widget.height != null) { |
| result = SizedBox( |
| 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); |
| } |
| } |
| } |