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';
export 'src/material/grid_tile_bar.dart';
export 'src/material/icon_button.dart';
export 'src/material/icons.dart';
export 'src/material/ink_decoration.dart';
export 'src/material/ink_highlight.dart';
export 'src/material/ink_ripple.dart';
export 'src/material/ink_splash.dart';
......
......@@ -36,6 +36,26 @@ import 'theme.dart';
///
/// 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:
///
/// * [RaisedButton], which is a button that hovers above the containing
......
This diff is collapsed.
......@@ -119,11 +119,11 @@ class InkSplash extends InteractiveInkFeature {
Color color,
bool containedInkWell: false,
RectCallback rectCallback,
BorderRadius borderRadius = BorderRadius.zero,
BorderRadius borderRadius,
double radius,
VoidCallback onRemoved,
}) : _position = position,
_borderRadius = borderRadius,
_borderRadius = borderRadius ?? BorderRadius.zero,
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position),
_clipCallback = _getClipCallback(referenceBox, containedInkWell, rectCallback),
_repositionToReferenceBox = !containedInkWell,
......
......@@ -155,7 +155,16 @@ abstract class InteractiveInkFeatureFactory {
/// ```dart
/// 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:
///
......@@ -166,6 +175,9 @@ class InkResponse extends StatefulWidget {
/// Creates an area of a [Material] that responds to touch.
///
/// 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({
Key key,
this.child,
......@@ -176,13 +188,17 @@ class InkResponse extends StatefulWidget {
this.containedInkWell: false,
this.highlightShape: BoxShape.circle,
this.radius,
this.borderRadius: BorderRadius.zero,
this.borderRadius,
this.highlightColor,
this.splashColor,
this.splashFactory,
this.enableFeedback: true,
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.
///
......@@ -251,6 +267,8 @@ class InkResponse extends StatefulWidget {
final double radius;
/// The clipping radius of the containing rect.
///
/// If this is null, it is interpreted as [BorderRadius.zero].
final BorderRadius borderRadius;
/// 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
final Offset position = referenceBox.globalToLocal(details.globalPosition);
final Color color = widget.splashColor ?? Theme.of(context).splashColor;
final RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null;
final BorderRadius borderRadius = widget.borderRadius ?? BorderRadius.zero;
final BorderRadius borderRadius = widget.borderRadius;
InteractiveInkFeature splash;
void onRemoved() {
......@@ -532,6 +550,16 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
/// 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:
///
/// * [GestureDetector], for listening for gestures without ink splashes.
......@@ -542,6 +570,9 @@ class InkWell extends InkResponse {
/// Creates an ink well.
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// The [enableFeedback] and [excludeFromSemantics] arguments must not be
/// null.
const InkWell({
Key key,
Widget child,
......
......@@ -90,11 +90,14 @@ abstract class MaterialInkController {
/// contents because content that is conceptually printing on a separate piece
/// 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
/// been scrolled), a LayoutChangedNotification must be dispatched at the
/// relevant subtree. (This in particular means that Transitions should not be
/// placed inside Material.) Otherwise, in-progress ink features (e.g., ink
/// splashes and ink highlights) won't move to account for the new layout.
/// 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
/// relevant subtree. This in particular means that transitions (e.g.
/// [SlideTransition]) should not be placed inside [Material] widgets so as to
/// 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
/// [Material] should not change its [color], [shadowColor] or [type]). The one
......
......@@ -39,6 +39,8 @@ import 'image.dart';
///
/// 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
/// [decoration] property.
/// * [Decoration], which you can extend to provide other effects with
......@@ -224,6 +226,8 @@ class DecoratedBox extends SingleChildRenderObjectWidget {
/// * [AnimatedContainer], a variant that smoothly animates the properties when
/// they change.
/// * [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/).
class Container extends StatelessWidget {
/// Creates a widget that combines common painting, positioning, and sizing widgets.
......@@ -292,6 +296,9 @@ class Container extends StatelessWidget {
/// 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 behind the [child].
......
......@@ -101,7 +101,10 @@ Future<Null> precacheImage(ImageProvider provider, BuildContext context, { Size
///
/// 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 {
/// Creates a widget that displays an image.
///
......@@ -110,6 +113,11 @@ class Image extends StatefulWidget {
///
/// The [image], [alignment], [repeat], and [matchTextDirection] 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.
const Image({
Key key,
@required this.image,
......@@ -123,7 +131,6 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false,
this.package,
}) : assert(image != null),
assert(alignment != null),
assert(repeat != null),
......@@ -133,9 +140,16 @@ class Image extends StatefulWidget {
/// Creates a widget that displays an [ImageStream] obtained from the network.
///
/// 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.
///
/// An optional [headers] argument can be used to send custom HTTP headers
/// with the image request.
Image.network(String src, {
Key key,
double scale: 1.0,
......@@ -149,7 +163,6 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false,
this.package,
Map<String, String> headers,
}) : image = new NetworkImage(src, scale: scale, headers: headers),
assert(alignment != null),
......@@ -161,6 +174,11 @@ class Image extends StatefulWidget {
///
/// 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
/// `android.permission.READ_EXTERNAL_STORAGE` permission.
Image.file(File file, {
......@@ -176,7 +194,6 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false,
this.package,
}) : image = new FileImage(file, scale: scale),
assert(alignment != null),
assert(repeat != null),
......@@ -211,6 +228,11 @@ class Image extends StatefulWidget {
///
/// 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
///
/// Suppose that the project's `pubspec.yaml` file contains the following:
......@@ -281,8 +303,7 @@ class Image extends StatefulWidget {
/// - packages/fancy_backgrounds/backgrounds/background1.png
/// ```
///
/// Note that the `lib/` is implied, so it should not be included in the asset
/// path.
/// The `lib/` is implied, so it should not be included in the asset path.
///
///
/// See also:
......@@ -307,7 +328,7 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false,
this.package,
String package,
}) : image = scale != null
? new ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
: new AssetImage(name, bundle: bundle, package: package),
......@@ -319,6 +340,11 @@ class Image extends StatefulWidget {
/// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
///
/// 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, {
Key key,
double scale: 1.0,
......@@ -332,7 +358,6 @@ class Image extends StatefulWidget {
this.centerSlice,
this.matchTextDirection: false,
this.gaplessPlayback: false,
this.package,
}) : image = new MemoryImage(bytes, scale: scale),
assert(alignment != null),
assert(repeat != null),
......@@ -346,12 +371,24 @@ class Image extends StatefulWidget {
///
/// If null, the image will pick a size that best preserves its intrinsic
/// 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;
/// If non-null, require the image to have this height.
///
/// If null, the image will pick a size that best preserves its intrinsic
/// 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;
/// If non-null, this color is blended with each image pixel using [colorBlendMode].
......@@ -433,10 +470,6 @@ class Image extends StatefulWidget {
/// (false), when the image provider changes.
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
_ImageState createState() => new _ImageState();
......
......@@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
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 splashColor = const Color(0xAA0000FF);
final BorderRadius borderRadius = new BorderRadius.circular(6.0);
......@@ -51,7 +51,7 @@ void main() {
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 splashColor = const Color(0xB40000FF);
final BorderRadius borderRadius = new BorderRadius.circular(6.0);
......@@ -172,6 +172,79 @@ void main() {
Expected: center == $expectedCenter, radius == $expectedRadius, alpha == 0
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