Unverified Commit 6031c56d authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

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.
parent 0e8720e9
5e0cf0b9b347526d299718a4755e2d65cffacaba 25ab3c95b1ac02a41973c05f96e664370857ce55
...@@ -350,6 +350,18 @@ class RenderEditable extends RenderBox { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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();
......
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