Material Long Press Text Handle Flash (#32911)
Fix a bug where holding down on text selection caused the handles to flash. The fix was to only update selection when it actually changed.
This commit is contained in:
parent
0e8720e926
commit
6031c56d4c
@ -1 +1 @@
|
|||||||
5e0cf0b9b347526d299718a4755e2d65cffacaba
|
25ab3c95b1ac02a41973c05f96e664370857ce55
|
||||||
|
@ -350,6 +350,18 @@ class RenderEditable extends RenderBox {
|
|||||||
static const int _kShiftMask = 1; // https://developer.android.com/reference/android/view/KeyEvent.html#META_SHIFT_ON
|
static const int _kShiftMask = 1; // https://developer.android.com/reference/android/view/KeyEvent.html#META_SHIFT_ON
|
||||||
static const int _kControlMask = 1 << 12; // https://developer.android.com/reference/android/view/KeyEvent.html#META_CTRL_ON
|
static const int _kControlMask = 1 << 12; // https://developer.android.com/reference/android/view/KeyEvent.html#META_CTRL_ON
|
||||||
|
|
||||||
|
// Call through to onSelectionChanged only if the given nextSelection is
|
||||||
|
// different from the existing selection.
|
||||||
|
void _handlePotentialSelectionChange(
|
||||||
|
TextSelection nextSelection,
|
||||||
|
SelectionChangedCause cause,
|
||||||
|
) {
|
||||||
|
if (nextSelection == selection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelectionChanged(nextSelection, this, cause);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
|
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
|
||||||
void _handleKeyEvent(RawKeyEvent keyEvent) {
|
void _handleKeyEvent(RawKeyEvent keyEvent) {
|
||||||
// Only handle key events on Android.
|
// Only handle key events on Android.
|
||||||
@ -484,21 +496,19 @@ class RenderEditable extends RenderBox {
|
|||||||
// base offset is always less than the extent offset.
|
// base offset is always less than the extent offset.
|
||||||
if (shift) {
|
if (shift) {
|
||||||
if (_baseOffset < newOffset) {
|
if (_baseOffset < newOffset) {
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(
|
TextSelection(
|
||||||
baseOffset: _baseOffset,
|
baseOffset: _baseOffset,
|
||||||
extentOffset: newOffset,
|
extentOffset: newOffset,
|
||||||
),
|
),
|
||||||
this,
|
|
||||||
SelectionChangedCause.keyboard,
|
SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(
|
TextSelection(
|
||||||
baseOffset: newOffset,
|
baseOffset: newOffset,
|
||||||
extentOffset: _baseOffset,
|
extentOffset: _baseOffset,
|
||||||
),
|
),
|
||||||
this,
|
|
||||||
SelectionChangedCause.keyboard,
|
SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -511,13 +521,12 @@ class RenderEditable extends RenderBox {
|
|||||||
else if (rightArrow)
|
else if (rightArrow)
|
||||||
newOffset = _baseOffset > _extentOffset ? _baseOffset : _extentOffset;
|
newOffset = _baseOffset > _extentOffset ? _baseOffset : _extentOffset;
|
||||||
}
|
}
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection.fromPosition(
|
TextSelection.fromPosition(
|
||||||
TextPosition(
|
TextPosition(
|
||||||
offset: newOffset
|
offset: newOffset
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
this,
|
|
||||||
SelectionChangedCause.keyboard,
|
SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -564,12 +573,11 @@ class RenderEditable extends RenderBox {
|
|||||||
case _kAKeyCode:
|
case _kAKeyCode:
|
||||||
_baseOffset = 0;
|
_baseOffset = 0;
|
||||||
_extentOffset = textSelectionDelegate.textEditingValue.text.length;
|
_extentOffset = textSelectionDelegate.textEditingValue.text.length;
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(
|
TextSelection(
|
||||||
baseOffset: 0,
|
baseOffset: 0,
|
||||||
extentOffset: textSelectionDelegate.textEditingValue.text.length,
|
extentOffset: textSelectionDelegate.textEditingValue.text.length,
|
||||||
),
|
),
|
||||||
this,
|
|
||||||
SelectionChangedCause.keyboard,
|
SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -967,7 +975,7 @@ class RenderEditable extends RenderBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleSetSelection(TextSelection selection) {
|
void _handleSetSelection(TextSelection selection) {
|
||||||
onSelectionChanged(selection, this, SelectionChangedCause.keyboard);
|
_handlePotentialSelectionChange(selection, SelectionChangedCause.keyboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
|
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
|
||||||
@ -975,8 +983,8 @@ class RenderEditable extends RenderBox {
|
|||||||
if (extentOffset == null)
|
if (extentOffset == null)
|
||||||
return;
|
return;
|
||||||
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
|
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
|
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -985,8 +993,8 @@ class RenderEditable extends RenderBox {
|
|||||||
if (extentOffset == null)
|
if (extentOffset == null)
|
||||||
return;
|
return;
|
||||||
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
|
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
|
TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -998,12 +1006,11 @@ class RenderEditable extends RenderBox {
|
|||||||
if (nextWord == null)
|
if (nextWord == null)
|
||||||
return;
|
return;
|
||||||
final int baseOffset = extentSelection ? _selection.baseOffset : nextWord.start;
|
final int baseOffset = extentSelection ? _selection.baseOffset : nextWord.start;
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(
|
TextSelection(
|
||||||
baseOffset: baseOffset,
|
baseOffset: baseOffset,
|
||||||
extentOffset: nextWord.start,
|
extentOffset: nextWord.start,
|
||||||
),
|
),
|
||||||
this,
|
|
||||||
SelectionChangedCause.keyboard,
|
SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1016,12 +1023,11 @@ class RenderEditable extends RenderBox {
|
|||||||
if (previousWord == null)
|
if (previousWord == null)
|
||||||
return;
|
return;
|
||||||
final int baseOffset = extentSelection ? _selection.baseOffset : previousWord.start;
|
final int baseOffset = extentSelection ? _selection.baseOffset : previousWord.start;
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(
|
TextSelection(
|
||||||
baseOffset: baseOffset,
|
baseOffset: baseOffset,
|
||||||
extentOffset: previousWord.start,
|
extentOffset: previousWord.start,
|
||||||
),
|
),
|
||||||
this,
|
|
||||||
SelectionChangedCause.keyboard,
|
SelectionChangedCause.keyboard,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1400,7 +1406,7 @@ class RenderEditable extends RenderBox {
|
|||||||
);
|
);
|
||||||
// Call [onSelectionChanged] only when the selection actually changed.
|
// Call [onSelectionChanged] only when the selection actually changed.
|
||||||
if (newSelection != _selection) {
|
if (newSelection != _selection) {
|
||||||
onSelectionChanged(newSelection, this, cause);
|
_handlePotentialSelectionChange(newSelection, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1428,12 +1434,13 @@ class RenderEditable extends RenderBox {
|
|||||||
final TextSelection lastWord = to == null ?
|
final TextSelection lastWord = to == null ?
|
||||||
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
|
firstWord : _selectWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
|
||||||
|
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection(
|
TextSelection(
|
||||||
baseOffset: firstWord.base.offset,
|
baseOffset: firstWord.base.offset,
|
||||||
extentOffset: lastWord.extent.offset,
|
extentOffset: lastWord.extent.offset,
|
||||||
affinity: firstWord.affinity,
|
affinity: firstWord.affinity,
|
||||||
), this, cause,
|
),
|
||||||
|
cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1449,15 +1456,13 @@ class RenderEditable extends RenderBox {
|
|||||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition - _paintOffset));
|
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition - _paintOffset));
|
||||||
final TextRange word = _textPainter.getWordBoundary(position);
|
final TextRange word = _textPainter.getWordBoundary(position);
|
||||||
if (position.offset - word.start <= 1) {
|
if (position.offset - word.start <= 1) {
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
|
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
|
||||||
this,
|
|
||||||
cause,
|
cause,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
onSelectionChanged(
|
_handlePotentialSelectionChange(
|
||||||
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
|
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
|
||||||
this,
|
|
||||||
cause,
|
cause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -367,13 +367,14 @@ void main() {
|
|||||||
|
|
||||||
const String testValue = 'A short phrase';
|
const String testValue = 'A short phrase';
|
||||||
await tester.enterText(find.byType(CupertinoTextField), testValue);
|
await tester.enterText(find.byType(CupertinoTextField), testValue);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
|
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byKey(const ValueKey<int>(1)),
|
find.byKey(const ValueKey<int>(1)),
|
||||||
matchesGoldenFile('text_field_cursor_test.0.1.png'),
|
matchesGoldenFile('text_field_cursor_test.0.2.png'),
|
||||||
);
|
);
|
||||||
}, skip: !Platform.isLinux);
|
}, skip: !Platform.isLinux);
|
||||||
|
|
||||||
@ -395,14 +396,15 @@ void main() {
|
|||||||
|
|
||||||
const String testValue = 'A short phrase';
|
const String testValue = 'A short phrase';
|
||||||
await tester.enterText(find.byType(CupertinoTextField), testValue);
|
await tester.enterText(find.byType(CupertinoTextField), testValue);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
|
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
|
||||||
await tester.pump();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
debugDefaultTargetPlatformOverride = null;
|
debugDefaultTargetPlatformOverride = null;
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byKey(const ValueKey<int>(1)),
|
find.byKey(const ValueKey<int>(1)),
|
||||||
matchesGoldenFile('text_field_cursor_test.1.1.png'),
|
matchesGoldenFile('text_field_cursor_test.1.2.png'),
|
||||||
);
|
);
|
||||||
}, skip: !Platform.isLinux);
|
}, skip: !Platform.isLinux);
|
||||||
|
|
||||||
|
@ -735,6 +735,45 @@ void main() {
|
|||||||
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
|
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Slight movements in longpress don\'t hide/show handles', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
overlay(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const String testValue = 'abc def ghi';
|
||||||
|
await tester.enterText(find.byType(TextField), testValue);
|
||||||
|
expect(controller.value.text, testValue);
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
|
||||||
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
|
||||||
|
// Long press the 'e' to select 'def', but don't release the gesture.
|
||||||
|
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||||
|
final TestGesture gesture = await tester.startGesture(ePos, pointer: 7);
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Handles are shown
|
||||||
|
final Finder fadeFinder = find.byType(FadeTransition);
|
||||||
|
expect(fadeFinder, findsNWidgets(2)); // 2 handles, 1 toolbar
|
||||||
|
FadeTransition handle = tester.widget(fadeFinder.at(0));
|
||||||
|
expect(handle.opacity.value, equals(1.0));
|
||||||
|
|
||||||
|
// Move the gesture very slightly
|
||||||
|
await gesture.moveBy(const Offset(1.0, 1.0));
|
||||||
|
await tester.pump(TextSelectionOverlay.fadeDuration * 0.5);
|
||||||
|
handle = tester.widget(fadeFinder.at(0));
|
||||||
|
|
||||||
|
// The handle should still be fully opaque.
|
||||||
|
expect(handle.opacity.value, equals(1.0));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
|
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user