Unverified Commit 09270dcb authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

The Ink widget (#13900)

This provides a way to draw colors, images, and general decorations on Material widgets, without interfering with InkWells that are further descendants of the widget.

This thus provides a cleaner way to solve the issue of FlatButtons and InkWells not working when placed over Image widgets than the old hack of introducing a transparency Material.

Fixes #3782.

Also, some fixes to documentation, and remove a redundant property on the Image widget.
parent ab874da7
...@@ -52,6 +52,7 @@ export 'src/material/grid_tile.dart'; ...@@ -52,6 +52,7 @@ export 'src/material/grid_tile.dart';
export 'src/material/grid_tile_bar.dart'; export 'src/material/grid_tile_bar.dart';
export 'src/material/icon_button.dart'; export 'src/material/icon_button.dart';
export 'src/material/icons.dart'; export 'src/material/icons.dart';
export 'src/material/ink_decoration.dart';
export 'src/material/ink_highlight.dart'; export 'src/material/ink_highlight.dart';
export 'src/material/ink_ripple.dart'; export 'src/material/ink_ripple.dart';
export 'src/material/ink_splash.dart'; export 'src/material/ink_splash.dart';
......
...@@ -36,6 +36,26 @@ import 'theme.dart'; ...@@ -36,6 +36,26 @@ import 'theme.dart';
/// ///
/// Flat buttons will expand to fit the child widget, if necessary. /// Flat buttons will expand to fit the child widget, if necessary.
/// ///
/// ## Troubleshooting
///
/// ### Why does my button not have splash effects?
///
/// If you place a [FlatButton] on top of an [Image], [Container],
/// [DecoratedBox], or some other widget that draws an opaque background between
/// the [FlatButton] and its ancestor [Material], the splashes will not be
/// visible. This is because ink splashes draw in the [Material] itself, as if
/// the ink was spreading inside the material.
///
/// The [Ink] widget can be used as a replacement for [Image], [Container], or
/// [DecoratedBox] to ensure that the image or decoration also paints in the
/// [Material] itself, below the ink.
///
/// If this is not possible for some reason, e.g. because you are using an
/// opaque [CustomPaint] widget, alternatively consider using a second
/// [Material] above the opaque widget but below the [FlatButton] (as an
/// ancestor to the button). The [MaterialType.transparency] material kind can
/// be used for this purpose.
///
/// See also: /// See also:
/// ///
/// * [RaisedButton], which is a button that hovers above the containing /// * [RaisedButton], which is a button that hovers above the containing
......
// Copyright 2017 The Chromium 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/foundation.dart';
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.
///
/// ## Sample code
///
/// 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
/// new Material(
/// color: Colors.teal[900],
/// child: new Center(
/// child: new Ink(
/// color: Colors.yellow,
/// width: 200.0,
/// height: 100.0,
/// child: new InkWell(
/// onTap: () { /* ... */ },
/// child: new Center(
/// child: new Text('YELLOW'),
/// )
/// ),
/// ),
/// ),
/// )
/// ```
///
/// The following example shows how an image can be printed on a [Material]
/// widget with an [InkWell] above it:
///
/// ```dart
/// new Material(
/// color: Colors.grey[800],
/// child: new Center(
/// child: new Ink.image(
/// image: new AssetImage('cat.jpeg'),
/// fit: BoxFit.cover,
/// width: 300.0,
/// height: 200.0,
/// child: new InkWell(
/// onTap: () { /* ... */ },
/// child: new Align(
/// alignment: Alignment.topLeft,
/// child: new Padding(
/// padding: const EdgeInsets.all(10.0),
/// child: new Text('KITTEN', style: new TextStyle(fontWeight: FontWeight.w900, color: Colors.white)),
/// ),
/// )
/// ),
/// ),
/// ),
/// )
/// ```
///
/// See also:
///
/// * [Container], a more generic form of this widget which paints itself,
/// rather that defering 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: new
/// 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`.
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: new BoxDecoration(color: color)".'
),
decoration = decoration ?? (color != null ? new 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 [new 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. 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,
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 = new BoxDecoration(
image: new DecorationImage(
image: image,
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}
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 [new
/// 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 description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
description.add(new DiagnosticsProperty<Decoration>('bg', decoration, defaultValue: null));
}
@override
_InkState createState() => new _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 = new InkDecoration(
decoration: widget.decoration,
configuration: createLocalImageConfiguration(context),
controller: Material.of(context),
referenceBox: context.findRenderObject(),
onRemoved: _handleRemoved,
);
} else {
_ink.decoration = widget.decoration;
_ink.configuration = createLocalImageConfiguration(context);
}
Widget current = widget.child;
final EdgeInsetsGeometry effectivePadding = widget._paddingIncludingDecoration;
if (effectivePadding != null)
current = new Padding(padding: effectivePadding, child: current);
return current;
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
Widget result = new LayoutBuilder(
builder: _build,
);
if (widget.width != null || widget.height != null) {
result = new 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);
}
}
}
...@@ -119,11 +119,11 @@ class InkSplash extends InteractiveInkFeature { ...@@ -119,11 +119,11 @@ class InkSplash extends InteractiveInkFeature {
Color color, Color color,
bool containedInkWell: false, bool containedInkWell: false,
RectCallback rectCallback, RectCallback rectCallback,
BorderRadius borderRadius = BorderRadius.zero, BorderRadius borderRadius,
double radius, double radius,
VoidCallback onRemoved, VoidCallback onRemoved,
}) : _position = position, }) : _position = position,
_borderRadius = borderRadius, _borderRadius = borderRadius ?? BorderRadius.zero,
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position), _targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position),
_clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback), _clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
_repositionToReferenceBox = !containedInkWell, _repositionToReferenceBox = !containedInkWell,
......
...@@ -155,7 +155,16 @@ abstract class InteractiveInkFeatureFactory { ...@@ -155,7 +155,16 @@ abstract class InteractiveInkFeatureFactory {
/// ```dart /// ```dart
/// assert(debugCheckHasMaterial(context)); /// assert(debugCheckHasMaterial(context));
/// ``` /// ```
/// The parameter [enableFeedback] must not be null. ///
/// ## Troubleshooting
///
/// ### The ink splashes aren't visible!
///
/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
/// [DecoratedBox], between the [Material] widget and the [InkResponse] widget,
/// then the splash won't be visible because it will be under the opaque
/// graphic. To avoid this problem, consider using an [Ink] widget to draw the
/// opaque graphic itself on the [Material], under the ink splash.
/// ///
/// See also: /// See also:
/// ///
...@@ -166,6 +175,9 @@ class InkResponse extends StatefulWidget { ...@@ -166,6 +175,9 @@ class InkResponse extends StatefulWidget {
/// Creates an area of a [Material] that responds to touch. /// Creates an area of a [Material] that responds to touch.
/// ///
/// Must have an ancestor [Material] widget in which to cause ink reactions. /// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// The [containedInkWell], [highlightShape], [enableFeedback], and
/// [excludeFromSemantics] arguments must not be null.
const InkResponse({ const InkResponse({
Key key, Key key,
this.child, this.child,
...@@ -176,13 +188,17 @@ class InkResponse extends StatefulWidget { ...@@ -176,13 +188,17 @@ class InkResponse extends StatefulWidget {
this.containedInkWell: false, this.containedInkWell: false,
this.highlightShape: BoxShape.circle, this.highlightShape: BoxShape.circle,
this.radius, this.radius,
this.borderRadius: BorderRadius.zero, this.borderRadius,
this.highlightColor, this.highlightColor,
this.splashColor, this.splashColor,
this.splashFactory, this.splashFactory,
this.enableFeedback: true, this.enableFeedback: true,
this.excludeFromSemantics: false, this.excludeFromSemantics: false,
}) : assert(enableFeedback != null), super(key: key); }) : assert(containedInkWell != null),
assert(highlightShape != null),
assert(enableFeedback != null),
assert(excludeFromSemantics != null),
super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
...@@ -251,6 +267,8 @@ class InkResponse extends StatefulWidget { ...@@ -251,6 +267,8 @@ class InkResponse extends StatefulWidget {
final double radius; final double radius;
/// The clipping radius of the containing rect. /// The clipping radius of the containing rect.
///
/// If this is null, it is interpreted as [BorderRadius.zero].
final BorderRadius borderRadius; final BorderRadius borderRadius;
/// The highlight color of the ink response. If this property is null then the /// The highlight color of the ink response. If this property is null then the
...@@ -403,7 +421,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe ...@@ -403,7 +421,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
final Offset position = referenceBox.globalToLocal(details.globalPosition); final Offset position = referenceBox.globalToLocal(details.globalPosition);
final Color color = widget.splashColor ?? Theme.of(context).splashColor; final Color color = widget.splashColor ?? Theme.of(context).splashColor;
final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null; final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
final BorderRadius borderRadius = widget.borderRadius ?? BorderRadius.zero; final BorderRadius borderRadius = widget.borderRadius;
InteractiveInkFeature splash; InteractiveInkFeature splash;
void onRemoved() { void onRemoved() {
...@@ -532,6 +550,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe ...@@ -532,6 +550,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
/// assert(debugCheckHasMaterial(context)); /// assert(debugCheckHasMaterial(context));
/// ``` /// ```
/// ///
/// ## Troubleshooting
///
/// ### The ink splashes aren't visible!
///
/// If there is an opaque graphic, e.g. painted using a [Container], [Image], or
/// [DecoratedBox], between the [Material] widget and the [InkWell] widget, then
/// the splash won't be visible because it will be under the opaque graphic. To
/// avoid this problem, consider using an [Ink] widget to draw the opaque
/// graphic itself on the [Material], under the ink splash.
///
/// See also: /// See also:
/// ///
/// * [GestureDetector], for listening for gestures without ink splashes. /// * [GestureDetector], for listening for gestures without ink splashes.
...@@ -542,6 +570,9 @@ class InkWell extends InkResponse { ...@@ -542,6 +570,9 @@ class InkWell extends InkResponse {
/// Creates an ink well. /// Creates an ink well.
/// ///
/// Must have an ancestor [Material] widget in which to cause ink reactions. /// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// The [enableFeedback] and [excludeFromSemantics] arguments must not be
/// null.
const InkWell({ const InkWell({
Key key, Key key,
Widget child, Widget child,
......
...@@ -90,11 +90,14 @@ abstract class MaterialInkController { ...@@ -90,11 +90,14 @@ abstract class MaterialInkController {
/// contents because content that is conceptually printing on a separate piece /// contents because content that is conceptually printing on a separate piece
/// of material cannot be printed beyond the bounds of the material. /// of material cannot be printed beyond the bounds of the material.
/// ///
/// If the layout changes (e.g. because there's a list on the paper, and it's /// If the layout changes (e.g. because there's a list on the material, and it's
/// been scrolled), a LayoutChangedNotification must be dispatched at the /// been scrolled), a [LayoutChangedNotification] must be dispatched at the
/// relevant subtree. (This in particular means that Transitions should not be /// relevant subtree. This in particular means that transitions (e.g.
/// placed inside Material.) Otherwise, in-progress ink features (e.g., ink /// [SlideTransition]) should not be placed inside [Material] widgets so as to
/// splashes and ink highlights) won't move to account for the new layout. /// move subtrees that contain [InkResponse]s, [InkWell]s, [Ink]s, or other
/// widgets that use the [InkFeature] mechanism. Otherwise, in-progress ink
/// features (e.g., ink splashes and ink highlights) won't move to account for
/// the new layout.
/// ///
/// In general, the features of a [Material] should not change over time (e.g. a /// In general, the features of a [Material] should not change over time (e.g. a
/// [Material] should not change its [color], [shadowColor] or [type]). The one /// [Material] should not change its [color], [shadowColor] or [type]). The one
......
...@@ -39,6 +39,8 @@ import 'image.dart'; ...@@ -39,6 +39,8 @@ import 'image.dart';
/// ///
/// See also: /// See also:
/// ///
/// * [Ink], which paints a [Decoration] on a [Material], allowing
/// [InkResponse] and [InkWell] splashes to paint over them.
/// * [DecoratedBoxTransition], the version of this class that animates on the /// * [DecoratedBoxTransition], the version of this class that animates on the
/// [decoration] property. /// [decoration] property.
/// * [Decoration], which you can extend to provide other effects with /// * [Decoration], which you can extend to provide other effects with
...@@ -224,6 +226,8 @@ class DecoratedBox extends SingleChildRenderObjectWidget { ...@@ -224,6 +226,8 @@ class DecoratedBox extends SingleChildRenderObjectWidget {
/// * [AnimatedContainer], a variant that smoothly animates the properties when /// * [AnimatedContainer], a variant that smoothly animates the properties when
/// they change. /// they change.
/// * [Border], which has a sample which uses [Container] heavily. /// * [Border], which has a sample which uses [Container] heavily.
/// * [Ink], which paints a [Decoration] on a [Material], allowing
/// [InkResponse] and [InkWell] splashes to paint over them.
/// * The [catalog of layout widgets](https://flutter.io/widgets/layout/). /// * The [catalog of layout widgets](https://flutter.io/widgets/layout/).
class Container extends StatelessWidget { class Container extends StatelessWidget {
/// Creates a widget that combines common painting, positioning, and sizing widgets. /// Creates a widget that combines common painting, positioning, and sizing widgets.
...@@ -292,6 +296,9 @@ class Container extends StatelessWidget { ...@@ -292,6 +296,9 @@ class Container extends StatelessWidget {
/// Empty space to inscribe inside the [decoration]. The [child], if any, is /// Empty space to inscribe inside the [decoration]. The [child], if any, is
/// placed inside this padding. /// placed inside this padding.
///
/// This padding is in addition to any padding inherent in the [decoration];
/// see [Decoration.padding].
final EdgeInsetsGeometry padding; final EdgeInsetsGeometry padding;
/// The decoration to paint behind the [child]. /// The decoration to paint behind the [child].
......
...@@ -101,7 +101,10 @@ Future<Null> precacheImage(ImageProvider provider, BuildContext context, { Size ...@@ -101,7 +101,10 @@ Future<Null> precacheImage(ImageProvider provider, BuildContext context, { Size
/// ///
/// See also: /// See also:
/// ///
/// * [Icon] /// * [Icon], which shows an image from a font.
/// * [new Ink.image], which is the preferred way to show an image in a
/// material application (especially if the image is in a [Material] and will
/// have an [InkWell] on top of it).
class Image extends StatefulWidget { class Image extends StatefulWidget {
/// Creates a widget that displays an image. /// Creates a widget that displays an image.
/// ///
...@@ -110,6 +113,11 @@ class Image extends StatefulWidget { ...@@ -110,6 +113,11 @@ class Image extends StatefulWidget {
/// ///
/// The [image], [alignment], [repeat], and [matchTextDirection] arguments /// The [image], [alignment], [repeat], and [matchTextDirection] arguments
/// must not be null. /// must not be null.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
const Image({ const Image({
Key key, Key key,
@required this.image, @required this.image,
...@@ -123,7 +131,6 @@ class Image extends StatefulWidget { ...@@ -123,7 +131,6 @@ class Image extends StatefulWidget {
this.centerSlice, this.centerSlice,
this.matchTextDirection: false, this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package,
}) : assert(image != null), }) : assert(image != null),
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
...@@ -133,9 +140,16 @@ class Image extends StatefulWidget { ...@@ -133,9 +140,16 @@ class Image extends StatefulWidget {
/// Creates a widget that displays an [ImageStream] obtained from the network. /// Creates a widget that displays an [ImageStream] obtained from the network.
/// ///
/// The [src], [scale], and [repeat] arguments must not be null. /// The [src], [scale], and [repeat] arguments must not be null.
/// An optional [headers] argument can be used to use custom HTTP headers. ///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
/// ///
/// All network images are cached regardless of HTTP headers. /// All network images are cached regardless of HTTP headers.
///
/// An optional [headers] argument can be used to send custom HTTP headers
/// with the image request.
Image.network(String src, { Image.network(String src, {
Key key, Key key,
double scale: 1.0, double scale: 1.0,
...@@ -149,7 +163,6 @@ class Image extends StatefulWidget { ...@@ -149,7 +163,6 @@ class Image extends StatefulWidget {
this.centerSlice, this.centerSlice,
this.matchTextDirection: false, this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package,
Map<String, String> headers, Map<String, String> headers,
}) : image = new NetworkImage(src, scale: scale, headers: headers), }) : image = new NetworkImage(src, scale: scale, headers: headers),
assert(alignment != null), assert(alignment != null),
...@@ -161,6 +174,11 @@ class Image extends StatefulWidget { ...@@ -161,6 +174,11 @@ class Image extends StatefulWidget {
/// ///
/// The [file], [scale], and [repeat] arguments must not be null. /// The [file], [scale], and [repeat] arguments must not be null.
/// ///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// On Android, this may require the /// On Android, this may require the
/// `android.permission.READ_EXTERNAL_STORAGE` permission. /// `android.permission.READ_EXTERNAL_STORAGE` permission.
Image.file(File file, { Image.file(File file, {
...@@ -176,7 +194,6 @@ class Image extends StatefulWidget { ...@@ -176,7 +194,6 @@ class Image extends StatefulWidget {
this.centerSlice, this.centerSlice,
this.matchTextDirection: false, this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package,
}) : image = new FileImage(file, scale: scale), }) : image = new FileImage(file, scale: scale),
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
...@@ -211,6 +228,11 @@ class Image extends StatefulWidget { ...@@ -211,6 +228,11 @@ class Image extends StatefulWidget {
/// ///
/// The [name] and [repeat] arguments must not be null. /// The [name] and [repeat] arguments must not be null.
/// ///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
///
/// ## Sample code /// ## Sample code
/// ///
/// Suppose that the project's `pubspec.yaml` file contains the following: /// Suppose that the project's `pubspec.yaml` file contains the following:
...@@ -281,8 +303,7 @@ class Image extends StatefulWidget { ...@@ -281,8 +303,7 @@ class Image extends StatefulWidget {
/// - packages/fancy_backgrounds/backgrounds/background1.png /// - packages/fancy_backgrounds/backgrounds/background1.png
/// ``` /// ```
/// ///
/// Note that the `lib/` is implied, so it should not be included in the asset /// The `lib/` is implied, so it should not be included in the asset path.
/// path.
/// ///
/// ///
/// See also: /// See also:
...@@ -307,7 +328,7 @@ class Image extends StatefulWidget { ...@@ -307,7 +328,7 @@ class Image extends StatefulWidget {
this.centerSlice, this.centerSlice,
this.matchTextDirection: false, this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package, String package,
}) : image = scale != null }) : image = scale != null
? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package) ? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: new AssetImage(name, bundle: bundle, package: package), : new AssetImage(name, bundle: bundle, package: package),
...@@ -319,6 +340,11 @@ class Image extends StatefulWidget { ...@@ -319,6 +340,11 @@ class Image extends StatefulWidget {
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List]. /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
/// ///
/// The [bytes], [scale], and [repeat] arguments must not be null. /// The [bytes], [scale], and [repeat] arguments must not be null.
///
/// Either the [width] and [height] arguments should be specified, or the
/// widget should be placed in a context that sets tight layout constraints.
/// Otherwise, the image dimensions will change as the image is loaded, which
/// will result in ugly layout changes.
Image.memory(Uint8List bytes, { Image.memory(Uint8List bytes, {
Key key, Key key,
double scale: 1.0, double scale: 1.0,
...@@ -332,7 +358,6 @@ class Image extends StatefulWidget { ...@@ -332,7 +358,6 @@ class Image extends StatefulWidget {
this.centerSlice, this.centerSlice,
this.matchTextDirection: false, this.matchTextDirection: false,
this.gaplessPlayback: false, this.gaplessPlayback: false,
this.package,
}) : image = new MemoryImage(bytes, scale: scale), }) : image = new MemoryImage(bytes, scale: scale),
assert(alignment != null), assert(alignment != null),
assert(repeat != null), assert(repeat != null),
...@@ -346,12 +371,24 @@ class Image extends StatefulWidget { ...@@ -346,12 +371,24 @@ class Image extends StatefulWidget {
/// ///
/// If null, the image will pick a size that best preserves its intrinsic /// If null, the image will pick a size that best preserves its intrinsic
/// aspect ratio. /// aspect ratio.
///
/// It is strongly recommended that either both the [width] and the [height]
/// be specified, or that the widget be placed in a context that sets tight
/// layout constraints, so that the image does not change size as it loads.
/// Consider using [fit] to adapt the image's rendering to fit the given width
/// and height if the exact image dimensions are not known in advance.
final double width; final double width;
/// If non-null, require the image to have this height. /// If non-null, require the image to have this height.
/// ///
/// If null, the image will pick a size that best preserves its intrinsic /// If null, the image will pick a size that best preserves its intrinsic
/// aspect ratio. /// aspect ratio.
///
/// It is strongly recommended that either both the [width] and the [height]
/// be specified, or that the widget be placed in a context that sets tight
/// layout constraints, so that the image does not change size as it loads.
/// Consider using [fit] to adapt the image's rendering to fit the given width
/// and height if the exact image dimensions are not known in advance.
final double height; final double height;
/// If non-null, this color is blended with each image pixel using [colorBlendMode]. /// If non-null, this color is blended with each image pixel using [colorBlendMode].
...@@ -433,10 +470,6 @@ class Image extends StatefulWidget { ...@@ -433,10 +470,6 @@ class Image extends StatefulWidget {
/// (false), when the image provider changes. /// (false), when the image provider changes.
final bool gaplessPlayback; final bool gaplessPlayback;
/// The name of the package from which the image is included. See the
/// documentation for the [Image.asset] constructor for details.
final String package;
@override @override
_ImageState createState() => new _ImageState(); _ImageState createState() => new _ImageState();
......
...@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
void main() { void main() {
testWidgets('The inkwell widget renders an ink splash', (WidgetTester tester) async { testWidgets('The InkWell widget renders an ink splash', (WidgetTester tester) async {
final Color highlightColor = const Color(0xAAFF0000); final Color highlightColor = const Color(0xAAFF0000);
final Color splashColor = const Color(0xAA0000FF); final Color splashColor = const Color(0xAA0000FF);
final BorderRadius borderRadius = new BorderRadius.circular(6.0); final BorderRadius borderRadius = new BorderRadius.circular(6.0);
...@@ -51,7 +51,7 @@ void main() { ...@@ -51,7 +51,7 @@ void main() {
await gesture.up(); await gesture.up();
}); });
testWidgets('The inkwell widget renders an ink ripple', (WidgetTester tester) async { testWidgets('The InkWell widget renders an ink ripple', (WidgetTester tester) async {
final Color highlightColor = const Color(0xAAFF0000); final Color highlightColor = const Color(0xAAFF0000);
final Color splashColor = const Color(0xB40000FF); final Color splashColor = const Color(0xB40000FF);
final BorderRadius borderRadius = new BorderRadius.circular(6.0); final BorderRadius borderRadius = new BorderRadius.circular(6.0);
...@@ -172,6 +172,79 @@ void main() { ...@@ -172,6 +172,79 @@ void main() {
Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 0 Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 0
Found: center == $center radius == $radius alpha == ${paint.color.alpha}'''; Found: center == $center radius == $radius alpha == ${paint.color.alpha}''';
})); }));
});
testWidgets('Does the Ink widget render anything', (WidgetTester tester) async {
await tester.pumpWidget(
new Material(
child: new Center(
child: new Ink(
color: Colors.blue,
width: 200.0,
height: 200.0,
child: new InkWell(
splashColor: Colors.green,
onTap: () { },
),
),
),
),
);
final Offset center = tester.getCenter(find.byType(InkWell));
final TestGesture gesture = await tester.startGesture(center);
await tester.pump(); // start gesture
await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
expect(
box,
paints
..rect(rect: new Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), color: new Color(Colors.blue.value))
..circle(color: new Color(Colors.green.value))
);
await tester.pumpWidget(
new Material(
child: new Center(
child: new Ink(
color: Colors.red,
width: 200.0,
height: 200.0,
child: new InkWell(
splashColor: Colors.green,
onTap: () { },
),
),
),
),
);
expect(Material.of(tester.element(find.byType(InkWell))), same(box));
expect(
box,
paints
..rect(rect: new Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), color: new Color(Colors.red.value))
..circle(color: new Color(Colors.green.value))
);
await tester.pumpWidget(
new Material(
child: new Center(
child: new InkWell( // this is at a different depth in the tree so it's now a new InkWell
splashColor: Colors.green,
onTap: () { },
),
),
),
);
expect(Material.of(tester.element(find.byType(InkWell))), same(box));
expect(box, isNot(paints..rect()));
expect(box, isNot(paints..circle()));
await gesture.up();
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment