blob: e93f02b8736fa23b90103a217a2bdc895529a742 [file] [log] [blame]
// 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}
///
/// 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 ?? 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 ?? Container();
}
@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 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);
}
}
}