Unverified Commit e7266dbb authored by YeungKC's avatar YeungKC Committed by GitHub

Let InkWell/Ink/ancestor support GlobalKey so that splash does not stop when...

Let InkWell/Ink/ancestor support GlobalKey so that splash does not stop when changing position. (#71138)
parent e384ca79
......@@ -251,9 +251,21 @@ class _InkState extends State<Ink> {
@override
void deactivate() {
_ink?.visible = false;
super.deactivate();
}
@override
void reactivate() {
_ink?.visible = true;
super.reactivate();
}
@override
void dispose() {
_ink?.dispose();
assert(_ink == null);
super.deactivate();
super.dispose();
}
Widget _build(BuildContext context) {
......
......@@ -733,6 +733,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
implements _ParentInkResponseState {
Set<InteractiveInkFeature>? _splashes;
InteractiveInkFeature? _currentSplash;
bool _active = true;
bool _hovering = false;
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
......@@ -779,7 +780,16 @@ class _InkResponseState extends State<_InkResponseStateWidget>
@override
void didUpdateWidget(_InkResponseStateWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
final InkFeature? validInkFeature = _getSingleInkFeature();
if (validInkFeature != null && !identical(validInkFeature.controller, Material.of(context)!)) {
_removeAllFeatures();
if (_hovering && enabled)
updateHighlight(_HighlightType.hover, value: _hovering, callOnHover: false);
_updateFocusHighlights();
return;
}
if (enabled != _isWidgetEnabled(oldWidget)) {
if (enabled) {
// Don't call wigdet.onHover because many wigets, including the button
// widgets, apply setState to an ancestor context from onHover.
......@@ -789,12 +799,47 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
}
InkFeature? _getSingleInkFeature() {
final List<InkFeature?> inkFeatures = <InkFeature?>[...?_splashes, ..._highlights.values];
assert(() {
MaterialInkController? lastController;
for (final InkFeature? inkFeature in inkFeatures) {
if (inkFeature == null)
continue;
final MaterialInkController controller = inkFeature.controller;
if (lastController != null && !identical(controller, lastController))
return false;
lastController = controller;
}
return true;
}());
final InkFeature? validInkFeature = inkFeatures.firstWhere((InkFeature? inkFeature) => inkFeature != null, orElse: () => null);
return validInkFeature;
}
@override
void dispose() {
_removeAllFeatures();
FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange);
super.dispose();
}
void _removeAllFeatures() {
if (_splashes != null) {
final Set<InteractiveInkFeature> splashes = _splashes!;
_splashes = null;
for (final InteractiveInkFeature splash in splashes)
splash.dispose();
_currentSplash = null;
}
assert(_currentSplash == null);
for (final _HighlightType highlight in _highlights.keys) {
_highlights[highlight]?.dispose();
_highlights[highlight] = null;
}
widget.parentState?.markChildInkResponsePressed(this, false);
}
@override
bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes!.isNotEmpty);
......@@ -830,6 +875,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
void handleInkRemoval() {
assert(_highlights[type] != null);
_highlights[type] = null;
if (_active)
updateKeepAlive();
}
......@@ -1013,24 +1059,26 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
}
void _setAllFeaturesVisible(bool visible) {
for (final InkFeature? splash in <InkFeature?>[...?_splashes, ..._highlights.values])
splash?.visible = visible;
}
@override
void deactivate() {
if (_splashes != null) {
final Set<InteractiveInkFeature> splashes = _splashes!;
_splashes = null;
for (final InteractiveInkFeature splash in splashes)
splash.dispose();
_currentSplash = null;
}
assert(_currentSplash == null);
for (final _HighlightType highlight in _highlights.keys) {
_highlights[highlight]?.dispose();
_highlights[highlight] = null;
}
widget.parentState?.markChildInkResponsePressed(this, false);
_active = !_active;
_setAllFeaturesVisible(false);
super.deactivate();
}
@override
void reactivate() {
_active = !_active;
_setAllFeaturesVisible(true);
updateKeepAlive();
super.reactivate();
}
bool _isWidgetEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
}
......@@ -1201,6 +1249,10 @@ class _InkResponseState extends State<_InkResponseStateWidget>
/// during animation. You should avoid using InkWells within [Material] widgets
/// that are changing size.
///
/// Animations triggered by an [InkWell] will survive their widget moving due
/// to [GlobalKey] reparenting, as long as the nearest [Material] ancestor is
/// the same before and after the move.
///
/// See also:
///
/// * [GestureDetector], for listening for gestures without ink splashes.
......
......@@ -544,12 +544,20 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
canvas.save();
canvas.translate(offset.dx, offset.dy);
canvas.clipRect(Offset.zero & size);
for (final InkFeature inkFeature in _inkFeatures!)
for (final InkFeature inkFeature in _inkFeatures!) {
if (inkFeature.visible)
inkFeature._paint(canvas);
}
canvas.restore();
}
super.paint(context, offset);
}
void dispose() {
// [InkFeature.dispose] will eventually call [_inkFeatures!.remove].
while (_inkFeatures?.isNotEmpty == true)
_inkFeatures!.first.dispose();
}
}
class _InkFeatures extends SingleChildRenderObjectWidget {
......@@ -585,6 +593,11 @@ class _InkFeatures extends SingleChildRenderObjectWidget {
..absorbHitTest = absorbHitTest;
assert(vsync == renderObject.vsync);
}
@override
void didUnmountRenderObject(_RenderInkFeatures renderObject) {
renderObject.dispose();
}
}
/// A visual reaction on a piece of [Material].
......@@ -617,6 +630,15 @@ abstract class InkFeature {
bool _debugDisposed = false;
/// Whether or not visual reaction is activated.
///
/// Change this field will affect whether this InkFeature is render in next
/// frame.
///
/// For this InkFeature to render properly, it should usually be change in
/// [State.deactivate] and [State.reactivate].
bool visible = true;
/// Free up the resources associated with this ink feature.
@mustCallSuper
void dispose() {
......
......@@ -584,7 +584,6 @@ class _MergeableMaterialState extends State<MergeableMaterial> with TickerProvid
}
child = AnimatedContainer(
key: _MergeableMaterialSliceKey(_children[i].key),
decoration: BoxDecoration(border: border),
duration: kThemeAnimationDuration,
curve: Curves.fastOutSlowIn,
......@@ -600,6 +599,7 @@ class _MergeableMaterialState extends State<MergeableMaterial> with TickerProvid
shape: BoxShape.rectangle,
),
child: Material(
key: _MergeableMaterialSliceKey(_children[i].key),
type: MaterialType.transparency,
child: child,
),
......
......@@ -930,6 +930,12 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
/// It is an error to call [setState] unless [mounted] is true.
bool get mounted => _element != null;
/// This field is used tracks [reactivate] and [deactivate], to assert that
/// they are called alternatively.
///
/// This field is not set in release mode.
bool _debugActive = true;
/// Called when this object is inserted into the tree.
///
/// The framework will call this method exactly once for each [State] object
......@@ -1110,17 +1116,17 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
_element!.markNeedsBuild();
}
/// Called when this object is removed from the tree.
/// Whenever the framework removes this [State] object from the tree, the
/// framework will call this method.
///
/// The framework calls this method whenever it removes this [State] object
/// from the tree. In some cases, the framework will reinsert the [State]
/// object into another part of the tree (e.g., if the subtree containing this
/// [State] object is grafted from one location in the tree to another). If
/// that happens, the framework will ensure that it calls [build] to give the
/// [State] object a chance to adapt to its new location in the tree. If
/// the framework does reinsert this subtree, it will do so before the end of
/// the animation frame in which the subtree was removed from the tree. For
/// this reason, [State] objects can defer releasing most resources until the
/// In some cases, the framework will reinsert the [State] object into
/// another part of the tree (e.g., if the subtree containing this [State]
/// object is grafted from one location in the tree to another). If that
/// happens, the framework will ensure that it calls [reactivate] to give the
/// [State] object a chance to adapt to its new location in the tree. If the
/// framework does reinsert this subtree, it will do so before the end of the
/// animation frame in which the subtree was removed from the tree. For this
/// reason, [State] objects can defer releasing most resources until the
/// framework calls their [dispose] method.
///
/// Subclasses should override this method to clean up any links between
......@@ -1136,7 +1142,35 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
/// from the tree permanently.
@protected
@mustCallSuper
void deactivate() { }
void deactivate() {
assert(() {
_debugActive = !_debugActive;
return !_debugActive;
}());
}
/// Called when this object is reactivated.
///
/// If the [widget] or one of its ancestors has a [GlobalKey], the framework
/// will mark this object as inactive when it is removed and call
/// [deactivate].
///
/// If the object is reinserted to the tree in the next frame (e.g. by
/// changing position), it will be marked as active again and this method will be
/// called.
///
/// See also:
///
/// * [Element.activate] and [Element.deactivate] for more information about
/// lifecycle.
@protected
@mustCallSuper
void reactivate() {
assert(() {
_debugActive = !_debugActive;
return _debugActive;
}());
}
/// Called when this object is removed from the tree permanently.
///
......@@ -4777,6 +4811,7 @@ class StatefulElement extends ComponentElement {
@override
void activate() {
super.activate();
state.reactivate();
// Since the State could have observed the deactivate() and thus disposed of
// resources allocated in the build method, we have to rebuild the widget
// so that its State can reallocate its resources.
......
......@@ -431,4 +431,67 @@ void main() {
throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
}));
});
// Regression test for https://github.com/flutter/flutter/issues/6751
testWidgets('When Ink has a GlobalKey and changes position, splash should not stop', (WidgetTester tester) async {
const Color color = Colors.blue;
const Color splashColor = Colors.green;
void expectTest(bool painted) {
final PaintPattern paintPattern = paints
..rect(color: Color(color.value))
..circle(color: Color(splashColor.value));
expect(
Material.of(tester.element(find.byType(InkWell)))! as RenderBox,
painted ? paintPattern : isNot(paintPattern),
);
}
bool wrap = false;
final Key globalKey = GlobalKey();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
Widget child = Ink(
key: globalKey,
color: color,
width: 200.0,
height: 200.0,
child: InkWell(
splashColor: splashColor,
onTap: () { },
onTapDown: (_) async {
await Future<void>.delayed(const Duration(milliseconds: 50));
setState(() {
wrap = !wrap;
});
}
),
);
if (wrap) {
child = Container(
margin: const EdgeInsets.only(top: 320),
child: child,
);
}
return child;
}
),
),
),
));
final TestGesture testGesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center);
await tester.pump(const Duration(milliseconds: 60));
expectTest(true);
await testGesture.up();
await tester.pumpAndSettle();
expectTest(false);
});
}
......@@ -1331,24 +1331,28 @@ void main() {
expect(key.currentState, isNotNull);
expect(state.didChangeDependenciesCount, 1);
expect(state.deactivatedCount, 0);
expect(state.reactivatedCount, 0);
/// Rebuild with updated value - should call didChangeDependencies
await tester.pumpWidget(Inherited(2, child: DependentStatefulWidget(key: key)));
expect(key.currentState, isNotNull);
expect(state.didChangeDependenciesCount, 2);
expect(state.deactivatedCount, 0);
expect(state.reactivatedCount, 0);
// reparent it - should call deactivate and didChangeDependencies
// reparent it - should call deactivate, reactivate, didChangeDependencies
await tester.pumpWidget(Inherited(3, child: SizedBox(child: DependentStatefulWidget(key: key))));
expect(key.currentState, isNotNull);
expect(state.didChangeDependenciesCount, 3);
expect(state.deactivatedCount, 1);
expect(state.reactivatedCount, 1);
// Remove it - should call deactivate, but not didChangeDependencies
// Remove it - should call deactivate, but not reactivate or didChangeDependencies
await tester.pumpWidget(const Inherited(4, child: SizedBox()));
expect(key.currentState, isNull);
expect(state.didChangeDependenciesCount, 3);
expect(state.deactivatedCount, 2);
expect(state.reactivatedCount, 1);
});
testWidgets('StatefulElement subclass can decorate State.build', (WidgetTester tester) async {
......@@ -1391,17 +1395,21 @@ void main() {
expect(debugDoingBuildOnBuild, isTrue);
});
testWidgets('StatefulWidget', (WidgetTester tester) async {
final Key key = GlobalKey();
late bool debugDoingBuildOnBuild;
late bool debugDoingBuildOnInitState;
late bool debugDoingBuildOnDidChangeDependencies;
late bool debugDoingBuildOnDidUpdateWidget;
bool? debugDoingBuildOnDispose;
bool? debugDoingBuildOnDeactivate;
bool? debugDoingBuildOnReactivate;
await tester.pumpWidget(
Inherited(
0,
child: StatefulWidgetSpy(
key: key,
onInitState: (BuildContext context) {
debugDoingBuildOnInitState = context.debugDoingBuild;
},
......@@ -1427,6 +1435,7 @@ void main() {
Inherited(
1,
child: StatefulWidgetSpy(
key: key,
onDidUpdateWidget: (BuildContext context) {
debugDoingBuildOnDidUpdateWidget = context.debugDoingBuild;
},
......@@ -1442,6 +1451,9 @@ void main() {
onDeactivate: (BuildContext context) {
debugDoingBuildOnDeactivate = context.debugDoingBuild;
},
onReactivate: (BuildContext context) {
debugDoingBuildOnReactivate = context.debugDoingBuild;
},
),
),
);
......@@ -1451,6 +1463,35 @@ void main() {
expect(debugDoingBuildOnDidUpdateWidget, isFalse);
expect(debugDoingBuildOnDidChangeDependencies, isFalse);
expect(debugDoingBuildOnDeactivate, isNull);
expect(debugDoingBuildOnReactivate, isNull);
expect(debugDoingBuildOnDispose, isNull);
await tester.pumpWidget(
Inherited(
1,
child: SizedBox(
child: StatefulWidgetSpy(
key: key,
onBuild: (BuildContext context) {
debugDoingBuildOnBuild = context.debugDoingBuild;
},
onDispose: (BuildContext context) {
debugDoingBuildOnDispose = context.debugDoingBuild;
},
onDeactivate: (BuildContext context) {
debugDoingBuildOnDeactivate = context.debugDoingBuild;
},
onReactivate: (BuildContext context) {
debugDoingBuildOnReactivate = context.debugDoingBuild;
},
),
),
),
);
expect(debugDoingBuildOnBuild, isTrue);
expect(debugDoingBuildOnDeactivate, isFalse);
expect(debugDoingBuildOnReactivate, isFalse);
expect(debugDoingBuildOnDispose, isNull);
await tester.pumpWidget(Container());
......@@ -1705,6 +1746,7 @@ class DependentStatefulWidget extends StatefulWidget {
class DependentState extends State<DependentStatefulWidget> {
int didChangeDependenciesCount = 0;
int deactivatedCount = 0;
int reactivatedCount = 0;
@override
void didChangeDependencies() {
......@@ -1723,6 +1765,12 @@ class DependentState extends State<DependentStatefulWidget> {
super.deactivate();
deactivatedCount += 1;
}
@override
void reactivate() {
super.reactivate();
reactivatedCount += 1;
}
}
class SwapKeyWidget extends StatefulWidget {
......@@ -1810,6 +1858,7 @@ class StatefulWidgetSpy extends StatefulWidget {
this.onDidChangeDependencies,
this.onDispose,
this.onDeactivate,
this.onReactivate,
this.onDidUpdateWidget,
}) : super(key: key);
......@@ -1818,6 +1867,7 @@ class StatefulWidgetSpy extends StatefulWidget {
final void Function(BuildContext)? onDidChangeDependencies;
final void Function(BuildContext)? onDispose;
final void Function(BuildContext)? onDeactivate;
final void Function(BuildContext)? onReactivate;
final void Function(BuildContext)? onDidUpdateWidget;
@override
......@@ -1837,6 +1887,12 @@ class _StatefulWidgetSpyState extends State<StatefulWidgetSpy> {
widget.onDeactivate?.call(context);
}
@override
void reactivate() {
super.reactivate();
widget.onReactivate?.call(context);
}
@override
void dispose() {
super.dispose();
......
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