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
Expand all
Show 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.
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:async'
;
import
'package:flutter/gestures.dart'
show
kDoubleTapTimeout
,
kDoubleTapSlop
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/widgets.dart'
;
...
@@ -412,13 +409,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
...
@@ -412,13 +409,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
FocusNode
_focusNode
;
FocusNode
_focusNode
;
FocusNode
get
_effectiveFocusNode
=>
widget
.
focusNode
??
(
_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
@override
void
initState
()
{
void
initState
()
{
super
.
initState
();
super
.
initState
();
...
@@ -448,7 +438,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
...
@@ -448,7 +438,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
void
dispose
()
{
void
dispose
()
{
_focusNode
?.
dispose
();
_focusNode
?.
dispose
();
_controller
?.
removeListener
(
updateKeepAlive
);
_controller
?.
removeListener
(
updateKeepAlive
);
_doubleTapTimer
?.
cancel
();
super
.
dispose
();
super
.
dispose
();
}
}
...
@@ -458,54 +447,21 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
...
@@ -458,54 +447,21 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
RenderEditable
get
_renderEditable
=>
_editableTextKey
.
currentState
.
renderEditable
;
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
)
{
void
_handleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
handleTapDown
(
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
)
{
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
if
(!
_isDoubleTap
)
{
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
_lastTapOffset
=
details
.
globalPosition
;
_doubleTapTimer
=
Timer
(
kDoubleTapTimeout
,
_doubleTapTimeout
);
_requestKeyboard
();
_requestKeyboard
();
}
}
_isDoubleTap
=
false
;
}
void
_handleLongPress
()
{
void
_handleSingleLongTapDown
()
{
if
(!
_isDoubleTap
)
{
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
longPress
);
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
longPress
);
}
}
_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
;
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
return
difference
.
distance
<=
kDoubleTapSlop
;
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleTap
)
;
}
}
@override
@override
...
@@ -690,12 +646,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
...
@@ -690,12 +646,12 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
:
CupertinoTheme
.
of
(
context
).
brightness
==
Brightness
.
light
:
CupertinoTheme
.
of
(
context
).
brightness
==
Brightness
.
light
?
_kDisabledBackground
?
_kDisabledBackground
:
CupertinoColors
.
darkBackgroundGray
,
:
CupertinoColors
.
darkBackgroundGray
,
child:
GestureDetector
(
child:
TextSelectionGestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
onTapDown:
_handleTapDown
,
onTapDown:
_handleTapDown
,
onTapUp:
_handleTapUp
,
onSingleTapUp:
_handleSingleTapUp
,
onLongPress:
_handleLongPress
,
onSingleLongTapDown:
_handleSingleLongTapDown
,
excludeFromSemantics:
true
,
onDoubleTapDown:
_handleDoubleTapDown
,
behavior:
HitTestBehavior
.
translucent
,
child:
_addTextDependentAttachments
(
paddedEditable
,
textStyle
),
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
...
@@ -411,8 +411,9 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
InputDecoration
_getEffectiveDecoration
()
{
InputDecoration
_getEffectiveDecoration
()
{
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
InputDecoration
effectiveDecoration
=
(
widget
.
decoration
??
const
InputDecoration
())
final
InputDecoration
effectiveDecoration
=
(
widget
.
decoration
??
const
InputDecoration
())
.
applyDefaults
(
Theme
.
of
(
context
)
.
inputDecorationTheme
)
.
applyDefaults
(
themeData
.
inputDecorationTheme
)
.
copyWith
(
.
copyWith
(
enabled:
widget
.
enabled
,
enabled:
widget
.
enabled
,
hintMaxLines:
widget
.
decoration
?.
hintMaxLines
??
widget
.
maxLines
hintMaxLines:
widget
.
decoration
?.
hintMaxLines
??
widget
.
maxLines
...
@@ -434,7 +435,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
...
@@ -434,7 +435,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
// Handle length exceeds maxLength
// Handle length exceeds maxLength
if
(
_effectiveController
.
value
.
text
.
runes
.
length
>
widget
.
maxLength
)
{
if
(
_effectiveController
.
value
.
text
.
runes
.
length
>
widget
.
maxLength
)
{
final
ThemeData
themeData
=
Theme
.
of
(
context
);
return
effectiveDecoration
.
copyWith
(
return
effectiveDecoration
.
copyWith
(
errorText:
effectiveDecoration
.
errorText
??
''
,
errorText:
effectiveDecoration
.
errorText
??
''
,
counterStyle:
effectiveDecoration
.
errorStyle
counterStyle:
effectiveDecoration
.
errorStyle
...
@@ -489,10 +489,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
...
@@ -489,10 +489,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
InteractiveInkFeature
_createInkFeature
(
TapDownDetails
details
)
{
InteractiveInkFeature
_createInkFeature
(
TapDownDetails
details
)
{
final
MaterialInkController
inkController
=
Material
.
of
(
context
);
final
MaterialInkController
inkController
=
Material
.
of
(
context
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
BuildContext
editableContext
=
_editableTextKey
.
currentContext
;
final
BuildContext
editableContext
=
_editableTextKey
.
currentContext
;
final
RenderBox
referenceBox
=
InputDecorator
.
containerOf
(
editableContext
)
??
editableContext
.
findRenderObject
();
final
RenderBox
referenceBox
=
InputDecorator
.
containerOf
(
editableContext
)
??
editableContext
.
findRenderObject
();
final
Offset
position
=
referenceBox
.
globalToLocal
(
details
.
globalPosition
);
final
Offset
position
=
referenceBox
.
globalToLocal
(
details
.
globalPosition
);
final
Color
color
=
Theme
.
of
(
context
)
.
splashColor
;
final
Color
color
=
themeData
.
splashColor
;
InteractiveInkFeature
splash
;
InteractiveInkFeature
splash
;
void
handleRemoved
()
{
void
handleRemoved
()
{
...
@@ -505,7 +506,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
...
@@ -505,7 +506,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
}
// else we're probably in deactivate()
}
// else we're probably in deactivate()
}
}
splash
=
Theme
.
of
(
context
)
.
splashFactory
.
create
(
splash
=
themeData
.
splashFactory
.
create
(
controller:
inkController
,
controller:
inkController
,
referenceBox:
referenceBox
,
referenceBox:
referenceBox
,
position:
position
,
position:
position
,
...
@@ -527,25 +528,47 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
...
@@ -527,25 +528,47 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
_startSplash
(
details
);
_startSplash
(
details
);
}
}
void
_handleTap
()
{
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
if
(
widget
.
selectionEnabled
)
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
handleTap
();
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
();
_requestKeyboard
();
_confirmCurrentSplash
();
_confirmCurrentSplash
();
if
(
widget
.
onTap
!=
null
)
if
(
widget
.
onTap
!=
null
)
widget
.
onTap
();
widget
.
onTap
();
}
}
void
_handleTapCancel
()
{
void
_handle
Single
TapCancel
()
{
_cancelCurrentSplash
();
_cancelCurrentSplash
();
}
}
void
_handleLongPress
()
{
void
_handleSingleLongTapDown
()
{
if
(
widget
.
selectionEnabled
)
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
handleLongPress
();
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
();
_confirmCurrentSplash
();
}
}
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleTap
);
}
void
_startSplash
(
TapDownDetails
details
)
{
void
_startSplash
(
TapDownDetails
details
)
{
if
(
_effectiveFocusNode
.
hasFocus
)
if
(
_effectiveFocusNode
.
hasFocus
)
return
;
return
;
...
@@ -632,7 +655,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
...
@@ -632,7 +655,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
rendererIgnoresPointer:
true
,
rendererIgnoresPointer:
true
,
cursorWidth:
widget
.
cursorWidth
,
cursorWidth:
widget
.
cursorWidth
,
cursorRadius:
widget
.
cursorRadius
,
cursorRadius:
widget
.
cursorRadius
,
cursorColor:
widget
.
cursorColor
??
Theme
.
of
(
context
)
.
cursorColor
,
cursorColor:
widget
.
cursorColor
??
themeData
.
cursorColor
,
backgroundCursorColor:
CupertinoColors
.
inactiveGray
,
backgroundCursorColor:
CupertinoColors
.
inactiveGray
,
scrollPadding:
widget
.
scrollPadding
,
scrollPadding:
widget
.
scrollPadding
,
keyboardAppearance:
keyboardAppearance
,
keyboardAppearance:
keyboardAppearance
,
...
@@ -665,13 +688,13 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
...
@@ -665,13 +688,13 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
},
},
child:
IgnorePointer
(
child:
IgnorePointer
(
ignoring:
!(
widget
.
enabled
??
widget
.
decoration
?.
enabled
??
true
),
ignoring:
!(
widget
.
enabled
??
widget
.
decoration
?.
enabled
??
true
),
child:
GestureDetector
(
child:
TextSelectionGestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
onTapDown:
_handleTapDown
,
onTapDown:
_handleTapDown
,
onTap:
_handleTap
,
onSingleTapUp:
_handleSingleTapUp
,
onTapCancel:
_handleTapCancel
,
onSingleTapCancel:
_handleSingleTapCancel
,
onLongPress:
_handleLongPress
,
onSingleLongTapDown:
_handleSingleLongTapDown
,
excludeFromSemantics:
true
,
onDoubleTapDown:
_handleDoubleTapDown
,
behavior:
HitTestBehavior
.
translucent
,
child:
child
,
child:
child
,
),
),
),
),
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
b3b764c9
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
import
'dart:async'
;
import
'dart:async'
;
import
'package:flutter/gestures.dart'
show
kDoubleTapTimeout
,
kDoubleTapSlop
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/scheduler.dart'
;
...
@@ -568,3 +569,159 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
...
@@ -568,3 +569,159 @@ class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay
return
null
;
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
This diff is collapsed.
Click to expand it.
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