Skip to content

Commit 25174d6

Browse files
authored
Make InkDecoraiton not paint if the ink is not visible (#122585)
Make InkDecoration not paint if the ink is not visible
1 parent 4b2853d commit 25174d6

File tree

6 files changed

+319
-20
lines changed

6 files changed

+319
-20
lines changed

packages/flutter/lib/src/material/ink_decoration.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,15 @@ class _InkState extends State<Ink> {
279279
if (_ink == null) {
280280
_ink = InkDecoration(
281281
decoration: widget.decoration,
282+
isVisible: Visibility.of(context),
282283
configuration: createLocalImageConfiguration(context),
283284
controller: Material.of(context),
284285
referenceBox: _boxKey.currentContext!.findRenderObject()! as RenderBox,
285286
onRemoved: _handleRemoved,
286287
);
287288
} else {
288289
_ink!.decoration = widget.decoration;
290+
_ink!.isVisible = Visibility.of(context);
289291
_ink!.configuration = createLocalImageConfiguration(context);
290292
}
291293
return widget.child ?? ConstrainedBox(constraints: const BoxConstraints.expand());
@@ -329,12 +331,14 @@ class InkDecoration extends InkFeature {
329331
/// Draws a decoration on a [Material].
330332
InkDecoration({
331333
required Decoration? decoration,
334+
bool isVisible = true,
332335
required ImageConfiguration configuration,
333336
required super.controller,
334337
required super.referenceBox,
335338
super.onRemoved,
336339
}) : _configuration = configuration {
337340
this.decoration = decoration;
341+
this.isVisible = isVisible;
338342
controller.addInkFeature(this);
339343
}
340344

@@ -356,6 +360,19 @@ class InkDecoration extends InkFeature {
356360
controller.markNeedsPaint();
357361
}
358362

363+
/// Whether the decoration should be painted.
364+
///
365+
/// Defaults to true.
366+
bool get isVisible => _isVisible;
367+
bool _isVisible = true;
368+
set isVisible(bool value) {
369+
if (value == _isVisible) {
370+
return;
371+
}
372+
_isVisible = value;
373+
controller.markNeedsPaint();
374+
}
375+
359376
/// The configuration to pass to the [BoxPainter] obtained from the
360377
/// [decoration], when painting.
361378
///
@@ -383,7 +400,7 @@ class InkDecoration extends InkFeature {
383400

384401
@override
385402
void paintFeature(Canvas canvas, Matrix4 transform) {
386-
if (_painter == null) {
403+
if (_painter == null || !isVisible) {
387404
return;
388405
}
389406
final Offset? originOffset = MatrixUtils.getAsTranslation(transform);

packages/flutter/lib/src/widgets/basic.dart

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'binding.dart';
1515
import 'debug.dart';
1616
import 'framework.dart';
1717
import 'localizations.dart';
18+
import 'visibility.dart';
1819
import 'widget_span.dart';
1920

2021
export 'package:flutter/animation.dart';
@@ -3962,12 +3963,80 @@ class Stack extends MultiChildRenderObjectWidget {
39623963
///
39633964
/// * [Stack], for more details about stacks.
39643965
/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
3965-
class IndexedStack extends Stack {
3966+
class IndexedStack extends StatelessWidget {
39663967
/// Creates a [Stack] widget that paints a single child.
39673968
///
39683969
/// The [index] argument must not be null.
39693970
const IndexedStack({
39703971
super.key,
3972+
this.alignment = AlignmentDirectional.topStart,
3973+
this.textDirection,
3974+
this.clipBehavior = Clip.hardEdge,
3975+
this.sizing = StackFit.loose,
3976+
this.index = 0,
3977+
this.children = const <Widget>[],
3978+
});
3979+
3980+
/// How to align the non-positioned and partially-positioned children in the
3981+
/// stack.
3982+
///
3983+
/// Defaults to [AlignmentDirectional.topStart].
3984+
///
3985+
/// See [Stack.alignment] for more information.
3986+
final AlignmentGeometry alignment;
3987+
3988+
/// The text direction with which to resolve [alignment].
3989+
///
3990+
/// Defaults to the ambient [Directionality].
3991+
final TextDirection? textDirection;
3992+
3993+
/// {@macro flutter.material.Material.clipBehavior}
3994+
///
3995+
/// Defaults to [Clip.hardEdge].
3996+
final Clip clipBehavior;
3997+
3998+
/// How to size the non-positioned children in the stack.
3999+
///
4000+
/// Defaults to [StackFit.loose].
4001+
///
4002+
/// See [Stack.fit] for more information.
4003+
final StackFit sizing;
4004+
4005+
/// The index of the child to show.
4006+
///
4007+
/// If this is null, none of the children will be shown.
4008+
final int? index;
4009+
4010+
/// The child widgets of the stack.
4011+
///
4012+
/// Only the child at index [index] will be shown.
4013+
///
4014+
/// See [Stack.children] for more information.
4015+
final List<Widget> children;
4016+
4017+
@override
4018+
Widget build(BuildContext context) {
4019+
final List<Widget> wrappedChildren = List<Widget>.generate(children.length, (int i) {
4020+
return Visibility.maintain(
4021+
visible: i == index,
4022+
child: children[i],
4023+
);
4024+
});
4025+
return _RawIndexedStack(
4026+
alignment: alignment,
4027+
textDirection: textDirection,
4028+
clipBehavior: clipBehavior,
4029+
sizing: sizing,
4030+
index: index,
4031+
children: wrappedChildren,
4032+
);
4033+
}
4034+
}
4035+
4036+
/// The render object widget that backs [IndexedStack].
4037+
class _RawIndexedStack extends Stack {
4038+
/// Creates a [Stack] widget that paints a single child.
4039+
const _RawIndexedStack({
39714040
super.alignment,
39724041
super.textDirection,
39734042
super.clipBehavior,
@@ -3984,7 +4053,7 @@ class IndexedStack extends Stack {
39844053
assert(_debugCheckHasDirectionality(context));
39854054
return RenderIndexedStack(
39864055
index: index,
3987-
fit:fit,
4056+
fit: fit,
39884057
clipBehavior: clipBehavior,
39894058
alignment: alignment,
39904059
textDirection: textDirection ?? Directionality.maybeOf(context),

packages/flutter/lib/src/widgets/visibility.dart

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -223,39 +223,68 @@ class Visibility extends StatelessWidget {
223223
/// objects, to be immediately created if [visible] is true).
224224
final bool maintainInteractivity;
225225

226+
/// Tells the visibility state of an element in the tree based off its
227+
/// ancestor [Visibility] elements.
228+
///
229+
/// If there's one or more [Visibility] widgets in the ancestor tree, this
230+
/// will return true if and only if all of those widgets have [visible] set
231+
/// to true. If there is no [Visibility] widget in the ancestor tree of the
232+
/// specified build context, this will return true.
233+
///
234+
/// This will register a dependency from the specified context on any
235+
/// [Visibility] elements in the ancestor tree, such that if any of their
236+
/// visibilities changes, the specified context will be rebuilt.
237+
static bool of(BuildContext context) {
238+
bool isVisible = true;
239+
BuildContext ancestorContext = context;
240+
InheritedElement? ancestor = ancestorContext.getElementForInheritedWidgetOfExactType<_VisibilityScope>();
241+
while (isVisible && ancestor != null) {
242+
final _VisibilityScope scope = context.dependOnInheritedElement(ancestor) as _VisibilityScope;
243+
isVisible = scope.isVisible;
244+
ancestor.visitAncestorElements((Element parent) {
245+
ancestorContext = parent;
246+
return false;
247+
});
248+
ancestor = ancestorContext.getElementForInheritedWidgetOfExactType<_VisibilityScope>();
249+
}
250+
return isVisible;
251+
}
252+
226253
@override
227254
Widget build(BuildContext context) {
255+
Widget result = child;
228256
if (maintainSize) {
229-
Widget result = child;
230257
if (!maintainInteractivity) {
231258
result = IgnorePointer(
232259
ignoring: !visible,
233260
ignoringSemantics: !visible && !maintainSemantics,
234261
child: child,
235262
);
236263
}
237-
return _Visibility(
264+
result = _Visibility(
238265
visible: visible,
239266
maintainSemantics: maintainSemantics,
240267
child: result,
241268
);
242-
}
243-
assert(!maintainInteractivity);
244-
assert(!maintainSemantics);
245-
assert(!maintainSize);
246-
if (maintainState) {
247-
Widget result = child;
248-
if (!maintainAnimation) {
249-
result = TickerMode(enabled: visible, child: child);
269+
} else {
270+
assert(!maintainInteractivity);
271+
assert(!maintainSemantics);
272+
assert(!maintainSize);
273+
if (maintainState) {
274+
if (!maintainAnimation) {
275+
result = TickerMode(enabled: visible, child: child);
276+
}
277+
result = Offstage(
278+
offstage: !visible,
279+
child: result,
280+
);
281+
} else {
282+
assert(!maintainAnimation);
283+
assert(!maintainState);
284+
result = visible ? child : replacement;
250285
}
251-
return Offstage(
252-
offstage: !visible,
253-
child: result,
254-
);
255286
}
256-
assert(!maintainAnimation);
257-
assert(!maintainState);
258-
return visible ? child : replacement;
287+
return _VisibilityScope(isVisible: visible, child: result);
259288
}
260289

261290
@override
@@ -270,6 +299,18 @@ class Visibility extends StatelessWidget {
270299
}
271300
}
272301

302+
/// Inherited widget that allows descendants to find their visibility status.
303+
class _VisibilityScope extends InheritedWidget {
304+
const _VisibilityScope({required this.isVisible, required super.child});
305+
306+
final bool isVisible;
307+
308+
@override
309+
bool updateShouldNotify(_VisibilityScope old) {
310+
return isVisible != old.isVisible;
311+
}
312+
}
313+
273314
/// Whether to show or hide a sliver child.
274315
///
275316
/// By default, the [visible] property controls whether the [sliver] is included

packages/flutter/test/material/ink_paint_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,28 @@ void main() {
568568
..circle(x: 50.0, y: 50.0, color: splashColor)
569569
);
570570
});
571+
572+
testWidgets('Ink with isVisible=false does not paint', (WidgetTester tester) async {
573+
const Color testColor = Color(0xffff1234);
574+
Widget inkWidget({required bool isVisible}) {
575+
return Material(
576+
child: Visibility.maintain(
577+
visible: isVisible,
578+
child: Ink(
579+
decoration: const BoxDecoration(color: testColor),
580+
),
581+
),
582+
);
583+
}
584+
585+
await tester.pumpWidget(inkWidget(isVisible: true));
586+
RenderBox box = tester.renderObject(find.byType(Material));
587+
expect(box, paints..rect(color: testColor));
588+
589+
await tester.pumpWidget(inkWidget(isVisible: false));
590+
box = tester.renderObject(find.byType(Material));
591+
expect(box, isNot(paints..rect(color: testColor)));
592+
});
571593
}
572594

573595
class _InkRippleFactory extends InteractiveInkFeatureFactory {

packages/flutter/test/widgets/stack_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,41 @@ void main() {
308308
expect(itemsTapped, <int>[2]);
309309
});
310310

311+
testWidgets('IndexedStack sets non-selected indexes to visible=false', (WidgetTester tester) async {
312+
Widget buildStack({required int itemCount, required int? selectedIndex}) {
313+
final List<Widget> children = List<Widget>.generate(itemCount, (int i) {
314+
return _ShowVisibility(index: i);
315+
});
316+
return Directionality(
317+
textDirection: TextDirection.ltr,
318+
child: IndexedStack(
319+
index: selectedIndex,
320+
children: children,
321+
),
322+
);
323+
}
324+
325+
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: null));
326+
expect(find.text('index 0 is visible ? false', skipOffstage: false), findsOneWidget);
327+
expect(find.text('index 1 is visible ? false', skipOffstage: false), findsOneWidget);
328+
expect(find.text('index 2 is visible ? false', skipOffstage: false), findsOneWidget);
329+
330+
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: 0));
331+
expect(find.text('index 0 is visible ? true', skipOffstage: false), findsOneWidget);
332+
expect(find.text('index 1 is visible ? false', skipOffstage: false), findsOneWidget);
333+
expect(find.text('index 2 is visible ? false', skipOffstage: false), findsOneWidget);
334+
335+
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: 1));
336+
expect(find.text('index 0 is visible ? false', skipOffstage: false), findsOneWidget);
337+
expect(find.text('index 1 is visible ? true', skipOffstage: false), findsOneWidget);
338+
expect(find.text('index 2 is visible ? false', skipOffstage: false), findsOneWidget);
339+
340+
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: 2));
341+
expect(find.text('index 0 is visible ? false', skipOffstage: false), findsOneWidget);
342+
expect(find.text('index 1 is visible ? false', skipOffstage: false), findsOneWidget);
343+
expect(find.text('index 2 is visible ? true', skipOffstage: false), findsOneWidget);
344+
});
345+
311346
testWidgets('Can set width and height', (WidgetTester tester) async {
312347
const Key key = Key('container');
313348

@@ -866,3 +901,14 @@ void main() {
866901
]);
867902
});
868903
}
904+
905+
class _ShowVisibility extends StatelessWidget {
906+
const _ShowVisibility({required this.index});
907+
908+
final int index;
909+
910+
@override
911+
Widget build(BuildContext context) {
912+
return Text('index $index is visible ? ${Visibility.of(context)}');
913+
}
914+
}

0 commit comments

Comments
 (0)