diff --git a/examples/api/lib/material/ink/ink.image_clip.0.dart b/examples/api/lib/material/ink/ink.image_clip.0.dart new file mode 100644 index 0000000000..1914a0dc2f --- /dev/null +++ b/examples/api/lib/material/ink/ink.image_clip.0.dart @@ -0,0 +1,55 @@ +// 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. + +// Flutter code sample for Image.frameBuilder + +import 'package:flutter/material.dart'; + +void main() { + runApp(MaterialApp( + title: 'Flutter Code Sample', + home: Scaffold( + appBar: AppBar(title: const Text('Flutter Code Sample')), + body: const Center( + child: MyStatelessWidget( + image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg'), + ), + ), + ), + )); +} + +class MyStatelessWidget extends StatelessWidget { + const MyStatelessWidget({Key? key, required this.image}) : super(key: key); + + final ImageProvider image; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(100), + child: Ink.image( + fit: BoxFit.fill, + width: 300, + height: 300, + image: image, + child: InkWell( + onTap: () {/* ... */}, + child: const Align( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Text( + 'PUFFIN', + style: TextStyle( + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/examples/api/lib/material/ink/ink.image_clip.1.dart b/examples/api/lib/material/ink/ink.image_clip.1.dart new file mode 100644 index 0000000000..2cb0bf8914 --- /dev/null +++ b/examples/api/lib/material/ink/ink.image_clip.1.dart @@ -0,0 +1,57 @@ +// 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. + +// Flutter code sample for Image.frameBuilder + +import 'package:flutter/material.dart'; + +void main() { + runApp(MaterialApp( + title: 'Flutter Code Sample', + home: Scaffold( + appBar: AppBar(title: const Text('Flutter Code Sample')), + body: const Center( + child: MyStatelessWidget( + image: NetworkImage('https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg'), + ), + ), + ), + )); +} + +class MyStatelessWidget extends StatelessWidget { + const MyStatelessWidget({Key? key, required this.image}) : super(key: key); + + final ImageProvider image; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(100), + child: Material( + child: Ink.image( + fit: BoxFit.fill, + width: 300, + height: 300, + image: image, + child: InkWell( + onTap: () {/* ... */}, + child: const Align( + child: Padding( + padding: EdgeInsets.all(10.0), + child: Text( + 'PUFFIN', + style: TextStyle( + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/examples/api/test/material/ink/ink.image_clip.0.test.dart b/examples/api/test/material/ink/ink.image_clip.0.test.dart new file mode 100644 index 0000000000..474fa09abc --- /dev/null +++ b/examples/api/test/material/ink/ink.image_clip.0.test.dart @@ -0,0 +1,34 @@ +// 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 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/ink/ink.image_clip.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const List kTransparentImage = [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, + 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + ]; + + testWidgets('Ink ancestor material is not clipped', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: example.MyStatelessWidget( + image: MemoryImage(Uint8List.fromList(kTransparentImage)), + ), + ), + ), + ); + + final Finder inkMaterialFinder = find.ancestor(of: find.byType(Ink), matching: find.byType(Material)); + expect(find.ancestor(of: inkMaterialFinder, matching: find.byType(ClipRRect)), findsNothing); + }); +} diff --git a/examples/api/test/material/ink/ink.image_clip.1.test.dart b/examples/api/test/material/ink/ink.image_clip.1.test.dart new file mode 100644 index 0000000000..0428c3603a --- /dev/null +++ b/examples/api/test/material/ink/ink.image_clip.1.test.dart @@ -0,0 +1,34 @@ +// 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 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_api_samples/material/ink/ink.image_clip.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const List kTransparentImage = [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, + 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + ]; + + testWidgets('Ink ancestor material is clipped', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: example.MyStatelessWidget( + image: MemoryImage(Uint8List.fromList(kTransparentImage)), + ), + ), + ), + ); + + final Finder inkMaterialFinder = find.ancestor(of: find.byType(Ink), matching: find.byType(Material)); + expect(find.ancestor(of: inkMaterialFinder, matching: find.byType(ClipRRect)), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/material/ink_decoration.dart b/packages/flutter/lib/src/material/ink_decoration.dart index e93f02b873..0050ddd10d 100644 --- a/packages/flutter/lib/src/material/ink_decoration.dart +++ b/packages/flutter/lib/src/material/ink_decoration.dart @@ -104,6 +104,27 @@ import 'material.dart'; /// ``` /// {@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, diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 0f8786cb4e..919227a22b 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -1159,6 +1159,18 @@ class _InkResponseState extends State<_InkResponseStateWidget> /// ancestor to the ink well). The [MaterialType.transparency] material /// kind can be used for this purpose. /// +/// ### InkWell isn't clipping properly +/// +/// If you want to clip an InkWell or any [Ink] widgets you need to keep in mind +/// that the [Material] that the Ink will be printed on is responsible for clipping. +/// This means you can't wrap the [Ink] widget in a clipping widget directly, +/// since this will leave the [Material] not clipped (and by extension the printed +/// [Ink] widgets as well). +/// +/// An easy solution is to deliberately wrap the [Ink] widgets you want to clip +/// in a [Material], and wrap that in a clipping widget instead. See [Ink] for +/// an example. +/// /// ### The ink splashes don't track the size of an animated container /// If the size of an InkWell's [Material] ancestor changes while the InkWell's /// splashes are expanding, you may notice that the splashes aren't clipped diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index 2af28f7ae6..12dd8f4870 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -91,6 +91,7 @@ abstract class MaterialInkController { /// 1. Clipping: If [clipBehavior] is not [Clip.none], Material clips its widget /// sub-tree to the shape specified by [shape], [type], and [borderRadius]. /// By default, [clipBehavior] is [Clip.none] for performance considerations. +/// See [Ink] for an example of how this affects clipping [Ink] widgets. /// 2. Elevation: Material elevates its widget sub-tree on the Z axis by /// [elevation] pixels, and draws the appropriate shadow. /// 3. Ink effects: Material shows ink effects implemented by [InkFeature]s