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:
parent
13cb46dd99
commit
b375b4ac87
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user