Unverified Commit 93b00425 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Handle dragging improvements (#114042)

Improves the feel of dragging text selection vertically between lines on mobile.
parent d0afbd72
...@@ -637,7 +637,13 @@ class TextSelectionOverlay { ...@@ -637,7 +637,13 @@ class TextSelectionOverlay {
); );
} }
late Offset _dragEndPosition; // The contact position of the gesture at the current end handle location.
// Updated when the handle moves.
late double _endHandleDragPosition;
// The distance from _endHandleDragPosition to the center of the line that it
// corresponds to.
late double _endHandleDragPositionToCenterOfLine;
void _handleSelectionEndHandleDragStart(DragStartDetails details) { void _handleSelectionEndHandleDragStart(DragStartDetails details) {
if (!renderObject.attached) { if (!renderObject.attached) {
...@@ -646,10 +652,17 @@ class TextSelectionOverlay { ...@@ -646,10 +652,17 @@ class TextSelectionOverlay {
// This adjusts for the fact that the selection handles may not // This adjusts for the fact that the selection handles may not
// perfectly cover the TextPosition that they correspond to. // perfectly cover the TextPosition that they correspond to.
final Offset offsetFromHandleToTextPosition = _getOffsetToTextPositionPoint(_selectionOverlay.endHandleType); _endHandleDragPosition = details.globalPosition.dy;
_dragEndPosition = details.globalPosition + offsetFromHandleToTextPosition; final Offset endPoint =
renderObject.localToGlobal(_selectionOverlay.selectionEndpoints.last.point);
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition); final double centerOfLine = endPoint.dy - renderObject.preferredLineHeight / 2;
_endHandleDragPositionToCenterOfLine = centerOfLine - _endHandleDragPosition;
final TextPosition position = renderObject.getPositionForPoint(
Offset(
details.globalPosition.dx,
centerOfLine,
),
);
_selectionOverlay.showMagnifier( _selectionOverlay.showMagnifier(
_buildMagnifier( _buildMagnifier(
...@@ -660,14 +673,33 @@ class TextSelectionOverlay { ...@@ -660,14 +673,33 @@ class TextSelectionOverlay {
); );
} }
/// Given a handle position and drag position, returns the position of handle
/// after the drag.
///
/// The handle jumps instantly between lines when the drag reaches a full
/// line's height away from the original handle position. In other words, the
/// line jump happens when the contact point would be located at the same
/// place on the handle at the new line as when the gesture started.
double _getHandleDy(double dragDy, double handleDy) {
final double distanceDragged = dragDy - handleDy;
final int dragDirection = distanceDragged < 0.0 ? -1 : 1;
final int linesDragged =
dragDirection * (distanceDragged.abs() / renderObject.preferredLineHeight).floor();
return handleDy + linesDragged * renderObject.preferredLineHeight;
}
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
if (!renderObject.attached) { if (!renderObject.attached) {
return; return;
} }
_dragEndPosition += details.delta;
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition); _endHandleDragPosition = _getHandleDy(details.globalPosition.dy, _endHandleDragPosition);
final TextSelection currentSelection = TextSelection.fromPosition(position); final Offset adjustedOffset = Offset(
details.globalPosition.dx,
_endHandleDragPosition + _endHandleDragPositionToCenterOfLine,
);
final TextPosition position = renderObject.getPositionForPoint(adjustedOffset);
if (_selection.isCollapsed) { if (_selection.isCollapsed) {
_selectionOverlay.updateMagnifier(_buildMagnifier( _selectionOverlay.updateMagnifier(_buildMagnifier(
...@@ -676,6 +708,7 @@ class TextSelectionOverlay { ...@@ -676,6 +708,7 @@ class TextSelectionOverlay {
renderEditable: renderObject, renderEditable: renderObject,
)); ));
final TextSelection currentSelection = TextSelection.fromPosition(position);
_handleSelectionHandleChanged(currentSelection, isEnd: true); _handleSelectionHandleChanged(currentSelection, isEnd: true);
return; return;
} }
...@@ -716,7 +749,13 @@ class TextSelectionOverlay { ...@@ -716,7 +749,13 @@ class TextSelectionOverlay {
)); ));
} }
late Offset _dragStartPosition; // The contact position of the gesture at the current start handle location.
// Updated when the handle moves.
late double _startHandleDragPosition;
// The distance from _startHandleDragPosition to the center of the line that
// it corresponds to.
late double _startHandleDragPositionToCenterOfLine;
void _handleSelectionStartHandleDragStart(DragStartDetails details) { void _handleSelectionStartHandleDragStart(DragStartDetails details) {
if (!renderObject.attached) { if (!renderObject.attached) {
...@@ -725,10 +764,17 @@ class TextSelectionOverlay { ...@@ -725,10 +764,17 @@ class TextSelectionOverlay {
// This adjusts for the fact that the selection handles may not // This adjusts for the fact that the selection handles may not
// perfectly cover the TextPosition that they correspond to. // perfectly cover the TextPosition that they correspond to.
final Offset offsetFromHandleToTextPosition = _getOffsetToTextPositionPoint(_selectionOverlay.startHandleType); _startHandleDragPosition = details.globalPosition.dy;
_dragStartPosition = details.globalPosition + offsetFromHandleToTextPosition; final Offset startPoint =
renderObject.localToGlobal(_selectionOverlay.selectionEndpoints.first.point);
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition); final double centerOfLine = startPoint.dy - renderObject.preferredLineHeight / 2;
_startHandleDragPositionToCenterOfLine = centerOfLine - _startHandleDragPosition;
final TextPosition position = renderObject.getPositionForPoint(
Offset(
details.globalPosition.dx,
centerOfLine,
),
);
_selectionOverlay.showMagnifier( _selectionOverlay.showMagnifier(
_buildMagnifier( _buildMagnifier(
...@@ -743,8 +789,13 @@ class TextSelectionOverlay { ...@@ -743,8 +789,13 @@ class TextSelectionOverlay {
if (!renderObject.attached) { if (!renderObject.attached) {
return; return;
} }
_dragStartPosition += details.delta;
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition); _startHandleDragPosition = _getHandleDy(details.globalPosition.dy, _startHandleDragPosition);
final Offset adjustedOffset = Offset(
details.globalPosition.dx,
_startHandleDragPosition + _startHandleDragPositionToCenterOfLine,
);
final TextPosition position = renderObject.getPositionForPoint(adjustedOffset);
if (_selection.isCollapsed) { if (_selection.isCollapsed) {
_selectionOverlay.updateMagnifier(_buildMagnifier( _selectionOverlay.updateMagnifier(_buildMagnifier(
...@@ -753,7 +804,8 @@ class TextSelectionOverlay { ...@@ -753,7 +804,8 @@ class TextSelectionOverlay {
renderEditable: renderObject, renderEditable: renderObject,
)); ));
_handleSelectionHandleChanged(TextSelection.fromPosition(position), isEnd: false); final TextSelection currentSelection = TextSelection.fromPosition(position);
_handleSelectionHandleChanged(currentSelection, isEnd: false);
return; return;
} }
...@@ -813,32 +865,6 @@ class TextSelectionOverlay { ...@@ -813,32 +865,6 @@ class TextSelectionOverlay {
} }
} }
// 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}) { void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base; final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
selectionDelegate.userUpdateTextEditingValue( selectionDelegate.userUpdateTextEditingValue(
......
...@@ -1213,7 +1213,11 @@ void main() { ...@@ -1213,7 +1213,11 @@ void main() {
// Drag the right handle to the third line, just after 'Third'. // Drag the right handle to the third line, just after 'Third'.
Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0); Offset handlePos = endpoints[1].point + const Offset(1.0, 1.0);
Offset newHandlePos = textOffsetToPosition(tester, testValue.indexOf('Third') + 5); // The distance below the y value returned by textOffsetToPosition required
// to register a full vertical line drag.
const Offset downLineOffset = Offset(0.0, 3.0);
Offset newHandlePos =
textOffsetToPosition(tester, testValue.indexOf('Third') + 5) + downLineOffset;
gesture = await tester.startGesture(handlePos, pointer: 7); gesture = await tester.startGesture(handlePos, pointer: 7);
await tester.pump(); await tester.pump();
await gesture.moveTo(newHandlePos); await gesture.moveTo(newHandlePos);
...@@ -1221,8 +1225,13 @@ void main() { ...@@ -1221,8 +1225,13 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(controller.selection.baseOffset, 39); expect(
expect(controller.selection.extentOffset, 50); controller.selection,
const TextSelection(
baseOffset: 39,
extentOffset: 50,
),
);
// Drag the left handle to the first line, just after 'First'. // Drag the left handle to the first line, just after 'First'.
handlePos = endpoints[0].point + const Offset(-1.0, 1.0); handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
......
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