// 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: const 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: const AssetImage('cat.jpeg'), /// fit: BoxFit.cover, /// width: 300.0, /// height: 200.0, /// child: InkWell( /// onTap: () { /* ... */ }, /// child: const Align( /// alignment: Alignment.topLeft, /// child: Padding( /// padding: EdgeInsets.all(10.0), /// child: Text( /// 'KITTEN', /// style: TextStyle( /// fontWeight: FontWeight.w900, /// color: Colors.white, /// ), /// ), /// ), /// ) /// ), /// ), /// ), /// ) /// ``` /// {@end-tool} /// /// What to do if you want to clip this [Ink.image]? /// /// {@tool dartpad} /// Wrapping the [Ink] in a clipping widget directly will not work since the /// [Material] it will be printed on is responsible for clipping. /// /// In this example the image is not being clipped as expected. This is because /// it is being rendered onto the Scaffold body Material, which isn't wrapped in /// the [ClipRRect]. /// /// ** See code in examples/api/lib/material/ink/ink.image_clip.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// One solution would be to deliberately wrap the [Ink.image] in a [Material]. /// This makes sure the Material that the image is painted on is also responsible /// for clipping said content. /// /// ** See code in examples/api/lib/material/ink/ink.image_clip.1.dart ** /// {@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({ super.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); /// 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({ super.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, ), ); /// 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 ?? EdgeInsets.zero; } 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 State<Ink> createState() => _InkState(); } class _InkState extends State<Ink> { final GlobalKey _boxKey = GlobalKey(); InkDecoration? _ink; void _handleRemoved() { _ink = null; } @override void deactivate() { _ink?.dispose(); assert(_ink == null); super.deactivate(); } Widget _build(BuildContext context) { // By creating the InkDecoration from within a Builder widget, we can // use the RenderBox of the Padding widget. if (_ink == null) { _ink = InkDecoration( decoration: widget.decoration, configuration: createLocalImageConfiguration(context), controller: Material.of(context), referenceBox: _boxKey.currentContext!.findRenderObject()! as RenderBox, onRemoved: _handleRemoved, ); } else { _ink!.decoration = widget.decoration; _ink!.configuration = createLocalImageConfiguration(context); } return widget.child ?? const SizedBox(); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); Widget result = Padding( key: _boxKey, padding: widget._paddingIncludingDecoration, child: Builder(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 super.controller, required super.referenceBox, super.onRemoved, }) : assert(configuration != null), _configuration = configuration { 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); } } }