Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
b3b764c9
Unverified
Commit
b3b764c9
authored
Dec 21, 2018
by
xster
Committed by
GitHub
Dec 21, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Revise Android and iOS gestures on Material TextField (#24457)
parent
eb7a59b6
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
918 additions
and
74 deletions
+918
-74
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+12
-56
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+41
-18
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+157
-0
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+563
-0
text_selection_test.dart
packages/flutter/test/widgets/text_selection_test.dart
+145
-0
No files found.
packages/flutter/lib/src/cupertino/text_field.dart
View file @
b3b764c9
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:async'
;
import
'package:flutter/gestures.dart'
show
kDoubleTapTimeout
,
kDoubleTapSlop
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
...
...
@@ -412,13 +409,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
FocusNode
_focusNode
;
FocusNode
get
_effectiveFocusNode
=>
widget
.
focusNode
??
(
_focusNode
??=
FocusNode
());
// Is shortly after a previous single tap when not null.
Timer
_doubleTapTimer
;
Offset
_lastTapOffset
;
// True if second tap down of a double tap is detected. Used to discard
// subsequent tap up / tap hold of the same tap.
bool
_isDoubleTap
=
false
;
@override
void
initState
()
{
super
.
initState
();
...
...
@@ -448,7 +438,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
void
dispose
()
{
_focusNode
?.
dispose
();
_controller
?.
removeListener
(
updateKeepAlive
);
_doubleTapTimer
?.
cancel
();
super
.
dispose
();
}
...
...
@@ -458,54 +447,21 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
RenderEditable
get
_renderEditable
=>
_editableTextKey
.
currentState
.
renderEditable
;
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
void
_handleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
handleTapDown
(
details
);
// This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
if
(
_doubleTapTimer
!=
null
&&
_isWithinDoubleTapTolerance
(
details
.
globalPosition
))
{
// If there was already a previous tap, the second down hold/tap is a
// double tap.
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleTap
);
_doubleTapTimer
.
cancel
();
_doubleTapTimeout
();
_isDoubleTap
=
true
;
}
}
void
_handleTapUp
(
TapUpDetails
details
)
{
if
(!
_isDoubleTap
)
{
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
_lastTapOffset
=
details
.
globalPosition
;
_doubleTapTimer
=
Timer
(
kDoubleTapTimeout
,
_doubleTapTimeout
);
_requestKeyboard
();
}
_isDoubleTap
=
false
;
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
_requestKeyboard
();
}
void
_handleLongPress
()
{
if
(!
_isDoubleTap
)
{
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
longPress
);
}
_isDoubleTap
=
false
;
void
_handleSingleLongTapDown
()
{
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
longPress
);
}
void
_doubleTapTimeout
()
{
_doubleTapTimer
=
null
;
_lastTapOffset
=
null
;
}
bool
_isWithinDoubleTapTolerance
(
Offset
secondTapOffset
)
{
assert
(
secondTapOffset
!=
null
);
if
(
_lastTapOffset
==
null
)
{
return
false
;
}
final
Offset
difference
=
secondTapOffset
-
_lastTapOffset
;
return
difference
.
distance
<=
kDoubleTapSlop
;
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleTap
);
}
@override
...
...
@@ -690,12 +646,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
:
CupertinoTheme
.
of
(
context
).
brightness
==
Brightness
.
light
?
_kDisabledBackground
:
CupertinoColors
.
darkBackgroundGray
,
child:
GestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
child:
TextSelectionGestureDetector
(
onTapDown:
_handleTapDown
,
onTapUp:
_handleTapUp
,
onLongPress:
_handleLongPress
,
excludeFromSemantics:
true
,
onSingleTapUp:
_handleSingleTapUp
,
onSingleLongTapDown:
_handleSingleLongTapDown
,
onDoubleTapDown:
_handleDoubleTapDown
,
behavior:
HitTestBehavior
.
translucent
,
child:
_addTextDependentAttachments
(
paddedEditable
,
textStyle
),
),
),
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
b3b764c9
...
...
@@ -411,8 +411,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
InputDecoration
_getEffectiveDecoration
()
{
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
InputDecoration
effectiveDecoration
=
(
widget
.
decoration
??
const
InputDecoration
())
.
applyDefaults
(
Theme
.
of
(
context
)
.
inputDecorationTheme
)
.
applyDefaults
(
themeData
.
inputDecorationTheme
)
.
copyWith
(
enabled:
widget
.
enabled
,
hintMaxLines:
widget
.
decoration
?.
hintMaxLines
??
widget
.
maxLines
...
...
@@ -434,7 +435,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
// Handle length exceeds maxLength
if
(
_effectiveController
.
value
.
text
.
runes
.
length
>
widget
.
maxLength
)
{
final
ThemeData
themeData
=
Theme
.
of
(
context
);
return
effectiveDecoration
.
copyWith
(
errorText:
effectiveDecoration
.
errorText
??
''
,
counterStyle:
effectiveDecoration
.
errorStyle
...
...
@@ -489,10 +489,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
InteractiveInkFeature
_createInkFeature
(
TapDownDetails
details
)
{
final
MaterialInkController
inkController
=
Material
.
of
(
context
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
BuildContext
editableContext
=
_editableTextKey
.
currentContext
;
final
RenderBox
referenceBox
=
InputDecorator
.
containerOf
(
editableContext
)
??
editableContext
.
findRenderObject
();
final
Offset
position
=
referenceBox
.
globalToLocal
(
details
.
globalPosition
);
final
Color
color
=
Theme
.
of
(
context
)
.
splashColor
;
final
Color
color
=
themeData
.
splashColor
;
InteractiveInkFeature
splash
;
void
handleRemoved
()
{
...
...
@@ -505,7 +506,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
}
// else we're probably in deactivate()
}
splash
=
Theme
.
of
(
context
)
.
splashFactory
.
create
(
splash
=
themeData
.
splashFactory
.
create
(
controller:
inkController
,
referenceBox:
referenceBox
,
position:
position
,
...
...
@@ -527,25 +528,47 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
_startSplash
(
details
);
}
void
_handleTap
()
{
if
(
widget
.
selectionEnabled
)
_renderEditable
.
handleTap
();
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
tap
);
break
;
}
}
_requestKeyboard
();
_confirmCurrentSplash
();
if
(
widget
.
onTap
!=
null
)
widget
.
onTap
();
}
void
_handleTapCancel
()
{
void
_handle
Single
TapCancel
()
{
_cancelCurrentSplash
();
}
void
_handleLongPress
()
{
if
(
widget
.
selectionEnabled
)
_renderEditable
.
handleLongPress
();
void
_handleSingleLongTapDown
()
{
if
(
widget
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
longPress
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
longPress
);
break
;
}
}
_confirmCurrentSplash
();
}
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleTap
);
}
void
_startSplash
(
TapDownDetails
details
)
{
if
(
_effectiveFocusNode
.
hasFocus
)
return
;
...
...
@@ -632,7 +655,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
rendererIgnoresPointer:
true
,
cursorWidth:
widget
.
cursorWidth
,
cursorRadius:
widget
.
cursorRadius
,
cursorColor:
widget
.
cursorColor
??
Theme
.
of
(
context
)
.
cursorColor
,
cursorColor:
widget
.
cursorColor
??
themeData
.
cursorColor
,
backgroundCursorColor:
CupertinoColors
.
inactiveGray
,
scrollPadding:
widget
.
scrollPadding
,
keyboardAppearance:
keyboardAppearance
,
...
...
@@ -665,13 +688,13 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
},
child:
IgnorePointer
(
ignoring:
!(
widget
.
enabled
??
widget
.
decoration
?.
enabled
??
true
),
child:
GestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
child:
TextSelectionGestureDetector
(
onTapDown:
_handleTapDown
,
onTap:
_handleTap
,
onTapCancel:
_handleTapCancel
,
onLongPress:
_handleLongPress
,
excludeFromSemantics:
true
,
onSingleTapUp:
_handleSingleTapUp
,
onSingleTapCancel:
_handleSingleTapCancel
,
onSingleLongTapDown:
_handleSingleLongTapDown
,
onDoubleTapDown:
_handleDoubleTapDown
,
behavior:
HitTestBehavior
.
translucent
,
child:
child
,
),
),
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
b3b764c9
...
...
@@ -4,6 +4,7 @@
import
'dart:async'
;
import
'package:flutter/gestures.dart'
show
kDoubleTapTimeout
,
kDoubleTapSlop
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/scheduler.dart'
;
...
...
@@ -568,3 +569,159 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
return
null
;
}
}
/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
/// double tap will only recognize one or the other. This widget detects both:
/// first the tap and then, if another tap down occurs within a time limit, the
/// double tap.
///
/// See also:
///
/// * [TextField], a Material text field which uses this gesture detector.
/// * [CupertinoTextField], a Cupertino text field which uses this gesture
/// detector.
class
TextSelectionGestureDetector
extends
StatefulWidget
{
/// Create a [TextSelectionGestureDetector].
///
/// Multiple callbacks can be called for one sequence of input gesture.
/// The [child] parameter must not be null.
const
TextSelectionGestureDetector
({
Key
key
,
this
.
onTapDown
,
this
.
onSingleTapUp
,
this
.
onSingleTapCancel
,
this
.
onSingleLongTapDown
,
this
.
onDoubleTapDown
,
this
.
behavior
,
@required
this
.
child
,
})
:
assert
(
child
!=
null
),
super
(
key:
key
);
/// Called for every tap down including every tap down that's part of a
/// double click or a long press, except touches that include enough movement
/// to not qualify as taps (e.g. pans and flings).
final
GestureTapDownCallback
onTapDown
;
/// Called for each distinct tap except for every second tap of a double tap.
/// For example, if the detector was configured [onSingleTapDown] and
/// [onDoubleTapDown], three quick taps would be recognized as a single tap
/// down, followed by a double tap down, followed by a single tap down.
final
GestureTapUpCallback
onSingleTapUp
;
/// Called for each touch that becomes recognized as a gesture that is not a
/// short tap, such as a long tap or drag. It is called at the moment when
/// another gesture from the touch is recognized.
final
GestureTapCancelCallback
onSingleTapCancel
;
/// Called for a single long tap that's sustained for longer than
/// [kLongPressTimeout] but not necessarily lifted. Not called for a
/// double-tap-hold, which calls [onDoubleTapDown] instead.
final
GestureLongPressCallback
onSingleLongTapDown
;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous short tap.
final
GestureTapDownCallback
onDoubleTapDown
;
/// How this gesture detector should behave during hit testing.
///
/// This defaults to [HitTestBehavior.deferToChild].
final
HitTestBehavior
behavior
;
/// Child below this widget.
final
Widget
child
;
@override
State
<
StatefulWidget
>
createState
()
=>
_TextSelectionGestureDetectorState
();
}
class
_TextSelectionGestureDetectorState
extends
State
<
TextSelectionGestureDetector
>
{
// Counts down for a short duration after a previous tap. Null otherwise.
Timer
_doubleTapTimer
;
Offset
_lastTapOffset
;
// True if a second tap down of a double tap is detected. Used to discard
// subsequent tap up / tap hold of the same tap.
bool
_isDoubleTap
=
false
;
@override
void
dispose
()
{
_doubleTapTimer
?.
cancel
();
super
.
dispose
();
}
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
void
_handleTapDown
(
TapDownDetails
details
)
{
if
(
widget
.
onTapDown
!=
null
)
{
widget
.
onTapDown
(
details
);
}
// This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
if
(
_doubleTapTimer
!=
null
&&
_isWithinDoubleTapTolerance
(
details
.
globalPosition
))
{
// If there was already a previous tap, the second down hold/tap is a
// double tap down.
if
(
widget
.
onDoubleTapDown
!=
null
)
{
widget
.
onDoubleTapDown
(
details
);
}
_doubleTapTimer
.
cancel
();
_doubleTapTimeout
();
_isDoubleTap
=
true
;
}
}
void
_handleTapUp
(
TapUpDetails
details
)
{
if
(!
_isDoubleTap
)
{
if
(
widget
.
onSingleTapUp
!=
null
)
{
widget
.
onSingleTapUp
(
details
);
}
_lastTapOffset
=
details
.
globalPosition
;
_doubleTapTimer
=
Timer
(
kDoubleTapTimeout
,
_doubleTapTimeout
);
}
_isDoubleTap
=
false
;
}
void
_handleTapCancel
()
{
if
(
widget
.
onSingleTapCancel
!=
null
)
{
widget
.
onSingleTapCancel
();
}
}
void
_handleLongPress
()
{
if
(!
_isDoubleTap
&&
widget
.
onSingleLongTapDown
!=
null
)
{
widget
.
onSingleLongTapDown
();
}
_isDoubleTap
=
false
;
}
void
_doubleTapTimeout
()
{
_doubleTapTimer
=
null
;
_lastTapOffset
=
null
;
}
bool
_isWithinDoubleTapTolerance
(
Offset
secondTapOffset
)
{
assert
(
secondTapOffset
!=
null
);
if
(
_lastTapOffset
==
null
)
{
return
false
;
}
final
Offset
difference
=
secondTapOffset
-
_lastTapOffset
;
return
difference
.
distance
<=
kDoubleTapSlop
;
}
@override
Widget
build
(
BuildContext
context
)
{
return
GestureDetector
(
onTapDown:
_handleTapDown
,
onTapUp:
_handleTapUp
,
onTapCancel:
_handleTapCancel
,
onLongPress:
_handleLongPress
,
excludeFromSemantics:
true
,
behavior:
widget
.
behavior
,
child:
widget
.
child
,
);
}
}
packages/flutter/test/material/text_field_test.dart
View file @
b3b764c9
...
...
@@ -7,6 +7,7 @@ import 'dart:io' show Platform;
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
show
window
;
import
'package:flutter/cupertino.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/rendering.dart'
;
...
...
@@ -3455,8 +3456,12 @@ void main() {
expect
(
tapCount
,
0
);
await
tester
.
tap
(
find
.
byType
(
TextField
));
// Wait a bit so they're all single taps and not double taps.
await
tester
.
pump
(
const
Duration
(
milliseconds:
300
));
await
tester
.
tap
(
find
.
byType
(
TextField
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
300
));
await
tester
.
tap
(
find
.
byType
(
TextField
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
300
));
expect
(
tapCount
,
3
);
});
...
...
@@ -3560,4 +3565,562 @@ void main() {
)));
expect
(
tester
.
takeException
(),
isNotNull
);
});
testWidgets
(
'tap moves cursor to the edge of the word it tapped on (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
();
// We moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
7
,
affinity:
TextAffinity
.
upstream
),
);
// But don't trigger the toolbar.
expect
(
find
.
byType
(
CupertinoButton
),
findsNothing
);
},
);
testWidgets
(
'tap moves cursor to the position tapped (Android)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
();
// We moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
3
),
);
// But don't trigger the toolbar.
expect
(
find
.
byType
(
FlatButton
),
findsNothing
);
},
);
testWidgets
(
'two slow taps do not trigger a word selection (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
();
// Plain collapsed selection.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
7
,
affinity:
TextAffinity
.
upstream
),
);
// No toolbar.
expect
(
find
.
byType
(
CupertinoButton
),
findsNothing
);
},
);
testWidgets
(
'double tap selects word and first tap of double tap moves cursor (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
8
,
affinity:
TextAffinity
.
downstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
();
// Second tap selects the word around the cursor.
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
8
,
extentOffset:
12
),
);
// Selected text shows 3 toolbar buttons.
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
},
);
testWidgets
(
'double tap selects word and first tap of double tap moves cursor and shows toolbar (Android)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
// This tap just puts the cursor somewhere different than where the double
// tap will occur to test that the double tap moves the existing cursor first.
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
9
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
();
// Second tap selects the word around the cursor.
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
8
,
extentOffset:
12
),
);
// Selected text shows 3 toolbar buttons.
expect
(
find
.
byType
(
FlatButton
),
findsNWidgets
(
3
));
},
);
testWidgets
(
'double tap hold selects word (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
final
TestGesture
gesture
=
await
tester
.
startGesture
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
// Hold the press.
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
8
,
extentOffset:
12
),
);
// Selected text shows 3 toolbar buttons.
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
await
gesture
.
up
();
await
tester
.
pump
();
// Still selected.
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
8
,
extentOffset:
12
),
);
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
},
);
testWidgets
(
'tap after a double tap select is not affected (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
8
,
affinity:
TextAffinity
.
downstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
100.0
,
5.0
));
await
tester
.
pump
();
// Plain collapsed selection at the edge of first word. In iOS 12, the
// the first tap after a double tap ends up putting the cursor at where
// you tapped instead of the edge like every other single tap. This is
// likely a bug in iOS 12 and not present in other versions.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
7
,
affinity:
TextAffinity
.
upstream
),
);
// No toolbar.
expect
(
find
.
byType
(
CupertinoButton
),
findsNothing
);
},
);
testWidgets
(
'long press moves cursor to the exact long press position and shows toolbar (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
longPressAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
();
// Collapsed cursor for iOS long press.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
3
),
);
// Collapsed toolbar shows 2 buttons.
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
2
));
},
);
testWidgets
(
'long press selects word and shows toolbar (Android)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
longPressAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
();
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
0
,
extentOffset:
7
),
);
// Collapsed toolbar shows 3 buttons.
expect
(
find
.
byType
(
FlatButton
),
findsNWidgets
(
3
));
},
);
testWidgets
(
'long press tap is not a double tap (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
longPressAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
();
// We ended up moving the cursor to the edge of the same word and dismissed
// the toolbar.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
7
,
affinity:
TextAffinity
.
upstream
),
);
// Collapsed toolbar shows 2 buttons.
expect
(
find
.
byType
(
CupertinoButton
),
findsNothing
);
},
);
testWidgets
(
'long tap after a double tap select is not affected (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor to the beginning of the second word.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
8
,
affinity:
TextAffinity
.
downstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
await
tester
.
longPressAt
(
textfieldStart
+
const
Offset
(
100.0
,
5.0
));
await
tester
.
pump
();
// Plain collapsed selection at the exact tap position.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
6
),
);
// Long press toolbar.
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
2
));
},
);
testWidgets
(
'double tap after a long tap is not affected (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
longPressAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
8
,
affinity:
TextAffinity
.
downstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
();
// Double tap selection.
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
8
,
extentOffset:
12
),
);
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
},
);
testWidgets
(
'double tap chains work (iOS)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
'Atwater Peel Sherbrooke Bonaventure'
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(
platform:
TargetPlatform
.
iOS
),
home:
Material
(
child:
Center
(
child:
TextField
(
controller:
controller
,
),
),
),
),
);
final
Offset
textfieldStart
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
7
,
affinity:
TextAffinity
.
upstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
50.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
0
,
extentOffset:
7
),
);
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
// Double tap selecting the same word somewhere else is fine.
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
100.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
7
,
affinity:
TextAffinity
.
upstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
100.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
0
,
extentOffset:
7
),
);
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
// First tap moved the cursor.
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
8
,
affinity:
TextAffinity
.
downstream
),
);
await
tester
.
tapAt
(
textfieldStart
+
const
Offset
(
150.0
,
5.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
controller
.
selection
,
const
TextSelection
(
baseOffset:
8
,
extentOffset:
12
),
);
expect
(
find
.
byType
(
CupertinoButton
),
findsNWidgets
(
3
));
},
);
}
packages/flutter/test/widgets/text_selection_test.dart
0 → 100644
View file @
b3b764c9
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/widgets.dart'
;
void
main
(
)
{
int
tapCount
;
int
singleTapUpCount
;
int
singleTapCancelCount
;
int
singleLongTapDownCount
;
int
doubleTapDownCount
;
void
_handleTapDown
(
TapDownDetails
details
)
{
tapCount
++;
}
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
singleTapUpCount
++;
}
void
_handleSingleTapCancel
()
{
singleTapCancelCount
++;
}
void
_handleSingleLongTapDown
()
{
singleLongTapDownCount
++;
}
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
doubleTapDownCount
++;
}
setUp
(()
{
tapCount
=
0
;
singleTapUpCount
=
0
;
singleTapCancelCount
=
0
;
singleLongTapDownCount
=
0
;
doubleTapDownCount
=
0
;
});
Future
<
void
>
pumpGestureDetector
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
TextSelectionGestureDetector
(
behavior:
HitTestBehavior
.
opaque
,
onTapDown:
_handleTapDown
,
onSingleTapUp:
_handleSingleTapUp
,
onSingleTapCancel:
_handleSingleTapCancel
,
onSingleLongTapDown:
_handleSingleLongTapDown
,
onDoubleTapDown:
_handleDoubleTapDown
,
child:
Container
(),
),
);
}
testWidgets
(
'a series of taps all call onTaps'
,
(
WidgetTester
tester
)
async
{
await
pumpGestureDetector
(
tester
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
150
));
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
expect
(
tapCount
,
6
);
});
testWidgets
(
'in a series of rapid taps, onTapDown and onDoubleTapDown alternate'
,
(
WidgetTester
tester
)
async
{
await
pumpGestureDetector
(
tester
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
singleTapUpCount
,
1
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
singleTapUpCount
,
1
);
expect
(
doubleTapDownCount
,
1
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
singleTapUpCount
,
2
);
expect
(
doubleTapDownCount
,
1
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
singleTapUpCount
,
2
);
expect
(
doubleTapDownCount
,
2
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
singleTapUpCount
,
3
);
expect
(
doubleTapDownCount
,
2
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
expect
(
singleTapUpCount
,
3
);
expect
(
doubleTapDownCount
,
3
);
expect
(
tapCount
,
6
);
});
testWidgets
(
'quick tap-tap-hold is a double tap down'
,
(
WidgetTester
tester
)
async
{
await
pumpGestureDetector
(
tester
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
expect
(
singleTapUpCount
,
1
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
expect
(
singleTapUpCount
,
1
);
// Every down is counted.
expect
(
tapCount
,
2
);
// No cancels because the second tap of the double tap is a second successful
// single tap behind the scene.
expect
(
singleTapCancelCount
,
0
);
expect
(
doubleTapDownCount
,
1
);
// The double tap down hold supersedes the single tap down.
expect
(
singleLongTapDownCount
,
0
);
await
gesture
.
up
();
// Nothing else happens on up.
expect
(
singleTapUpCount
,
1
);
expect
(
tapCount
,
2
);
expect
(
singleTapCancelCount
,
0
);
expect
(
doubleTapDownCount
,
1
);
expect
(
singleLongTapDownCount
,
0
);
});
testWidgets
(
'a very quick swipe is just a canceled tap'
,
(
WidgetTester
tester
)
async
{
await
pumpGestureDetector
(
tester
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
20
));
await
gesture
.
moveBy
(
const
Offset
(
100
,
100
));
await
tester
.
pump
();
expect
(
singleTapUpCount
,
0
);
expect
(
tapCount
,
0
);
expect
(
singleTapCancelCount
,
1
);
expect
(
doubleTapDownCount
,
0
);
expect
(
singleLongTapDownCount
,
0
);
await
gesture
.
up
();
// Nothing else happens on up.
expect
(
singleTapUpCount
,
0
);
expect
(
tapCount
,
0
);
expect
(
singleTapCancelCount
,
1
);
expect
(
doubleTapDownCount
,
0
);
expect
(
singleLongTapDownCount
,
0
);
});
testWidgets
(
'a slower swipe has a tap down and a canceled tap'
,
(
WidgetTester
tester
)
async
{
await
pumpGestureDetector
(
tester
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200
,
200
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
120
));
await
gesture
.
moveBy
(
const
Offset
(
100
,
100
));
await
tester
.
pump
();
expect
(
singleTapUpCount
,
0
);
expect
(
tapCount
,
1
);
expect
(
singleTapCancelCount
,
1
);
expect
(
doubleTapDownCount
,
0
);
expect
(
singleLongTapDownCount
,
0
);
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment