Skip to content

Commit 48019df

Browse files
Expand touch area for Android selection handles (Resolves #2075) (#2079)
1 parent 456cb00 commit 48019df

File tree

62 files changed

+84
-8
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+84
-8
lines changed

super_editor/lib/src/default_editor/document_gestures_touch_android.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,9 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
16791679
link: _controlsController!.collapsedHandleFocalPoint,
16801680
leaderAnchor: Alignment.bottomCenter,
16811681
followerAnchor: Alignment.topCenter,
1682+
// Use the offset to account for the invisible expanded touch region around the handle.
1683+
offset: -Offset(0, AndroidSelectionHandle.defaultTouchRegionExpansion.top) *
1684+
MediaQuery.devicePixelRatioOf(context),
16821685
child: AnimatedOpacity(
16831686
// When the controller doesn't want the handle to be visible, hide it.
16841687
opacity: shouldShow ? 1.0 : 0.0,
@@ -1726,6 +1729,9 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
17261729
link: _controlsController!.upstreamHandleFocalPoint,
17271730
leaderAnchor: Alignment.bottomLeft,
17281731
followerAnchor: Alignment.topRight,
1732+
// Use the offset to account for the invisible expanded touch region around the handle.
1733+
offset:
1734+
-AndroidSelectionHandle.defaultTouchRegionExpansion.topRight * MediaQuery.devicePixelRatioOf(context),
17291735
child: GestureDetector(
17301736
onTapDown: (_) {
17311737
// Register tap down to win gesture arena ASAP.
@@ -1755,6 +1761,9 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
17551761
link: _controlsController!.downstreamHandleFocalPoint,
17561762
leaderAnchor: Alignment.bottomRight,
17571763
followerAnchor: Alignment.topLeft,
1764+
// Use the offset to account for the invisible expanded touch region around the handle.
1765+
offset:
1766+
-AndroidSelectionHandle.defaultTouchRegionExpansion.topLeft * MediaQuery.devicePixelRatioOf(context),
17581767
child: GestureDetector(
17591768
onTapDown: (_) {
17601769
// Register tap down to win gesture arena ASAP.

super_editor/lib/src/infrastructure/platforms/android/selection_handles.dart

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,71 @@ import 'dart:math';
33
import 'package:flutter/material.dart';
44
import 'package:super_editor/src/infrastructure/touch_controls.dart';
55

6+
/// An Android-style mobile selection drag handle.
7+
///
8+
/// Android renders three different types of handles: collapsed, upstream, and downstream.
9+
///
10+
/// All three types of handles look like 3/4 of a circle combined with 1/4 of a square (with
11+
/// a pointy corner). The primary difference between each handle appearance is the way the
12+
/// pointy corner is directed.
13+
///
14+
/// * Collapsed: The pointy corner points up.
15+
/// * Upstream: The pointy corner points to the upper right (marking the start of a selection).
16+
/// * Downstream: The pointy corner points to the upper left (marking the end of a selection).
617
class AndroidSelectionHandle extends StatelessWidget {
18+
static const defaultTouchRegionExpansion = EdgeInsets.only(left: 16, right: 16, bottom: 16);
19+
720
const AndroidSelectionHandle({
821
Key? key,
922
required this.handleType,
1023
required this.color,
1124
this.radius = 10,
25+
this.touchRegionExpansion = defaultTouchRegionExpansion,
26+
this.showDebugTouchRegion = false,
1227
}) : super(key: key);
1328

29+
/// The type of handle, e.g., collapsed, upstream, downstream.
1430
final HandleType handleType;
31+
32+
/// The color of the handle.
1533
final Color color;
34+
35+
/// The radius of the handle - each handle is essentially a circle with one pointy
36+
/// corner.
1637
final double radius;
1738

39+
/// Invisible space added around the handle to increase the touch area the handle.
40+
///
41+
/// This invisible area expands the intrinsic size of the handle, and therefore the
42+
/// visual handle will no longer be aligned exactly with the content that's following.
43+
/// The parent layout needs to adjust the positioning of the handle to account for
44+
/// the [touchRegionExpansion].
45+
final EdgeInsets touchRegionExpansion;
46+
47+
/// Whether to render the [touchRegionExpansion] with a translucent color for visual
48+
/// debugging.
49+
final bool showDebugTouchRegion;
50+
1851
@override
1952
Widget build(BuildContext context) {
53+
late final Widget handle;
2054
switch (handleType) {
2155
case HandleType.collapsed:
22-
return _buildCollapsed();
56+
handle = _buildCollapsed();
2357
case HandleType.upstream:
24-
return _buildUpstream();
58+
handle = _buildUpstream();
2559
case HandleType.downstream:
26-
return _buildDownstream();
60+
handle = _buildDownstream();
2761
}
62+
63+
return Container(
64+
padding: touchRegionExpansion,
65+
decoration: BoxDecoration(
66+
shape: BoxShape.circle,
67+
color: showDebugTouchRegion ? Colors.red.withOpacity(0.5) : Colors.transparent,
68+
),
69+
child: handle,
70+
);
2871
}
2972

3073
Widget _buildCollapsed() {

super_editor/lib/src/super_textfield/android/_editing_controls.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,22 +716,26 @@ class _AndroidEditingOverlayControlsState extends State<AndroidEditingOverlayCon
716716
required void Function(DragStartDetails) onPanStart,
717717
}) {
718718
late Offset fractionalTranslation;
719+
late final Offset expandedTouchAreaAdjustment;
719720
switch (handleType) {
720721
case HandleType.collapsed:
721722
fractionalTranslation = const Offset(-0.5, 0.0);
723+
expandedTouchAreaAdjustment = Offset(0, -AndroidSelectionHandle.defaultTouchRegionExpansion.top);
722724
break;
723725
case HandleType.upstream:
724726
fractionalTranslation = const Offset(-1.0, 0.0);
727+
expandedTouchAreaAdjustment = -AndroidSelectionHandle.defaultTouchRegionExpansion.topRight;
725728
break;
726729
case HandleType.downstream:
727730
fractionalTranslation = Offset.zero;
731+
expandedTouchAreaAdjustment = -AndroidSelectionHandle.defaultTouchRegionExpansion.topLeft;
728732
break;
729733
}
730734

731735
return CompositedTransformFollower(
732736
key: handleKey,
733737
link: widget.textContentLayerLink,
734-
offset: followerOffset,
738+
offset: followerOffset + expandedTouchAreaAdjustment,
735739
child: FractionalTranslation(
736740
translation: fractionalTranslation,
737741
child: GestureDetector(

super_editor/test/super_editor/mobile/super_editor_android_overlay_controls_test.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_test/flutter_test.dart';
33
import 'package:flutter_test_robots/flutter_test_robots.dart';
44
import 'package:flutter_test_runners/flutter_test_runners.dart';
5+
import 'package:super_editor/src/infrastructure/platforms/android/selection_handles.dart';
56
import 'package:super_editor/super_editor.dart';
67
import 'package:super_editor/super_editor_test.dart';
78
import 'package:super_text_layout/super_text_layout.dart';
@@ -126,6 +127,10 @@ void main() {
126127
// Ensure the toolbar isn't visible.
127128
expect(SuperEditorInspector.isMobileToolbarVisible(), isFalse);
128129

130+
// Wait for the collapsed handle to disappear so that it doesn't cover the
131+
// line below.
132+
await tester.pump(const Duration(seconds: 5));
133+
129134
// Place the caret at the beginning of the second paragraph, at the same offset.
130135
await tester.placeCaretInParagraph("2", 0);
131136

@@ -215,11 +220,13 @@ void main() {
215220
expect(SuperEditorInspector.findAllMobileDragHandles(), findsExactly(2));
216221
expect(
217222
tester.getTopLeft(SuperEditorInspector.findMobileDownstreamDragHandle()),
218-
offsetMoreOrLessEquals(documentLayout.getGlobalOffsetFromDocumentOffset(selectedPositionRect.bottomRight)),
223+
offsetMoreOrLessEquals(documentLayout.getGlobalOffsetFromDocumentOffset(selectedPositionRect.bottomRight) -
224+
Offset(AndroidSelectionHandle.defaultTouchRegionExpansion.left, 0)),
219225
);
220226
expect(
221227
tester.getTopRight(SuperEditorInspector.findMobileUpstreamDragHandle()),
222-
offsetMoreOrLessEquals(documentLayout.getGlobalOffsetFromDocumentOffset(selectedPositionRect.bottomRight)),
228+
offsetMoreOrLessEquals(documentLayout.getGlobalOffsetFromDocumentOffset(selectedPositionRect.bottomRight) +
229+
Offset(AndroidSelectionHandle.defaultTouchRegionExpansion.right, 0)),
223230
);
224231

225232
// Release the drag handle.
0 Bytes
-34 Bytes
-59 Bytes
-24 Bytes
-14 Bytes
-9 Bytes

0 commit comments

Comments
 (0)