Unverified Commit 25174d62 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Make InkDecoraiton not paint if the ink is not visible (#122585)

Make InkDecoration not paint if the ink is not visible
parent 4b2853dd
...@@ -279,6 +279,7 @@ class _InkState extends State<Ink> { ...@@ -279,6 +279,7 @@ class _InkState extends State<Ink> {
if (_ink == null) { if (_ink == null) {
_ink = InkDecoration( _ink = InkDecoration(
decoration: widget.decoration, decoration: widget.decoration,
isVisible: Visibility.of(context),
configuration: createLocalImageConfiguration(context), configuration: createLocalImageConfiguration(context),
controller: Material.of(context), controller: Material.of(context),
referenceBox: _boxKey.currentContext!.findRenderObject()! as RenderBox, referenceBox: _boxKey.currentContext!.findRenderObject()! as RenderBox,
...@@ -286,6 +287,7 @@ class _InkState extends State<Ink> { ...@@ -286,6 +287,7 @@ class _InkState extends State<Ink> {
); );
} else { } else {
_ink!.decoration = widget.decoration; _ink!.decoration = widget.decoration;
_ink!.isVisible = Visibility.of(context);
_ink!.configuration = createLocalImageConfiguration(context); _ink!.configuration = createLocalImageConfiguration(context);
} }
return widget.child ?? ConstrainedBox(constraints: const BoxConstraints.expand()); return widget.child ?? ConstrainedBox(constraints: const BoxConstraints.expand());
...@@ -329,12 +331,14 @@ class InkDecoration extends InkFeature { ...@@ -329,12 +331,14 @@ class InkDecoration extends InkFeature {
/// Draws a decoration on a [Material]. /// Draws a decoration on a [Material].
InkDecoration({ InkDecoration({
required Decoration? decoration, required Decoration? decoration,
bool isVisible = true,
required ImageConfiguration configuration, required ImageConfiguration configuration,
required super.controller, required super.controller,
required super.referenceBox, required super.referenceBox,
super.onRemoved, super.onRemoved,
}) : _configuration = configuration { }) : _configuration = configuration {
this.decoration = decoration; this.decoration = decoration;
this.isVisible = isVisible;
controller.addInkFeature(this); controller.addInkFeature(this);
} }
...@@ -356,6 +360,19 @@ class InkDecoration extends InkFeature { ...@@ -356,6 +360,19 @@ class InkDecoration extends InkFeature {
controller.markNeedsPaint(); controller.markNeedsPaint();
} }
/// Whether the decoration should be painted.
///
/// Defaults to true.
bool get isVisible => _isVisible;
bool _isVisible = true;
set isVisible(bool value) {
if (value == _isVisible) {
return;
}
_isVisible = value;
controller.markNeedsPaint();
}
/// The configuration to pass to the [BoxPainter] obtained from the /// The configuration to pass to the [BoxPainter] obtained from the
/// [decoration], when painting. /// [decoration], when painting.
/// ///
...@@ -383,7 +400,7 @@ class InkDecoration extends InkFeature { ...@@ -383,7 +400,7 @@ class InkDecoration extends InkFeature {
@override @override
void paintFeature(Canvas canvas, Matrix4 transform) { void paintFeature(Canvas canvas, Matrix4 transform) {
if (_painter == null) { if (_painter == null || !isVisible) {
return; return;
} }
final Offset? originOffset = MatrixUtils.getAsTranslation(transform); final Offset? originOffset = MatrixUtils.getAsTranslation(transform);
......
...@@ -15,6 +15,7 @@ import 'binding.dart'; ...@@ -15,6 +15,7 @@ import 'binding.dart';
import 'debug.dart'; import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'visibility.dart';
import 'widget_span.dart'; import 'widget_span.dart';
export 'package:flutter/animation.dart'; export 'package:flutter/animation.dart';
...@@ -3962,12 +3963,80 @@ class Stack extends MultiChildRenderObjectWidget { ...@@ -3962,12 +3963,80 @@ class Stack extends MultiChildRenderObjectWidget {
/// ///
/// * [Stack], for more details about stacks. /// * [Stack], for more details about stacks.
/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). /// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/).
class IndexedStack extends Stack { class IndexedStack extends StatelessWidget {
/// Creates a [Stack] widget that paints a single child. /// Creates a [Stack] widget that paints a single child.
/// ///
/// The [index] argument must not be null. /// The [index] argument must not be null.
const IndexedStack({ const IndexedStack({
super.key, super.key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.clipBehavior = Clip.hardEdge,
this.sizing = StackFit.loose,
this.index = 0,
this.children = const <Widget>[],
});
/// How to align the non-positioned and partially-positioned children in the
/// stack.
///
/// Defaults to [AlignmentDirectional.topStart].
///
/// See [Stack.alignment] for more information.
final AlignmentGeometry alignment;
/// The text direction with which to resolve [alignment].
///
/// Defaults to the ambient [Directionality].
final TextDirection? textDirection;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// How to size the non-positioned children in the stack.
///
/// Defaults to [StackFit.loose].
///
/// See [Stack.fit] for more information.
final StackFit sizing;
/// The index of the child to show.
///
/// If this is null, none of the children will be shown.
final int? index;
/// The child widgets of the stack.
///
/// Only the child at index [index] will be shown.
///
/// See [Stack.children] for more information.
final List<Widget> children;
@override
Widget build(BuildContext context) {
final List<Widget> wrappedChildren = List<Widget>.generate(children.length, (int i) {
return Visibility.maintain(
visible: i == index,
child: children[i],
);
});
return _RawIndexedStack(
alignment: alignment,
textDirection: textDirection,
clipBehavior: clipBehavior,
sizing: sizing,
index: index,
children: wrappedChildren,
);
}
}
/// The render object widget that backs [IndexedStack].
class _RawIndexedStack extends Stack {
/// Creates a [Stack] widget that paints a single child.
const _RawIndexedStack({
super.alignment, super.alignment,
super.textDirection, super.textDirection,
super.clipBehavior, super.clipBehavior,
...@@ -3984,7 +4053,7 @@ class IndexedStack extends Stack { ...@@ -3984,7 +4053,7 @@ class IndexedStack extends Stack {
assert(_debugCheckHasDirectionality(context)); assert(_debugCheckHasDirectionality(context));
return RenderIndexedStack( return RenderIndexedStack(
index: index, index: index,
fit:fit, fit: fit,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
alignment: alignment, alignment: alignment,
textDirection: textDirection ?? Directionality.maybeOf(context), textDirection: textDirection ?? Directionality.maybeOf(context),
......
...@@ -223,10 +223,37 @@ class Visibility extends StatelessWidget { ...@@ -223,10 +223,37 @@ class Visibility extends StatelessWidget {
/// objects, to be immediately created if [visible] is true). /// objects, to be immediately created if [visible] is true).
final bool maintainInteractivity; final bool maintainInteractivity;
/// Tells the visibility state of an element in the tree based off its
/// ancestor [Visibility] elements.
///
/// If there's one or more [Visibility] widgets in the ancestor tree, this
/// will return true if and only if all of those widgets have [visible] set
/// to true. If there is no [Visibility] widget in the ancestor tree of the
/// specified build context, this will return true.
///
/// This will register a dependency from the specified context on any
/// [Visibility] elements in the ancestor tree, such that if any of their
/// visibilities changes, the specified context will be rebuilt.
static bool of(BuildContext context) {
bool isVisible = true;
BuildContext ancestorContext = context;
InheritedElement? ancestor = ancestorContext.getElementForInheritedWidgetOfExactType<_VisibilityScope>();
while (isVisible && ancestor != null) {
final _VisibilityScope scope = context.dependOnInheritedElement(ancestor) as _VisibilityScope;
isVisible = scope.isVisible;
ancestor.visitAncestorElements((Element parent) {
ancestorContext = parent;
return false;
});
ancestor = ancestorContext.getElementForInheritedWidgetOfExactType<_VisibilityScope>();
}
return isVisible;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget result = child;
if (maintainSize) { if (maintainSize) {
Widget result = child;
if (!maintainInteractivity) { if (!maintainInteractivity) {
result = IgnorePointer( result = IgnorePointer(
ignoring: !visible, ignoring: !visible,
...@@ -234,28 +261,30 @@ class Visibility extends StatelessWidget { ...@@ -234,28 +261,30 @@ class Visibility extends StatelessWidget {
child: child, child: child,
); );
} }
return _Visibility( result = _Visibility(
visible: visible, visible: visible,
maintainSemantics: maintainSemantics, maintainSemantics: maintainSemantics,
child: result, child: result,
); );
} } else {
assert(!maintainInteractivity); assert(!maintainInteractivity);
assert(!maintainSemantics); assert(!maintainSemantics);
assert(!maintainSize); assert(!maintainSize);
if (maintainState) { if (maintainState) {
Widget result = child; if (!maintainAnimation) {
if (!maintainAnimation) { result = TickerMode(enabled: visible, child: child);
result = TickerMode(enabled: visible, child: child); }
result = Offstage(
offstage: !visible,
child: result,
);
} else {
assert(!maintainAnimation);
assert(!maintainState);
result = visible ? child : replacement;
} }
return Offstage(
offstage: !visible,
child: result,
);
} }
assert(!maintainAnimation); return _VisibilityScope(isVisible: visible, child: result);
assert(!maintainState);
return visible ? child : replacement;
} }
@override @override
...@@ -270,6 +299,18 @@ class Visibility extends StatelessWidget { ...@@ -270,6 +299,18 @@ class Visibility extends StatelessWidget {
} }
} }
/// Inherited widget that allows descendants to find their visibility status.
class _VisibilityScope extends InheritedWidget {
const _VisibilityScope({required this.isVisible, required super.child});
final bool isVisible;
@override
bool updateShouldNotify(_VisibilityScope old) {
return isVisible != old.isVisible;
}
}
/// Whether to show or hide a sliver child. /// Whether to show or hide a sliver child.
/// ///
/// By default, the [visible] property controls whether the [sliver] is included /// By default, the [visible] property controls whether the [sliver] is included
......
...@@ -568,6 +568,28 @@ void main() { ...@@ -568,6 +568,28 @@ void main() {
..circle(x: 50.0, y: 50.0, color: splashColor) ..circle(x: 50.0, y: 50.0, color: splashColor)
); );
}); });
testWidgets('Ink with isVisible=false does not paint', (WidgetTester tester) async {
const Color testColor = Color(0xffff1234);
Widget inkWidget({required bool isVisible}) {
return Material(
child: Visibility.maintain(
visible: isVisible,
child: Ink(
decoration: const BoxDecoration(color: testColor),
),
),
);
}
await tester.pumpWidget(inkWidget(isVisible: true));
RenderBox box = tester.renderObject(find.byType(Material));
expect(box, paints..rect(color: testColor));
await tester.pumpWidget(inkWidget(isVisible: false));
box = tester.renderObject(find.byType(Material));
expect(box, isNot(paints..rect(color: testColor)));
});
} }
class _InkRippleFactory extends InteractiveInkFeatureFactory { class _InkRippleFactory extends InteractiveInkFeatureFactory {
......
...@@ -308,6 +308,41 @@ void main() { ...@@ -308,6 +308,41 @@ void main() {
expect(itemsTapped, <int>[2]); expect(itemsTapped, <int>[2]);
}); });
testWidgets('IndexedStack sets non-selected indexes to visible=false', (WidgetTester tester) async {
Widget buildStack({required int itemCount, required int? selectedIndex}) {
final List<Widget> children = List<Widget>.generate(itemCount, (int i) {
return _ShowVisibility(index: i);
});
return Directionality(
textDirection: TextDirection.ltr,
child: IndexedStack(
index: selectedIndex,
children: children,
),
);
}
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: null));
expect(find.text('index 0 is visible ? false', skipOffstage: false), findsOneWidget);
expect(find.text('index 1 is visible ? false', skipOffstage: false), findsOneWidget);
expect(find.text('index 2 is visible ? false', skipOffstage: false), findsOneWidget);
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: 0));
expect(find.text('index 0 is visible ? true', skipOffstage: false), findsOneWidget);
expect(find.text('index 1 is visible ? false', skipOffstage: false), findsOneWidget);
expect(find.text('index 2 is visible ? false', skipOffstage: false), findsOneWidget);
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: 1));
expect(find.text('index 0 is visible ? false', skipOffstage: false), findsOneWidget);
expect(find.text('index 1 is visible ? true', skipOffstage: false), findsOneWidget);
expect(find.text('index 2 is visible ? false', skipOffstage: false), findsOneWidget);
await tester.pumpWidget(buildStack(itemCount: 3, selectedIndex: 2));
expect(find.text('index 0 is visible ? false', skipOffstage: false), findsOneWidget);
expect(find.text('index 1 is visible ? false', skipOffstage: false), findsOneWidget);
expect(find.text('index 2 is visible ? true', skipOffstage: false), findsOneWidget);
});
testWidgets('Can set width and height', (WidgetTester tester) async { testWidgets('Can set width and height', (WidgetTester tester) async {
const Key key = Key('container'); const Key key = Key('container');
...@@ -866,3 +901,14 @@ void main() { ...@@ -866,3 +901,14 @@ void main() {
]); ]);
}); });
} }
class _ShowVisibility extends StatelessWidget {
const _ShowVisibility({required this.index});
final int index;
@override
Widget build(BuildContext context) {
return Text('index $index is visible ? ${Visibility.of(context)}');
}
}
...@@ -471,4 +471,108 @@ void main() { ...@@ -471,4 +471,108 @@ void main() {
expect(tester.layers, isNot(contains(isA<OpacityLayer>()))); expect(tester.layers, isNot(contains(isA<OpacityLayer>())));
expect(tester.layers.last, isA<PictureLayer>()); expect(tester.layers.last, isA<PictureLayer>());
}); });
testWidgets('Visibility.of returns correct value', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: _ShowVisibility(),
),
);
expect(find.text('is visible ? true', skipOffstage: false), findsOneWidget);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Visibility(
maintainState: true,
child: _ShowVisibility(),
),
),
);
expect(find.text('is visible ? true', skipOffstage: false), findsOneWidget);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Visibility(
visible: false,
maintainState: true,
child: _ShowVisibility(),
),
),
);
expect(find.text('is visible ? false', skipOffstage: false), findsOneWidget);
});
testWidgets('Visibility.of works when multiple Visibility widgets are in hierarchy', (WidgetTester tester) async {
bool didChangeDependencies = false;
void handleDidChangeDependencies() {
didChangeDependencies = true;
}
Widget newWidget({required bool ancestorIsVisible, required bool descendantIsVisible}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Visibility(
visible: ancestorIsVisible,
maintainState: true,
child: Center(
child: Visibility(
visible: descendantIsVisible,
maintainState: true,
child: _ShowVisibility(
onDidChangeDependencies: handleDidChangeDependencies,
),
),
),
),
);
}
await tester.pumpWidget(newWidget(ancestorIsVisible: true, descendantIsVisible: true));
expect(didChangeDependencies, isTrue);
expect(find.text('is visible ? true', skipOffstage: false), findsOneWidget);
didChangeDependencies = false;
await tester.pumpWidget(newWidget(ancestorIsVisible: true, descendantIsVisible: false));
expect(didChangeDependencies, isTrue);
expect(find.text('is visible ? false', skipOffstage: false), findsOneWidget);
didChangeDependencies = false;
await tester.pumpWidget(newWidget(ancestorIsVisible: true, descendantIsVisible: false));
expect(didChangeDependencies, isFalse);
await tester.pumpWidget(newWidget(ancestorIsVisible: false, descendantIsVisible: false));
expect(didChangeDependencies, isTrue);
didChangeDependencies = false;
await tester.pumpWidget(newWidget(ancestorIsVisible: false, descendantIsVisible: true));
expect(didChangeDependencies, isTrue);
expect(find.text('is visible ? false', skipOffstage: false), findsOneWidget);
});
}
class _ShowVisibility extends StatefulWidget {
const _ShowVisibility({this.onDidChangeDependencies});
final VoidCallback? onDidChangeDependencies;
@override
State<_ShowVisibility> createState() => _ShowVisibilityState();
}
class _ShowVisibilityState extends State<_ShowVisibility> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (widget.onDidChangeDependencies != null) {
widget.onDidChangeDependencies!();
}
}
@override
Widget build(BuildContext context) {
return Text('is visible ? ${Visibility.of(context)}');
}
} }
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