Fix an issue that Dragging the iOS text selection handles is jumpy and iOS text selection update incorrectly. (#109136)

Better text selection handle dragging experience on iOS.
This commit is contained in:
陈昱 2022-10-25 23:58:53 +08:00 committed by GitHub
parent 13cb46dd99
commit b375b4ac87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 8 deletions

View File

@ -582,11 +582,12 @@ class TextSelectionOverlay {
if (!renderObject.attached) {
return;
}
final Size handleSize = selectionControls!.getHandleSize(
renderObject.preferredLineHeight,
);
_dragEndPosition = details.globalPosition + Offset(0.0, -handleSize.height);
// This adjusts for the fact that the selection handles may not
// perfectly cover the TextPosition that they correspond to.
final Offset offsetFromHandleToTextPosition = _getOffsetToTextPositionPoint(_selectionOverlay.endHandleType);
_dragEndPosition = details.globalPosition + offsetFromHandleToTextPosition;
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
_selectionOverlay.showMagnifier(
@ -660,10 +661,12 @@ class TextSelectionOverlay {
if (!renderObject.attached) {
return;
}
final Size handleSize = selectionControls!.getHandleSize(
renderObject.preferredLineHeight,
);
_dragStartPosition = details.globalPosition + Offset(0.0, -handleSize.height);
// This adjusts for the fact that the selection handles may not
// perfectly cover the TextPosition that they correspond to.
final Offset offsetFromHandleToTextPosition = _getOffsetToTextPositionPoint(_selectionOverlay.startHandleType);
_dragStartPosition = details.globalPosition + offsetFromHandleToTextPosition;
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
_selectionOverlay.showMagnifier(
@ -731,6 +734,32 @@ class TextSelectionOverlay {
void _handleAnyDragEnd(DragEndDetails details) => _selectionOverlay.hideMagnifier(shouldShowToolbar: !_selection.isCollapsed);
// Returns the offset that locates a drag on a handle to the correct line of text.
Offset _getOffsetToTextPositionPoint(TextSelectionHandleType type) {
final Size handleSize = selectionControls!.getHandleSize(
renderObject.preferredLineHeight,
);
// Try to shift center of handle to top by half of handle height.
final double halfHandleHeight = handleSize.height / 2;
// [getHandleAnchor] is used to shift the selection endpoint to the top left
// point of the handle rect when building the handle widget.
// The endpoint is at the bottom of the selection rect, which is also at the
// bottom of the line of text.
// Try to shift the top of the handle to the selection endpoint by the dy of
// the handle's anchor.
final double handleAnchorDy = selectionControls!.getHandleAnchor(type, renderObject.preferredLineHeight).dy;
// Try to shift the selection endpoint to the center of the correct line by
// using half of the line height.
final double halfPreferredLineHeight = renderObject.preferredLineHeight / 2;
// The x offset is accurate, so we only need to adjust the y position.
final double offsetYFromHandleToTextPosition = handleAnchorDy - halfHandleHeight - halfPreferredLineHeight;
return Offset(0.0, offsetYFromHandleToTextPosition);
}
void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
selectionDelegate.userUpdateTextEditingValue(

View File

@ -6805,4 +6805,101 @@ void main() {
}, variant: TargetPlatformVariant.all());
});
});
testWidgets('Can drag handles to change selection correctly in multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
debugShowCheckedModeBanner: false,
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 3,
),
),
),
);
const String testValue =
'First line of text is\n'
'Second line goes until\n'
'Third line of stuff';
const String cutValue =
'First line of text is\n'
'Second until\n'
'Third line of stuff';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Skip past scrolling animation.
await tester.pump();
await tester.pumpAndSettle(const Duration(milliseconds: 200));
// Check that the text spans multiple lines.
final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
expect(firstPos.dx, secondPos.dx);
expect(firstPos.dx, thirdPos.dx);
expect(firstPos.dy, lessThan(secondPos.dy));
expect(secondPos.dy, lessThan(thirdPos.dy));
// Double tap on the 'n' in 'until' to select the word.
final Offset untilPos = textOffsetToPosition(tester, testValue.indexOf('until')+1);
await tester.tapAt(untilPos);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(untilPos);
await tester.pumpAndSettle();
// Skip past the frame where the opacity is zero.
await tester.pump(const Duration(milliseconds: 200));
expect(controller.selection.baseOffset, 39);
expect(controller.selection.extentOffset, 44);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(controller.selection),
renderEditable,
);
expect(endpoints.length, 2);
final Offset offsetFromEndPointToMiddlePoint = Offset(0.0, -renderEditable.preferredLineHeight / 2);
// Drag the left handle to just after 'Second', still on the second line.
Offset handlePos = endpoints[0].point + offsetFromEndPointToMiddlePoint;
Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Second') + 6) + offsetFromEndPointToMiddlePoint;
TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 28);
expect(controller.selection.extentOffset, 44);
// Drag the right handle to just after 'goes', still on the second line.
handlePos = endpoints[1].point + offsetFromEndPointToMiddlePoint;
newHandlePos = textOffsetToPosition(tester, testValue.indexOf('goes') + 4) + offsetFromEndPointToMiddlePoint;
gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump();
await gesture.moveTo(newHandlePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 28);
expect(controller.selection.extentOffset, 38);
if (!isContextMenuProvidedByPlatform) {
await tester.tap(find.text('Cut'));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.text, cutValue);
}
});
}