Let InkWell/Ink/ancestor support GlobalKey so that splash does not stop when changing position. (#71138)
This commit is contained in:
parent
e384ca7979
commit
e7266dbb07
@ -251,9 +251,21 @@ class _InkState extends State<Ink> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void deactivate() {
|
void deactivate() {
|
||||||
|
_ink?.visible = false;
|
||||||
|
super.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reactivate() {
|
||||||
|
_ink?.visible = true;
|
||||||
|
super.reactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
_ink?.dispose();
|
_ink?.dispose();
|
||||||
assert(_ink == null);
|
assert(_ink == null);
|
||||||
super.deactivate();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _build(BuildContext context) {
|
Widget _build(BuildContext context) {
|
||||||
|
@ -733,6 +733,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
|||||||
implements _ParentInkResponseState {
|
implements _ParentInkResponseState {
|
||||||
Set<InteractiveInkFeature>? _splashes;
|
Set<InteractiveInkFeature>? _splashes;
|
||||||
InteractiveInkFeature? _currentSplash;
|
InteractiveInkFeature? _currentSplash;
|
||||||
|
bool _active = true;
|
||||||
bool _hovering = false;
|
bool _hovering = false;
|
||||||
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
|
final Map<_HighlightType, InkHighlight?> _highlights = <_HighlightType, InkHighlight?>{};
|
||||||
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
|
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
|
||||||
@ -779,7 +780,16 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
|||||||
@override
|
@override
|
||||||
void didUpdateWidget(_InkResponseStateWidget oldWidget) {
|
void didUpdateWidget(_InkResponseStateWidget oldWidget) {
|
||||||
super.didUpdateWidget(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) {
|
if (enabled) {
|
||||||
// Don't call wigdet.onHover because many wigets, including the button
|
// Don't call wigdet.onHover because many wigets, including the button
|
||||||
// widgets, apply setState to an ancestor context from onHover.
|
// 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_removeAllFeatures();
|
||||||
FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange);
|
FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange);
|
||||||
super.dispose();
|
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
|
@override
|
||||||
bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes!.isNotEmpty);
|
bool get wantKeepAlive => highlightsExist || (_splashes != null && _splashes!.isNotEmpty);
|
||||||
|
|
||||||
@ -830,7 +875,8 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
|||||||
void handleInkRemoval() {
|
void handleInkRemoval() {
|
||||||
assert(_highlights[type] != null);
|
assert(_highlights[type] != null);
|
||||||
_highlights[type] = null;
|
_highlights[type] = null;
|
||||||
updateKeepAlive();
|
if (_active)
|
||||||
|
updateKeepAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == _HighlightType.pressed) {
|
if (type == _HighlightType.pressed) {
|
||||||
@ -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
|
@override
|
||||||
void deactivate() {
|
void deactivate() {
|
||||||
if (_splashes != null) {
|
_active = !_active;
|
||||||
final Set<InteractiveInkFeature> splashes = _splashes!;
|
_setAllFeaturesVisible(false);
|
||||||
_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);
|
|
||||||
super.deactivate();
|
super.deactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reactivate() {
|
||||||
|
_active = !_active;
|
||||||
|
_setAllFeaturesVisible(true);
|
||||||
|
updateKeepAlive();
|
||||||
|
super.reactivate();
|
||||||
|
}
|
||||||
|
|
||||||
bool _isWidgetEnabled(_InkResponseStateWidget widget) {
|
bool _isWidgetEnabled(_InkResponseStateWidget widget) {
|
||||||
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
|
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
|
/// during animation. You should avoid using InkWells within [Material] widgets
|
||||||
/// that are changing size.
|
/// 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:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [GestureDetector], for listening for gestures without ink splashes.
|
/// * [GestureDetector], for listening for gestures without ink splashes.
|
||||||
|
@ -544,12 +544,20 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
|
|||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate(offset.dx, offset.dy);
|
canvas.translate(offset.dx, offset.dy);
|
||||||
canvas.clipRect(Offset.zero & size);
|
canvas.clipRect(Offset.zero & size);
|
||||||
for (final InkFeature inkFeature in _inkFeatures!)
|
for (final InkFeature inkFeature in _inkFeatures!) {
|
||||||
inkFeature._paint(canvas);
|
if (inkFeature.visible)
|
||||||
|
inkFeature._paint(canvas);
|
||||||
|
}
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
super.paint(context, offset);
|
super.paint(context, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
// [InkFeature.dispose] will eventually call [_inkFeatures!.remove].
|
||||||
|
while (_inkFeatures?.isNotEmpty == true)
|
||||||
|
_inkFeatures!.first.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InkFeatures extends SingleChildRenderObjectWidget {
|
class _InkFeatures extends SingleChildRenderObjectWidget {
|
||||||
@ -585,6 +593,11 @@ class _InkFeatures extends SingleChildRenderObjectWidget {
|
|||||||
..absorbHitTest = absorbHitTest;
|
..absorbHitTest = absorbHitTest;
|
||||||
assert(vsync == renderObject.vsync);
|
assert(vsync == renderObject.vsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUnmountRenderObject(_RenderInkFeatures renderObject) {
|
||||||
|
renderObject.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A visual reaction on a piece of [Material].
|
/// A visual reaction on a piece of [Material].
|
||||||
@ -617,6 +630,15 @@ abstract class InkFeature {
|
|||||||
|
|
||||||
bool _debugDisposed = false;
|
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.
|
/// Free up the resources associated with this ink feature.
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -584,7 +584,6 @@ class _MergeableMaterialState extends State<MergeableMaterial> with TickerProvid
|
|||||||
}
|
}
|
||||||
|
|
||||||
child = AnimatedContainer(
|
child = AnimatedContainer(
|
||||||
key: _MergeableMaterialSliceKey(_children[i].key),
|
|
||||||
decoration: BoxDecoration(border: border),
|
decoration: BoxDecoration(border: border),
|
||||||
duration: kThemeAnimationDuration,
|
duration: kThemeAnimationDuration,
|
||||||
curve: Curves.fastOutSlowIn,
|
curve: Curves.fastOutSlowIn,
|
||||||
@ -600,6 +599,7 @@ class _MergeableMaterialState extends State<MergeableMaterial> with TickerProvid
|
|||||||
shape: BoxShape.rectangle,
|
shape: BoxShape.rectangle,
|
||||||
),
|
),
|
||||||
child: Material(
|
child: Material(
|
||||||
|
key: _MergeableMaterialSliceKey(_children[i].key),
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: child,
|
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.
|
/// It is an error to call [setState] unless [mounted] is true.
|
||||||
bool get mounted => _element != null;
|
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.
|
/// Called when this object is inserted into the tree.
|
||||||
///
|
///
|
||||||
/// The framework will call this method exactly once for each [State] object
|
/// 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();
|
_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
|
/// In some cases, the framework will reinsert the [State] object into
|
||||||
/// from the tree. In some cases, the framework will reinsert the [State]
|
/// another part of the tree (e.g., if the subtree containing this [State]
|
||||||
/// object into another part of the tree (e.g., if the subtree containing this
|
/// object is grafted from one location in the tree to another). If that
|
||||||
/// [State] object is grafted from one location in the tree to another). If
|
/// happens, the framework will ensure that it calls [reactivate] to give the
|
||||||
/// 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
|
||||||
/// [State] object a chance to adapt to its new location in the tree. If
|
/// framework does reinsert this subtree, it will do so before the end of the
|
||||||
/// the framework does reinsert this subtree, it will do so before the end of
|
/// animation frame in which the subtree was removed from the tree. For this
|
||||||
/// the animation frame in which the subtree was removed from the tree. For
|
/// reason, [State] objects can defer releasing most resources until the
|
||||||
/// this reason, [State] objects can defer releasing most resources until the
|
|
||||||
/// framework calls their [dispose] method.
|
/// framework calls their [dispose] method.
|
||||||
///
|
///
|
||||||
/// Subclasses should override this method to clean up any links between
|
/// 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.
|
/// from the tree permanently.
|
||||||
@protected
|
@protected
|
||||||
@mustCallSuper
|
@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.
|
/// Called when this object is removed from the tree permanently.
|
||||||
///
|
///
|
||||||
@ -4777,6 +4811,7 @@ class StatefulElement extends ComponentElement {
|
|||||||
@override
|
@override
|
||||||
void activate() {
|
void activate() {
|
||||||
super.activate();
|
super.activate();
|
||||||
|
state.reactivate();
|
||||||
// Since the State could have observed the deactivate() and thus disposed of
|
// Since the State could have observed the deactivate() and thus disposed of
|
||||||
// resources allocated in the build method, we have to rebuild the widget
|
// resources allocated in the build method, we have to rebuild the widget
|
||||||
// so that its State can reallocate its resources.
|
// so that its State can reallocate its resources.
|
||||||
|
@ -431,4 +431,67 @@ void main() {
|
|||||||
throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1055,118 +1055,6 @@ void main() {
|
|||||||
await gesture3.up();
|
await gesture3.up();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('When ink wells are reparented, the old parent can display splash while the new parent can not', (WidgetTester tester) async {
|
|
||||||
final GlobalKey innerKey = GlobalKey();
|
|
||||||
final GlobalKey leftKey = GlobalKey();
|
|
||||||
final GlobalKey rightKey = GlobalKey();
|
|
||||||
|
|
||||||
Widget doubleInkWellRow({
|
|
||||||
required double leftWidth,
|
|
||||||
required double rightWidth,
|
|
||||||
Widget? leftChild,
|
|
||||||
Widget? rightChild,
|
|
||||||
}) {
|
|
||||||
return Material(
|
|
||||||
child: Directionality(
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: SizedBox(
|
|
||||||
width: leftWidth+rightWidth,
|
|
||||||
height: 100,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
SizedBox(
|
|
||||||
width: leftWidth,
|
|
||||||
height: 100,
|
|
||||||
child: InkWell(
|
|
||||||
key: leftKey,
|
|
||||||
onTap: () {},
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: leftWidth,
|
|
||||||
height: 50,
|
|
||||||
child: leftChild,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: rightWidth,
|
|
||||||
height: 100,
|
|
||||||
child: InkWell(
|
|
||||||
key: rightKey,
|
|
||||||
onTap: () {},
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: leftWidth,
|
|
||||||
height: 50,
|
|
||||||
child: rightChild,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
doubleInkWellRow(
|
|
||||||
leftWidth: 110,
|
|
||||||
rightWidth: 90,
|
|
||||||
leftChild: InkWell(
|
|
||||||
key: innerKey,
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final MaterialInkController material = Material.of(tester.element(find.byKey(innerKey)))!;
|
|
||||||
|
|
||||||
// Press inner
|
|
||||||
final TestGesture gesture = await tester.startGesture(const Offset(100, 50), pointer: 1);
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
|
|
||||||
|
|
||||||
// Switch side
|
|
||||||
await tester.pumpWidget(
|
|
||||||
doubleInkWellRow(
|
|
||||||
leftWidth: 90,
|
|
||||||
rightWidth: 110,
|
|
||||||
rightChild: InkWell(
|
|
||||||
key: innerKey,
|
|
||||||
onTap: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(material, paintsExactlyCountTimes(#drawCircle, 0));
|
|
||||||
|
|
||||||
// A second pointer presses inner
|
|
||||||
final TestGesture gesture2 = await tester.startGesture(const Offset(100, 50), pointer: 2);
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
|
|
||||||
|
|
||||||
await gesture.up();
|
|
||||||
await gesture2.up();
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Press inner
|
|
||||||
await gesture.down(const Offset(100, 50));
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
|
|
||||||
|
|
||||||
// Press left
|
|
||||||
await gesture2.down(const Offset(50, 50));
|
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
|
||||||
expect(material, paintsExactlyCountTimes(#drawCircle, 2));
|
|
||||||
|
|
||||||
await gesture.up();
|
|
||||||
await gesture2.up();
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets("Ink wells's splash starts before tap is confirmed and disappear after tap is canceled", (WidgetTester tester) async {
|
testWidgets("Ink wells's splash starts before tap is confirmed and disappear after tap is canceled", (WidgetTester tester) async {
|
||||||
final GlobalKey innerKey = GlobalKey();
|
final GlobalKey innerKey = GlobalKey();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -1363,4 +1251,231 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/6751
|
||||||
|
testWidgets('When InkWell has a GlobalKey and changes position, splash should not stop', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<_TestAppState> testAppKey = GlobalKey();
|
||||||
|
int frames;
|
||||||
|
|
||||||
|
await tester.pumpWidget(TestApp(key: testAppKey));
|
||||||
|
|
||||||
|
void expectPaintedCircle(bool painted) {
|
||||||
|
final PaintPattern paintPattern = paints..circle();
|
||||||
|
expect(
|
||||||
|
Material.of(tester.element(find.byType(InkWell)))! as RenderBox,
|
||||||
|
painted ? paintPattern : isNot(paintPattern),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Future<void> expectSplashContinueAfterMove(bool value) async {
|
||||||
|
await tester.pump();
|
||||||
|
expectPaintedCircle(true);
|
||||||
|
await tester.pump(const Duration(milliseconds: 10));
|
||||||
|
expectPaintedCircle(true);
|
||||||
|
await tester.pump(const Duration(milliseconds: 40));
|
||||||
|
expectPaintedCircle(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// InkWell does not have any key, so splash will stop.
|
||||||
|
testAppKey.currentState!.switchTapChangeWrap();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byType(InkWell));
|
||||||
|
await expectSplashContinueAfterMove(false);
|
||||||
|
frames = await tester.pumpAndSettle();
|
||||||
|
expect(frames, 1);
|
||||||
|
expectPaintedCircle(false);
|
||||||
|
|
||||||
|
// InkWell has a ValueKey, so splash will also stop.
|
||||||
|
testAppKey.currentState!.setInkWellKey(const Key('foo'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byType(InkWell));
|
||||||
|
await expectSplashContinueAfterMove(false);
|
||||||
|
frames = await tester.pumpAndSettle();
|
||||||
|
expect(frames, 1);
|
||||||
|
expectPaintedCircle(false);
|
||||||
|
|
||||||
|
// InkWell has a GlobalKey, so splash will continue.
|
||||||
|
testAppKey.currentState!.setInkWellKey(GlobalKey());
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.byType(InkWell));
|
||||||
|
await expectSplashContinueAfterMove(true);
|
||||||
|
frames = await tester.pumpAndSettle();
|
||||||
|
expect(frames > 1, isTrue);
|
||||||
|
expectPaintedCircle(false);
|
||||||
|
|
||||||
|
testAppKey.currentState!.switchTapDownChangeWrap();
|
||||||
|
await tester.pump();
|
||||||
|
final TestGesture testGesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center);
|
||||||
|
await expectSplashContinueAfterMove(true);
|
||||||
|
await testGesture.up();
|
||||||
|
frames = await tester.pumpAndSettle();
|
||||||
|
expect(frames > 1, isTrue);
|
||||||
|
expectPaintedCircle(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('When InkWell/Ancestor has a GlobalKey and ancestor Material is replaced, splash should stop.', (WidgetTester tester) async {
|
||||||
|
final Key key = GlobalKey();
|
||||||
|
bool replaced = false;
|
||||||
|
|
||||||
|
await tester.pumpWidget(Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: StatefulBuilder(builder: (BuildContext context, void Function(void Function()) setState) {
|
||||||
|
Future<void> changeReplace() async {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
setState(() {
|
||||||
|
replaced = !replaced;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Material(
|
||||||
|
key: ValueKey<bool>(replaced),
|
||||||
|
child: InkWell(
|
||||||
|
key: key,
|
||||||
|
onTap: () {
|
||||||
|
changeReplace();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.byType(InkWell));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 60));
|
||||||
|
|
||||||
|
final PaintPattern paintPattern = paints..circle();
|
||||||
|
expect(
|
||||||
|
Material.of(tester.element(find.byType(InkWell)))! as RenderBox,
|
||||||
|
isNot(paintPattern),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('When InkWell/Ancestor has a GlobalKey and ancestor Material is replaced, highlight should always be maintained.', (WidgetTester tester) async {
|
||||||
|
const Color hoverColor = Color(0xff00ff00);
|
||||||
|
final Key key = GlobalKey();
|
||||||
|
int onHoverCount = 0;
|
||||||
|
int callChangeReplaceCount = 0;
|
||||||
|
bool replaced = false;
|
||||||
|
int frames;
|
||||||
|
|
||||||
|
await tester.pumpWidget(Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: StatefulBuilder(builder: (BuildContext context, void Function(void Function()) setState) {
|
||||||
|
Future<void> changeReplace() async {
|
||||||
|
callChangeReplaceCount += 1;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
setState(() {
|
||||||
|
replaced = !replaced;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
margin: replaced ? const EdgeInsets.only(top: 1) : EdgeInsets.zero,
|
||||||
|
child: Material(
|
||||||
|
key: ValueKey<bool>(replaced),
|
||||||
|
child: InkWell(
|
||||||
|
key: key,
|
||||||
|
hoverColor: hoverColor,
|
||||||
|
onTap: () {},
|
||||||
|
onHover: (bool hovered) {
|
||||||
|
onHoverCount += 1;
|
||||||
|
if (hovered)
|
||||||
|
changeReplace();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
await gesture.addPointer();
|
||||||
|
addTearDown(gesture.removePointer);
|
||||||
|
// When replaced is true, InkWell point at the top left is 1.
|
||||||
|
await gesture.moveTo(Offset.zero);
|
||||||
|
frames = await tester.pumpAndSettle();
|
||||||
|
expect(frames > 1, isTrue);
|
||||||
|
RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||||
|
expect(inkFeatures, isNot(paints..rect(color: hoverColor)));
|
||||||
|
expect(onHoverCount, 2);
|
||||||
|
expect(callChangeReplaceCount, 1);
|
||||||
|
expect(replaced, true);
|
||||||
|
|
||||||
|
await gesture.moveTo(tester.getCenter(find.byType(InkWell)));
|
||||||
|
frames = await tester.pumpAndSettle();
|
||||||
|
expect(frames > 1, isTrue);
|
||||||
|
inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||||
|
expect(inkFeatures, paints..rect(color: hoverColor));
|
||||||
|
expect(onHoverCount, 3);
|
||||||
|
expect(callChangeReplaceCount, 2);
|
||||||
|
expect(replaced, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestApp extends StatefulWidget {
|
||||||
|
const TestApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TestAppState createState() => _TestAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestAppState extends State<TestApp> {
|
||||||
|
bool wrap = false;
|
||||||
|
bool tapDownChangeWrap = false;
|
||||||
|
bool tapChangeWrap = false;
|
||||||
|
Key? inkWellKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget child = InkWell(
|
||||||
|
key: inkWellKey,
|
||||||
|
onTap: () async {
|
||||||
|
if (tapChangeWrap) {
|
||||||
|
await changeWrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTapDown: (_) async {
|
||||||
|
if (tapDownChangeWrap) {
|
||||||
|
await changeWrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wrap) {
|
||||||
|
child = Container(
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Material(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> changeWrap() async {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||||
|
setState(() {
|
||||||
|
wrap = !wrap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInkWellKey(Key key) {
|
||||||
|
setState(() {
|
||||||
|
inkWellKey = key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void switchTapDownChangeWrap() {
|
||||||
|
setState(() {
|
||||||
|
tapDownChangeWrap = true;
|
||||||
|
tapChangeWrap = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void switchTapChangeWrap() {
|
||||||
|
setState(() {
|
||||||
|
tapChangeWrap = true;
|
||||||
|
tapDownChangeWrap = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1331,24 +1331,28 @@ void main() {
|
|||||||
expect(key.currentState, isNotNull);
|
expect(key.currentState, isNotNull);
|
||||||
expect(state.didChangeDependenciesCount, 1);
|
expect(state.didChangeDependenciesCount, 1);
|
||||||
expect(state.deactivatedCount, 0);
|
expect(state.deactivatedCount, 0);
|
||||||
|
expect(state.reactivatedCount, 0);
|
||||||
|
|
||||||
/// Rebuild with updated value - should call didChangeDependencies
|
/// Rebuild with updated value - should call didChangeDependencies
|
||||||
await tester.pumpWidget(Inherited(2, child: DependentStatefulWidget(key: key)));
|
await tester.pumpWidget(Inherited(2, child: DependentStatefulWidget(key: key)));
|
||||||
expect(key.currentState, isNotNull);
|
expect(key.currentState, isNotNull);
|
||||||
expect(state.didChangeDependenciesCount, 2);
|
expect(state.didChangeDependenciesCount, 2);
|
||||||
expect(state.deactivatedCount, 0);
|
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))));
|
await tester.pumpWidget(Inherited(3, child: SizedBox(child: DependentStatefulWidget(key: key))));
|
||||||
expect(key.currentState, isNotNull);
|
expect(key.currentState, isNotNull);
|
||||||
expect(state.didChangeDependenciesCount, 3);
|
expect(state.didChangeDependenciesCount, 3);
|
||||||
expect(state.deactivatedCount, 1);
|
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()));
|
await tester.pumpWidget(const Inherited(4, child: SizedBox()));
|
||||||
expect(key.currentState, isNull);
|
expect(key.currentState, isNull);
|
||||||
expect(state.didChangeDependenciesCount, 3);
|
expect(state.didChangeDependenciesCount, 3);
|
||||||
expect(state.deactivatedCount, 2);
|
expect(state.deactivatedCount, 2);
|
||||||
|
expect(state.reactivatedCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('StatefulElement subclass can decorate State.build', (WidgetTester tester) async {
|
testWidgets('StatefulElement subclass can decorate State.build', (WidgetTester tester) async {
|
||||||
@ -1391,17 +1395,21 @@ void main() {
|
|||||||
expect(debugDoingBuildOnBuild, isTrue);
|
expect(debugDoingBuildOnBuild, isTrue);
|
||||||
});
|
});
|
||||||
testWidgets('StatefulWidget', (WidgetTester tester) async {
|
testWidgets('StatefulWidget', (WidgetTester tester) async {
|
||||||
|
final Key key = GlobalKey();
|
||||||
|
|
||||||
late bool debugDoingBuildOnBuild;
|
late bool debugDoingBuildOnBuild;
|
||||||
late bool debugDoingBuildOnInitState;
|
late bool debugDoingBuildOnInitState;
|
||||||
late bool debugDoingBuildOnDidChangeDependencies;
|
late bool debugDoingBuildOnDidChangeDependencies;
|
||||||
late bool debugDoingBuildOnDidUpdateWidget;
|
late bool debugDoingBuildOnDidUpdateWidget;
|
||||||
bool? debugDoingBuildOnDispose;
|
bool? debugDoingBuildOnDispose;
|
||||||
bool? debugDoingBuildOnDeactivate;
|
bool? debugDoingBuildOnDeactivate;
|
||||||
|
bool? debugDoingBuildOnReactivate;
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Inherited(
|
Inherited(
|
||||||
0,
|
0,
|
||||||
child: StatefulWidgetSpy(
|
child: StatefulWidgetSpy(
|
||||||
|
key: key,
|
||||||
onInitState: (BuildContext context) {
|
onInitState: (BuildContext context) {
|
||||||
debugDoingBuildOnInitState = context.debugDoingBuild;
|
debugDoingBuildOnInitState = context.debugDoingBuild;
|
||||||
},
|
},
|
||||||
@ -1427,6 +1435,7 @@ void main() {
|
|||||||
Inherited(
|
Inherited(
|
||||||
1,
|
1,
|
||||||
child: StatefulWidgetSpy(
|
child: StatefulWidgetSpy(
|
||||||
|
key: key,
|
||||||
onDidUpdateWidget: (BuildContext context) {
|
onDidUpdateWidget: (BuildContext context) {
|
||||||
debugDoingBuildOnDidUpdateWidget = context.debugDoingBuild;
|
debugDoingBuildOnDidUpdateWidget = context.debugDoingBuild;
|
||||||
},
|
},
|
||||||
@ -1442,6 +1451,9 @@ void main() {
|
|||||||
onDeactivate: (BuildContext context) {
|
onDeactivate: (BuildContext context) {
|
||||||
debugDoingBuildOnDeactivate = context.debugDoingBuild;
|
debugDoingBuildOnDeactivate = context.debugDoingBuild;
|
||||||
},
|
},
|
||||||
|
onReactivate: (BuildContext context) {
|
||||||
|
debugDoingBuildOnReactivate = context.debugDoingBuild;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1451,6 +1463,35 @@ void main() {
|
|||||||
expect(debugDoingBuildOnDidUpdateWidget, isFalse);
|
expect(debugDoingBuildOnDidUpdateWidget, isFalse);
|
||||||
expect(debugDoingBuildOnDidChangeDependencies, isFalse);
|
expect(debugDoingBuildOnDidChangeDependencies, isFalse);
|
||||||
expect(debugDoingBuildOnDeactivate, isNull);
|
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);
|
expect(debugDoingBuildOnDispose, isNull);
|
||||||
|
|
||||||
await tester.pumpWidget(Container());
|
await tester.pumpWidget(Container());
|
||||||
@ -1705,6 +1746,7 @@ class DependentStatefulWidget extends StatefulWidget {
|
|||||||
class DependentState extends State<DependentStatefulWidget> {
|
class DependentState extends State<DependentStatefulWidget> {
|
||||||
int didChangeDependenciesCount = 0;
|
int didChangeDependenciesCount = 0;
|
||||||
int deactivatedCount = 0;
|
int deactivatedCount = 0;
|
||||||
|
int reactivatedCount = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
@ -1723,6 +1765,12 @@ class DependentState extends State<DependentStatefulWidget> {
|
|||||||
super.deactivate();
|
super.deactivate();
|
||||||
deactivatedCount += 1;
|
deactivatedCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reactivate() {
|
||||||
|
super.reactivate();
|
||||||
|
reactivatedCount += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SwapKeyWidget extends StatefulWidget {
|
class SwapKeyWidget extends StatefulWidget {
|
||||||
@ -1810,6 +1858,7 @@ class StatefulWidgetSpy extends StatefulWidget {
|
|||||||
this.onDidChangeDependencies,
|
this.onDidChangeDependencies,
|
||||||
this.onDispose,
|
this.onDispose,
|
||||||
this.onDeactivate,
|
this.onDeactivate,
|
||||||
|
this.onReactivate,
|
||||||
this.onDidUpdateWidget,
|
this.onDidUpdateWidget,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -1818,6 +1867,7 @@ class StatefulWidgetSpy extends StatefulWidget {
|
|||||||
final void Function(BuildContext)? onDidChangeDependencies;
|
final void Function(BuildContext)? onDidChangeDependencies;
|
||||||
final void Function(BuildContext)? onDispose;
|
final void Function(BuildContext)? onDispose;
|
||||||
final void Function(BuildContext)? onDeactivate;
|
final void Function(BuildContext)? onDeactivate;
|
||||||
|
final void Function(BuildContext)? onReactivate;
|
||||||
final void Function(BuildContext)? onDidUpdateWidget;
|
final void Function(BuildContext)? onDidUpdateWidget;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1837,6 +1887,12 @@ class _StatefulWidgetSpyState extends State<StatefulWidgetSpy> {
|
|||||||
widget.onDeactivate?.call(context);
|
widget.onDeactivate?.call(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reactivate() {
|
||||||
|
super.reactivate();
|
||||||
|
widget.onReactivate?.call(context);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user