Skip to content

Commit 93b0042

Browse files
authored
Handle dragging improvements (#114042)
Improves the feel of dragging text selection vertically between lines on mobile.
1 parent d0afbd7 commit 93b0042

File tree

4 files changed

+853
-50
lines changed

4 files changed

+853
-50
lines changed

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,13 @@ class TextSelectionOverlay {
637637
);
638638
}
639639

640-
late Offset _dragEndPosition;
640+
// The contact position of the gesture at the current end handle location.
641+
// Updated when the handle moves.
642+
late double _endHandleDragPosition;
643+
644+
// The distance from _endHandleDragPosition to the center of the line that it
645+
// corresponds to.
646+
late double _endHandleDragPositionToCenterOfLine;
641647

642648
void _handleSelectionEndHandleDragStart(DragStartDetails details) {
643649
if (!renderObject.attached) {
@@ -646,10 +652,17 @@ class TextSelectionOverlay {
646652

647653
// This adjusts for the fact that the selection handles may not
648654
// perfectly cover the TextPosition that they correspond to.
649-
final Offset offsetFromHandleToTextPosition = _getOffsetToTextPositionPoint(_selectionOverlay.endHandleType);
650-
_dragEndPosition = details.globalPosition + offsetFromHandleToTextPosition;
651-
652-
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
655+
_endHandleDragPosition = details.globalPosition.dy;
656+
final Offset endPoint =
657+
renderObject.localToGlobal(_selectionOverlay.selectionEndpoints.last.point);
658+
final double centerOfLine = endPoint.dy - renderObject.preferredLineHeight / 2;
659+
_endHandleDragPositionToCenterOfLine = centerOfLine - _endHandleDragPosition;
660+
final TextPosition position = renderObject.getPositionForPoint(
661+
Offset(
662+
details.globalPosition.dx,
663+
centerOfLine,
664+
),
665+
);
653666

654667
_selectionOverlay.showMagnifier(
655668
_buildMagnifier(
@@ -660,14 +673,33 @@ class TextSelectionOverlay {
660673
);
661674
}
662675

676+
/// Given a handle position and drag position, returns the position of handle
677+
/// after the drag.
678+
///
679+
/// The handle jumps instantly between lines when the drag reaches a full
680+
/// line's height away from the original handle position. In other words, the
681+
/// line jump happens when the contact point would be located at the same
682+
/// place on the handle at the new line as when the gesture started.
683+
double _getHandleDy(double dragDy, double handleDy) {
684+
final double distanceDragged = dragDy - handleDy;
685+
final int dragDirection = distanceDragged < 0.0 ? -1 : 1;
686+
final int linesDragged =
687+
dragDirection * (distanceDragged.abs() / renderObject.preferredLineHeight).floor();
688+
return handleDy + linesDragged * renderObject.preferredLineHeight;
689+
}
690+
663691
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
664692
if (!renderObject.attached) {
665693
return;
666694
}
667-
_dragEndPosition += details.delta;
668695

669-
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
670-
final TextSelection currentSelection = TextSelection.fromPosition(position);
696+
_endHandleDragPosition = _getHandleDy(details.globalPosition.dy, _endHandleDragPosition);
697+
final Offset adjustedOffset = Offset(
698+
details.globalPosition.dx,
699+
_endHandleDragPosition + _endHandleDragPositionToCenterOfLine,
700+
);
701+
702+
final TextPosition position = renderObject.getPositionForPoint(adjustedOffset);
671703

672704
if (_selection.isCollapsed) {
673705
_selectionOverlay.updateMagnifier(_buildMagnifier(
@@ -676,6 +708,7 @@ class TextSelectionOverlay {
676708
renderEditable: renderObject,
677709
));
678710

711+
final TextSelection currentSelection = TextSelection.fromPosition(position);
679712
_handleSelectionHandleChanged(currentSelection, isEnd: true);
680713
return;
681714
}
@@ -716,7 +749,13 @@ class TextSelectionOverlay {
716749
));
717750
}
718751

719-
late Offset _dragStartPosition;
752+
// The contact position of the gesture at the current start handle location.
753+
// Updated when the handle moves.
754+
late double _startHandleDragPosition;
755+
756+
// The distance from _startHandleDragPosition to the center of the line that
757+
// it corresponds to.
758+
late double _startHandleDragPositionToCenterOfLine;
720759

721760
void _handleSelectionStartHandleDragStart(DragStartDetails details) {
722761
if (!renderObject.attached) {
@@ -725,10 +764,17 @@ class TextSelectionOverlay {
725764

726765
// This adjusts for the fact that the selection handles may not
727766
// perfectly cover the TextPosition that they correspond to.
728-
final Offset offsetFromHandleToTextPosition = _getOffsetToTextPositionPoint(_selectionOverlay.startHandleType);
729-
_dragStartPosition = details.globalPosition + offsetFromHandleToTextPosition;
730-
731-
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
767+
_startHandleDragPosition = details.globalPosition.dy;
768+
final Offset startPoint =
769+
renderObject.localToGlobal(_selectionOverlay.selectionEndpoints.first.point);
770+
final double centerOfLine = startPoint.dy - renderObject.preferredLineHeight / 2;
771+
_startHandleDragPositionToCenterOfLine = centerOfLine - _startHandleDragPosition;
772+
final TextPosition position = renderObject.getPositionForPoint(
773+
Offset(
774+
details.globalPosition.dx,
775+
centerOfLine,
776+
),
777+
);
732778

733779
_selectionOverlay.showMagnifier(
734780
_buildMagnifier(
@@ -743,8 +789,13 @@ class TextSelectionOverlay {
743789
if (!renderObject.attached) {
744790
return;
745791
}
746-
_dragStartPosition += details.delta;
747-
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
792+
793+
_startHandleDragPosition = _getHandleDy(details.globalPosition.dy, _startHandleDragPosition);
794+
final Offset adjustedOffset = Offset(
795+
details.globalPosition.dx,
796+
_startHandleDragPosition + _startHandleDragPositionToCenterOfLine,
797+
);
798+
final TextPosition position = renderObject.getPositionForPoint(adjustedOffset);
748799

749800
if (_selection.isCollapsed) {
750801
_selectionOverlay.updateMagnifier(_buildMagnifier(
@@ -753,7 +804,8 @@ class TextSelectionOverlay {
753804
renderEditable: renderObject,
754805
));
755806

756-
_handleSelectionHandleChanged(TextSelection.fromPosition(position), isEnd: false);
807+
final TextSelection currentSelection = TextSelection.fromPosition(position);
808+
_handleSelectionHandleChanged(currentSelection, isEnd: false);
757809
return;
758810
}
759811

@@ -813,32 +865,6 @@ class TextSelectionOverlay {
813865
}
814866
}
815867

816-
// Returns the offset that locates a drag on a handle to the correct line of text.
817-
Offset _getOffsetToTextPositionPoint(TextSelectionHandleType type) {
818-
final Size handleSize = selectionControls!.getHandleSize(
819-
renderObject.preferredLineHeight,
820-
);
821-
822-
// Try to shift center of handle to top by half of handle height.
823-
final double halfHandleHeight = handleSize.height / 2;
824-
825-
// [getHandleAnchor] is used to shift the selection endpoint to the top left
826-
// point of the handle rect when building the handle widget.
827-
// The endpoint is at the bottom of the selection rect, which is also at the
828-
// bottom of the line of text.
829-
// Try to shift the top of the handle to the selection endpoint by the dy of
830-
// the handle's anchor.
831-
final double handleAnchorDy = selectionControls!.getHandleAnchor(type, renderObject.preferredLineHeight).dy;
832-
833-
// Try to shift the selection endpoint to the center of the correct line by
834-
// using half of the line height.
835-
final double halfPreferredLineHeight = renderObject.preferredLineHeight / 2;
836-
837-
// The x offset is accurate, so we only need to adjust the y position.
838-
final double offsetYFromHandleToTextPosition = handleAnchorDy - halfHandleHeight - halfPreferredLineHeight;
839-
return Offset(0.0, offsetYFromHandleToTextPosition);
840-
}
841-
842868
void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
843869
final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
844870
selectionDelegate.userUpdateTextEditingValue(

0 commit comments

Comments
 (0)