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
e31f7089
Unverified
Commit
e31f7089
authored
Apr 20, 2020
by
LongCatIsLooong
Committed by
GitHub
Apr 20, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Autofill Part 1 (#52126)
parent
b7e30cfc
Changes
12
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
1605 additions
and
30 deletions
+1605
-30
services.dart
packages/flutter/lib/services.dart
+1
-0
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+5
-0
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+5
-0
autofill.dart
packages/flutter/lib/src/services/autofill.dart
+811
-0
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+57
-3
autofill.dart
packages/flutter/lib/src/widgets/autofill.dart
+227
-0
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+78
-25
widgets.dart
packages/flutter/lib/widgets.dart
+1
-0
autofill_test.dart
packages/flutter/test/services/autofill_test.dart
+238
-0
text_input_test.dart
packages/flutter/test/services/text_input_test.dart
+3
-0
autofill_group_test.dart
packages/flutter/test/widgets/autofill_group_test.dart
+166
-0
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+13
-2
No files found.
packages/flutter/lib/services.dart
View file @
e31f7089
...
...
@@ -11,6 +11,7 @@
library
services
;
export
'src/services/asset_bundle.dart'
;
export
'src/services/autofill.dart'
;
export
'src/services/binary_messenger.dart'
;
export
'src/services/binding.dart'
;
export
'src/services/clipboard.dart'
;
...
...
packages/flutter/lib/src/cupertino/text_field.dart
View file @
e31f7089
...
...
@@ -268,6 +268,7 @@ class CupertinoTextField extends StatefulWidget {
this
.
onTap
,
this
.
scrollController
,
this
.
scrollPhysics
,
this
.
autofillHints
,
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
autofocus
!=
null
),
...
...
@@ -579,6 +580,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.material.textfield.onTap}
final
GestureTapCallback
onTap
;
/// {@macro flutter.widgets.editableText.autofillHints}
final
Iterable
<
String
>
autofillHints
;
@override
_CupertinoTextFieldState
createState
()
=>
_CupertinoTextFieldState
();
...
...
@@ -950,6 +954,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
scrollController:
widget
.
scrollController
,
scrollPhysics:
widget
.
scrollPhysics
,
enableInteractiveSelection:
widget
.
enableInteractiveSelection
,
autofillHints:
widget
.
autofillHints
,
),
),
);
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
e31f7089
...
...
@@ -346,6 +346,7 @@ class TextField extends StatefulWidget {
this
.
buildCounter
,
this
.
scrollController
,
this
.
scrollPhysics
,
this
.
autofillHints
,
})
:
assert
(
textAlign
!=
null
),
assert
(
readOnly
!=
null
),
assert
(
autofocus
!=
null
),
...
...
@@ -710,6 +711,9 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.scrollController}
final
ScrollController
scrollController
;
/// {@macro flutter.widgets.editableText.autofillHints}
final
Iterable
<
String
>
autofillHints
;
@override
_TextFieldState
createState
()
=>
_TextFieldState
();
...
...
@@ -1049,6 +1053,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
dragStartBehavior:
widget
.
dragStartBehavior
,
scrollController:
widget
.
scrollController
,
scrollPhysics:
widget
.
scrollPhysics
,
autofillHints:
widget
.
autofillHints
,
autocorrectionTextRectColor:
autocorrectionTextRectColor
,
),
);
...
...
packages/flutter/lib/src/services/autofill.dart
0 → 100644
View file @
e31f7089
This diff is collapsed.
Click to expand it.
packages/flutter/lib/src/services/text_input.dart
View file @
e31f7089
...
...
@@ -16,6 +16,7 @@ import 'dart:ui' show
import
'package:flutter/foundation.dart'
;
import
'package:vector_math/vector_math_64.dart'
show
Matrix4
;
import
'autofill.dart'
;
import
'message_codec.dart'
;
import
'platform_channel.dart'
;
import
'system_channels.dart'
;
...
...
@@ -441,6 +442,7 @@ class TextInputConfiguration {
this
.
inputAction
=
TextInputAction
.
done
,
this
.
keyboardAppearance
=
Brightness
.
light
,
this
.
textCapitalization
=
TextCapitalization
.
none
,
this
.
autofillConfiguration
,
})
:
assert
(
inputType
!=
null
),
assert
(
obscureText
!=
null
),
smartDashesType
=
smartDashesType
??
(
obscureText
?
SmartDashesType
.
disabled
:
SmartDashesType
.
enabled
),
...
...
@@ -464,6 +466,14 @@ class TextInputConfiguration {
/// Defaults to true.
final
bool
autocorrect
;
/// The configuration to use for autofill.
///
/// Defaults to null, in which case no autofill information will be provided
/// to the platform. This will prevent the corresponding input field from
/// participating in autofills triggered by other fields. Additionally, on
/// Android and web, setting [autofillConfiguration] to null disables autofill.
final
AutofillConfiguration
autofillConfiguration
;
/// {@template flutter.services.textInput.smartDashesType}
/// Whether to allow the platform to automatically format dashes.
///
...
...
@@ -565,6 +575,7 @@ class TextInputConfiguration {
'inputAction'
:
inputAction
.
toString
(),
'textCapitalization'
:
textCapitalization
.
toString
(),
'keyboardAppearance'
:
keyboardAppearance
.
toString
(),
if
(
autofillConfiguration
!=
null
)
'autofill'
:
autofillConfiguration
.
toJson
(),
};
}
}
...
...
@@ -745,6 +756,21 @@ abstract class TextInputClient {
/// const constructors so that they can be used in const expressions.
const
TextInputClient
();
/// The current state of the [TextEditingValue] held by this client.
TextEditingValue
get
currentTextEditingValue
;
/// The [AutofillScope] this [TextInputClient] belongs to, if any.
///
/// It should return null if this [TextInputClient] does not need autofill
/// support. For a [TextInputClient] that supports autofill, returning null
/// causes it to participate in autofill alone.
///
/// See also:
///
/// * [AutofillGroup], a widget that creates an [AutofillScope] for its
/// descendent autofillable [TextInputClient]s.
AutofillScope
get
currentAutofillScope
;
/// Requests that this client update its editing state to the given value.
void
updateEditingValue
(
TextEditingValue
value
);
...
...
@@ -754,9 +780,6 @@ abstract class TextInputClient {
/// Updates the floating cursor position and state.
void
updateFloatingCursor
(
RawFloatingCursorPoint
point
);
/// The current state of the [TextEditingValue] held by this client.
TextEditingValue
get
currentTextEditingValue
;
/// Requests that this client display a prompt rectangle for the given text range,
/// to indicate the range of text that will be changed by a pending autocorrection.
///
...
...
@@ -809,6 +832,17 @@ class TextInputConnection {
TextInput
.
_instance
.
_show
();
}
/// Requests the platform autofill UI to appear.
///
/// The call has no effect unless the currently attached client supports
/// autofill, and the platform has a standalone autofill UI (for example, this
/// call has no effect on iOS since its autofill UI is part of the software
/// keyboard).
void
requestAutofill
()
{
assert
(
attached
);
TextInput
.
_instance
.
_requestAutofill
();
}
/// Requests that the text input control change its internal state to match the given state.
void
setEditingState
(
TextEditingValue
value
)
{
assert
(
attached
);
...
...
@@ -1065,6 +1099,22 @@ class TextInput {
}
final
List
<
dynamic
>
args
=
methodCall
.
arguments
as
List
<
dynamic
>;
if
(
method
==
'TextInputClient.updateEditingStateWithTag'
)
{
final
TextInputClient
client
=
_currentConnection
.
_client
;
assert
(
client
!=
null
);
final
AutofillScope
scope
=
client
.
currentAutofillScope
;
final
Map
<
String
,
dynamic
>
editingValue
=
args
[
1
]
as
Map
<
String
,
dynamic
>;
for
(
final
String
tag
in
editingValue
.
keys
)
{
final
TextEditingValue
textEditingValue
=
TextEditingValue
.
fromJSON
(
editingValue
[
tag
]
as
Map
<
String
,
dynamic
>,
);
scope
?.
getAutofillClient
(
tag
)?.
updateEditingValue
(
textEditingValue
);
}
return
;
}
final
int
client
=
args
[
0
]
as
int
;
// The incoming message was for a different client.
if
(
client
!=
_currentConnection
.
_id
)
...
...
@@ -1128,6 +1178,10 @@ class TextInput {
_channel
.
invokeMethod
<
void
>(
'TextInput.show'
);
}
void
_requestAutofill
()
{
_channel
.
invokeMethod
<
void
>(
'TextInput.requestAutofill'
);
}
void
_setEditableSizeAndTransform
(
Map
<
String
,
dynamic
>
args
)
{
_channel
.
invokeMethod
<
void
>(
'TextInput.setEditableSizeAndTransform'
,
...
...
packages/flutter/lib/src/widgets/autofill.dart
0 → 100644
View file @
e31f7089
// Copyright 2014 The Flutter 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/services.dart'
;
import
'framework.dart'
;
export
'package:flutter/services.dart'
show
AutofillHints
;
/// An [AutofillScope] widget that groups [AutofillClient]s together.
///
/// [AutofillClient]s within the same [AutofillScope] must be built together, and
/// they be will be autofilled together.
///
/// {@macro flutter.services.autofill.AutofillScope}
///
/// The [AutofillGroup] widget only knows about [AutofillClient]s registered to
/// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup]
/// will not pick up [AutofillClient]s that are not mounted, for example, an
/// [AutofillClient] within a [Scrollable] that has never been scrolled into the
/// viewport. To workaround this problem, ensure clients in the same [AutofillGroup]
/// are built together:
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// An example form with autofillable fields grouped into different `AutofillGroup`s.
///
/// ```dart
/// bool isSameAddress = true;
/// final TextEditingController shippingAddress1 = TextEditingController();
/// final TextEditingController shippingAddress2 = TextEditingController();
/// final TextEditingController billingAddress1 = TextEditingController();
/// final TextEditingController billingAddress2 = TextEditingController();
///
/// final TextEditingController creditCardNumber = TextEditingController();
/// final TextEditingController creditCardSecurityCode = TextEditingController();
///
/// final TextEditingController phoneNumber = TextEditingController();
///
/// @override
/// Widget build(BuildContext context) {
/// return ListView(
/// children: <Widget>[
/// const Text('Shipping address'),
/// // The address fields are grouped together as some platforms are capable
/// // of autofilling all these fields in one go.
/// AutofillGroup(
/// child: Column(
/// children: <Widget>[
/// TextField(
/// controller: shippingAddress1,
/// autofillHints: <String>[AutofillHints.streetAddressLine1],
/// ),
/// TextField(
/// controller: shippingAddress2,
/// autofillHints: <String>[AutofillHints.streetAddressLine2],
/// ),
/// ],
/// ),
/// ),
/// const Text('Billing address'),
/// Checkbox(
/// value: isSameAddress,
/// onChanged: (bool newValue) {
/// setState(() { isSameAddress = newValue; });
/// },
/// ),
/// // Again the address fields are grouped together for the same reason.
/// if (!isSameAddress) AutofillGroup(
/// child: Column(
/// children: <Widget>[
/// TextField(
/// controller: billingAddress1,
/// autofillHints: <String>[AutofillHints.streetAddressLine1],
/// ),
/// TextField(
/// controller: billingAddress2,
/// autofillHints: <String>[AutofillHints.streetAddressLine2],
/// ),
/// ],
/// ),
/// ),
/// const Text('Credit Card Information'),
/// // The credit card number and the security code are grouped together as
/// // some platforms are capable of autofilling both fields.
/// AutofillGroup(
/// child: Column(
/// children: <Widget>[
/// TextField(
/// controller: creditCardNumber,
/// autofillHints: <String>[AutofillHints.creditCardNumber],
/// ),
/// TextField(
/// controller: creditCardSecurityCode,
/// autofillHints: <String>[AutofillHints.creditCardSecurityCode],
/// ),
/// ],
/// ),
/// ),
/// const Text('Contact Phone Number'),
/// // The phone number field can still be autofilled despite lacking an
/// // `AutofillScope`.
/// TextField(
/// controller: phoneNumber,
/// autofillHints: <String>[AutofillHints.telephoneNumber],
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
class
AutofillGroup
extends
StatefulWidget
{
/// Creates a scope for autofillable input fields.
///
/// The [child] argument must not be null.
const
AutofillGroup
({
Key
key
,
@required
this
.
child
,
})
:
assert
(
child
!=
null
),
super
(
key:
key
);
/// Returns the closest [AutofillGroupState] which encloses the given context.
///
/// {@macro flutter.widgets.autofill.AutofillGroupState}
///
/// See also:
///
/// * [EditableTextState], where this method is used to retrive the closest
/// [AutofillGroupState].
static
AutofillGroupState
of
(
BuildContext
context
)
{
final
_AutofillScope
scope
=
context
.
dependOnInheritedWidgetOfExactType
<
_AutofillScope
>();
return
scope
?.
_scope
;
}
/// {@macro flutter.widgets.child}
final
Widget
child
;
@override
AutofillGroupState
createState
()
=>
AutofillGroupState
();
}
/// State associated with an [AutofillGroup] widget.
///
/// {@template flutter.widgets.autofill.AutofillGroupState}
/// An [AutofillGroupState] can be used to register an [AutofillClient] when it
/// enters this [AutofillGroup] (for example, when an [EditableText] is mounted or
/// reparented onto the [AutofillGroup]'s subtree), and unregister an
/// [AutofillClient] when it exits (for example, when an [EditableText] gets
/// unmounted or reparented out of the [AutofillGroup]'s subtree).
///
/// The [AutofillGroupState] class also provides an [attach] method that can be
/// called by [TextInputClient]s that support autofill, instead of
/// [TextInputClient.attach], to create a [TextInputConnection] to interact with
/// the platform's text input system.
/// {@endtemplate}
///
/// Typically obtained using [AutofillGroup.of].
class
AutofillGroupState
extends
State
<
AutofillGroup
>
with
AutofillScopeMixin
{
final
Map
<
String
,
AutofillClient
>
_clients
=
<
String
,
AutofillClient
>{};
@override
AutofillClient
getAutofillClient
(
String
tag
)
=>
_clients
[
tag
];
@override
Iterable
<
AutofillClient
>
get
autofillClients
{
return
_clients
.
values
.
where
((
AutofillClient
client
)
=>
client
?.
textInputConfiguration
?.
autofillConfiguration
!=
null
);
}
/// Adds the [AutofillClient] to this [AutofillGroup].
///
/// Typically, this is called by [TextInputClient]s that support autofill (for
/// example, [EditableTextState]) in [State.didChangeDependencies], when the
/// input field should be registered to a new [AutofillGroup].
///
/// See also:
///
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to update the current [AutofillScope] when needed.
void
register
(
AutofillClient
client
)
{
assert
(
client
!=
null
);
_clients
.
putIfAbsent
(
client
.
autofillId
,
()
=>
client
);
}
/// Removes an [AutofillClient] with the given [autofillId] from this
/// [AutofillGroup].
///
/// Typically, this should be called by autofillable [TextInputClient]s in
/// [State.dispose] and [State.didChangeDependencies], when the input field
/// needs to be removed from the [AutofillGroup] it is currently registered to.
///
/// See also:
///
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to unregister from the previous [AutofillScope].
/// * [EditableTextState.dispose], where this method is called to unregister
/// from the current [AutofillScope] when the widget is about to be removed
/// from the tree.
void
unregister
(
String
autofillId
)
{
assert
(
autofillId
!=
null
&&
_clients
.
containsKey
(
autofillId
));
_clients
.
remove
(
autofillId
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
_AutofillScope
(
autofillScopeState:
this
,
child:
widget
.
child
,
);
}
}
class
_AutofillScope
extends
InheritedWidget
{
const
_AutofillScope
({
Key
key
,
Widget
child
,
AutofillGroupState
autofillScopeState
,
})
:
_scope
=
autofillScopeState
,
super
(
key:
key
,
child:
child
);
final
AutofillGroupState
_scope
;
AutofillGroup
get
client
=>
_scope
.
widget
;
@override
bool
updateShouldNotify
(
_AutofillScope
old
)
=>
_scope
!=
old
.
_scope
;
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
e31f7089
...
...
@@ -7,12 +7,13 @@ import 'dart:math' as math;
import
'dart:ui'
as
ui
hide
TextStyle
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
import
'package:flutter/painting.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/scheduler.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/gestures.dart'
show
DragStartBehavior
;
import
'autofill.dart'
;
import
'automatic_keep_alive.dart'
;
import
'basic.dart'
;
import
'binding.dart'
;
...
...
@@ -29,8 +30,8 @@ import 'scrollable.dart';
import
'text_selection.dart'
;
import
'ticker_provider.dart'
;
export
'package:flutter/services.dart'
show
TextEditingValue
,
TextSelection
,
TextInputType
,
SmartQuotesType
,
SmartDashesType
;
export
'package:flutter/rendering.dart'
show
SelectionChangedCause
;
export
'package:flutter/services.dart'
show
TextEditingValue
,
TextSelection
,
TextInputType
,
SmartQuotesType
,
SmartDashesType
;
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
...
...
@@ -409,6 +410,7 @@ class EditableText extends StatefulWidget {
paste:
true
,
selectAll:
true
,
),
this
.
autofillHints
,
})
:
assert
(
controller
!=
null
),
assert
(
focusNode
!=
null
),
assert
(
obscureText
!=
null
),
...
...
@@ -1079,6 +1081,23 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.rendering.editable.selectionEnabled}
bool
get
selectionEnabled
=>
enableInteractiveSelection
;
/// {@template flutter.widgets.editableText.autofillHints}
/// A list of strings that helps the autofill service identify the type of this
/// text input.
///
/// When set to null or empty, the text input will not send any autofill related
/// information to the platform. As a result, it will not participate in
/// autofills triggered by a different [AutofillClient], even if they're in the
/// same [AutofillScope]. Additionally, on Android and web, setting this to null
/// or empty will disable autofill for this text field.
///
/// The minimum platform SDK version that supports Autofill is API level 26
/// for Android, and iOS 10.0 for iOS.
///
/// {@macro flutter.services.autofill.autofillHints}
/// {@endtemplate}
final
Iterable
<
String
>
autofillHints
;
@override
EditableTextState
createState
()
=>
EditableTextState
();
...
...
@@ -1104,11 +1123,12 @@ class EditableText extends StatefulWidget {
properties
.
add
(
DiagnosticsProperty
<
TextInputType
>(
'keyboardType'
,
keyboardType
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollController
>(
'scrollController'
,
scrollController
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
ScrollPhysics
>(
'scrollPhysics'
,
scrollPhysics
,
defaultValue:
null
));
properties
.
add
(
DiagnosticsProperty
<
Iterable
<
String
>>(
'autofillHints'
,
autofillHints
,
defaultValue:
null
));
}
}
/// State for a [EditableText].
class
EditableTextState
extends
State
<
EditableText
>
with
AutomaticKeepAliveClientMixin
<
EditableText
>,
WidgetsBindingObserver
,
TickerProviderStateMixin
<
EditableText
>
implements
Text
InputClient
,
TextSelectionDelegate
{
class
EditableTextState
extends
State
<
EditableText
>
with
AutomaticKeepAliveClientMixin
<
EditableText
>,
WidgetsBindingObserver
,
TickerProviderStateMixin
<
EditableText
>
implements
Text
SelectionDelegate
,
TextInputClient
,
AutofillClient
{
Timer
_cursorTimer
;
bool
_targetCursorVisibility
=
false
;
final
ValueNotifier
<
bool
>
_cursorVisibilityNotifier
=
ValueNotifier
<
bool
>(
true
);
...
...
@@ -1128,6 +1148,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
bool
_didAutoFocus
=
false
;
FocusAttachment
_focusAttachment
;
AutofillGroupState
_currentAutofillScope
;
@override
AutofillScope
get
currentAutofillScope
=>
_currentAutofillScope
;
// This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out.
static
const
Duration
_fadeDuration
=
Duration
(
milliseconds:
250
);
...
...
@@ -1175,6 +1199,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
final
AutofillGroupState
newAutofillGroup
=
AutofillGroup
.
of
(
context
);
if
(
currentAutofillScope
!=
newAutofillGroup
)
{
_currentAutofillScope
?.
unregister
(
autofillId
);
_currentAutofillScope
=
newAutofillGroup
;
newAutofillGroup
?.
register
(
this
);
}
if
(!
_didAutoFocus
&&
widget
.
autofocus
)
{
_didAutoFocus
=
true
;
SchedulerBinding
.
instance
.
addPostFrameCallback
((
_
)
{
...
...
@@ -1210,6 +1242,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
oldWidget
.
readOnly
&&
_hasFocus
)
_openInputConnection
();
}
if
(
widget
.
style
!=
oldWidget
.
style
)
{
final
TextStyle
style
=
widget
.
style
;
// The _textInputConnection will pick up the new style when it attaches in
...
...
@@ -1228,6 +1261,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void
dispose
()
{
_currentAutofillScope
?.
unregister
(
autofillId
);
widget
.
controller
.
removeListener
(
_didChangeTextEditingValue
);
_cursorBlinkOpacityController
.
removeListener
(
_onCursorColorTick
);
_floatingCursorResetController
.
removeListener
(
_onFloatingCursorResetTick
);
...
...
@@ -1278,11 +1312,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_formatAndSetValue
(
value
);
if
(
_hasInputConnection
)
{
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer
(
resetCharTicks:
false
);
_startCursorTimer
();
}
}
@override
void
performAction
(
TextInputAction
action
)
{
...
...
@@ -1465,26 +1501,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(!
_hasInputConnection
)
{
final
TextEditingValue
localValue
=
_value
;
_lastFormattedUnmodifiedTextEditingValue
=
localValue
;
_textInputConnection
=
TextInput
.
attach
(
this
,
TextInputConfiguration
(
inputType:
widget
.
keyboardType
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
smartDashesType:
widget
.
smartDashesType
??
(
widget
.
obscureText
?
SmartDashesType
.
disabled
:
SmartDashesType
.
enabled
),
smartQuotesType:
widget
.
smartQuotesType
??
(
widget
.
obscureText
?
SmartQuotesType
.
disabled
:
SmartQuotesType
.
enabled
),
enableSuggestions:
widget
.
enableSuggestions
,
inputAction:
widget
.
textInputAction
??
(
widget
.
keyboardType
==
TextInputType
.
multiline
?
TextInputAction
.
newline
:
TextInputAction
.
done
),
textCapitalization:
widget
.
textCapitalization
,
keyboardAppearance:
widget
.
keyboardAppearance
,
),
);
_textInputConnection
.
show
();
_textInputConnection
=
(
widget
.
autofillHints
?.
isNotEmpty
??
false
)
&&
currentAutofillScope
!=
null
?
currentAutofillScope
.
attach
(
this
,
textInputConfiguration
)
:
TextInput
.
attach
(
this
,
textInputConfiguration
);
_textInputConnection
.
show
();
_updateSizeAndTransform
();
// Request autofill AFTER the size and the transform have been sent to the
// platform side.
_textInputConnection
.
requestAutofill
();
final
TextStyle
style
=
widget
.
style
;
_textInputConnection
..
setStyle
(
...
...
@@ -1904,6 +1930,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
@override
String
get
autofillId
=>
'EditableText-
$hashCode
'
;
@override
TextInputConfiguration
get
textInputConfiguration
{
final
bool
isAutofillEnabled
=
widget
.
autofillHints
?.
isNotEmpty
??
false
;
return
TextInputConfiguration
(
inputType:
widget
.
keyboardType
,
obscureText:
widget
.
obscureText
,
autocorrect:
widget
.
autocorrect
,
smartDashesType:
widget
.
smartDashesType
??
(
widget
.
obscureText
?
SmartDashesType
.
disabled
:
SmartDashesType
.
enabled
),
smartQuotesType:
widget
.
smartQuotesType
??
(
widget
.
obscureText
?
SmartQuotesType
.
disabled
:
SmartQuotesType
.
enabled
),
enableSuggestions:
widget
.
enableSuggestions
,
inputAction:
widget
.
textInputAction
??
(
widget
.
keyboardType
==
TextInputType
.
multiline
?
TextInputAction
.
newline
:
TextInputAction
.
done
),
textCapitalization:
widget
.
textCapitalization
,
keyboardAppearance:
widget
.
keyboardAppearance
,
autofillConfiguration:
!
isAutofillEnabled
?
null
:
AutofillConfiguration
(
uniqueIdentifier:
autofillId
,
autofillHints:
widget
.
autofillHints
.
toList
(
growable:
false
),
currentEditingValue:
currentTextEditingValue
,
),
);
}
// null if no promptRect should be shown.
TextRange
_currentPromptRectRange
;
...
...
packages/flutter/lib/widgets.dart
View file @
e31f7089
...
...
@@ -22,6 +22,7 @@ export 'src/widgets/animated_switcher.dart';
export
'src/widgets/annotated_region.dart'
;
export
'src/widgets/app.dart'
;
export
'src/widgets/async.dart'
;
export
'src/widgets/autofill.dart'
;
export
'src/widgets/automatic_keep_alive.dart'
;
export
'src/widgets/banner.dart'
;
export
'src/widgets/basic.dart'
;
...
...
packages/flutter/test/services/autofill_test.dart
0 → 100644
View file @
e31f7089
// Copyright 2014 The Flutter 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:convert'
show
utf8
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
TestWidgetsFlutterBinding
.
ensureInitialized
();
group
(
'TextInput message channels'
,
()
{
FakeTextChannel
fakeTextChannel
;
FakeAutofillScope
scope
;
setUp
(()
{
fakeTextChannel
=
FakeTextChannel
((
MethodCall
call
)
async
{});
TextInput
.
setChannel
(
fakeTextChannel
);
scope
??=
FakeAutofillScope
();
scope
.
clients
.
clear
();
});
tearDown
(()
{
TextInputConnection
.
debugResetId
();
TextInput
.
setChannel
(
SystemChannels
.
textInput
);
});
test
(
'mandatory fields are mandatory'
,
()
async
{
AutofillConfiguration
config
;
try
{
config
=
AutofillConfiguration
(
uniqueIdentifier:
null
,
autofillHints:
const
<
String
>[
'test'
],
);
}
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'uniqueIdentifier != null'
));
}
expect
(
config
,
isNull
);
try
{
config
=
AutofillConfiguration
(
uniqueIdentifier:
'id'
,
autofillHints:
null
,
);
}
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'autofillHints != null'
));
}
expect
(
config
,
isNull
);
});
test
(
'throws if the hint list is empty'
,
()
async
{
Map
<
String
,
dynamic
>
json
;
try
{
const
AutofillConfiguration
config
=
AutofillConfiguration
(
uniqueIdentifier:
'id'
,
autofillHints:
<
String
>[],
);
json
=
config
.
toJson
();
}
catch
(
e
)
{
expect
(
e
.
toString
(),
contains
(
'isNotEmpty'
));
}
expect
(
json
,
isNull
);
});
test
(
'AutofillClients send the correct configuration to the platform'
'and responds to updateEditingStateWithTag method correctly'
,
()
async
{
final
FakeAutofillClient
client1
=
FakeAutofillClient
(
const
TextEditingValue
(
text:
'test1'
));
final
FakeAutofillClient
client2
=
FakeAutofillClient
(
const
TextEditingValue
(
text:
'test2'
));
client1
.
textInputConfiguration
=
TextInputConfiguration
(
autofillConfiguration:
AutofillConfiguration
(
uniqueIdentifier:
client1
.
autofillId
,
autofillHints:
const
<
String
>[
'client1'
],
currentEditingValue:
client1
.
currentTextEditingValue
,
),
);
client2
.
textInputConfiguration
=
TextInputConfiguration
(
autofillConfiguration:
AutofillConfiguration
(
uniqueIdentifier:
client2
.
autofillId
,
autofillHints:
const
<
String
>[
'client2'
],
currentEditingValue:
client2
.
currentTextEditingValue
,
),
);
scope
.
register
(
client1
);
scope
.
register
(
client2
);
client1
.
currentAutofillScope
=
scope
;
client2
.
currentAutofillScope
=
scope
;
scope
.
attach
(
client1
,
client1
.
textInputConfiguration
);
final
Map
<
String
,
dynamic
>
expectedConfiguration
=
client1
.
textInputConfiguration
.
toJson
();
expectedConfiguration
[
'fields'
]
=
<
Map
<
String
,
dynamic
>>[
client1
.
textInputConfiguration
.
toJson
(),
client2
.
textInputConfiguration
.
toJson
(),
];
fakeTextChannel
.
validateOutgoingMethodCalls
(<
MethodCall
>[
MethodCall
(
'TextInput.setClient'
,
<
dynamic
>[
1
,
expectedConfiguration
]),
]);
const
TextEditingValue
text2
=
TextEditingValue
(
text:
'Text 2'
);
fakeTextChannel
.
incoming
(
MethodCall
(
'TextInputClient.updateEditingStateWithTag'
,
<
dynamic
>[
0
,
<
String
,
dynamic
>{
client2
.
autofillId
:
text2
.
toJSON
()
}],
));
expect
(
client2
.
currentTextEditingValue
,
text2
);
});
});
}
class
FakeAutofillClient
implements
TextInputClient
,
AutofillClient
{
FakeAutofillClient
(
this
.
currentTextEditingValue
);
@override
String
get
autofillId
=>
hashCode
.
toString
();
@override
TextInputConfiguration
textInputConfiguration
;
@override
void
updateEditingValue
(
TextEditingValue
newEditingValue
)
{
currentTextEditingValue
=
newEditingValue
;
latestMethodCall
=
'updateEditingValue'
;
}
@override
AutofillScope
currentAutofillScope
;
String
latestMethodCall
=
''
;
@override
TextEditingValue
currentTextEditingValue
;
@override
void
performAction
(
TextInputAction
action
)
{
latestMethodCall
=
'performAction'
;
}
@override
void
updateFloatingCursor
(
RawFloatingCursorPoint
point
)
{
latestMethodCall
=
'updateFloatingCursor'
;
}
@override
void
connectionClosed
()
{
latestMethodCall
=
'connectionClosed'
;
}
@override
void
showAutocorrectionPromptRect
(
int
start
,
int
end
)
{
latestMethodCall
=
'showAutocorrectionPromptRect'
;
}
}
class
FakeAutofillScope
with
AutofillScopeMixin
implements
AutofillScope
{
final
Map
<
String
,
AutofillClient
>
clients
=
<
String
,
AutofillClient
>{};
@override
Iterable
<
AutofillClient
>
get
autofillClients
=>
clients
.
values
;
@override
AutofillClient
getAutofillClient
(
String
autofillId
)
=>
clients
[
autofillId
];
void
register
(
AutofillClient
client
)
{
clients
.
putIfAbsent
(
client
.
autofillId
,
()
=>
client
);
}
}
class
FakeTextChannel
implements
MethodChannel
{
FakeTextChannel
(
this
.
outgoing
)
:
assert
(
outgoing
!=
null
);
Future
<
dynamic
>
Function
(
MethodCall
)
outgoing
;
Future
<
void
>
Function
(
MethodCall
)
incoming
;
List
<
MethodCall
>
outgoingCalls
=
<
MethodCall
>[];
@override
BinaryMessenger
get
binaryMessenger
=>
throw
UnimplementedError
();
@override
MethodCodec
get
codec
=>
const
JSONMethodCodec
();
@override
Future
<
List
<
T
>>
invokeListMethod
<
T
>(
String
method
,
[
dynamic
arguments
])
=>
throw
UnimplementedError
();
@override
Future
<
Map
<
K
,
V
>>
invokeMapMethod
<
K
,
V
>(
String
method
,
[
dynamic
arguments
])
=>
throw
UnimplementedError
();
@override
Future
<
T
>
invokeMethod
<
T
>(
String
method
,
[
dynamic
arguments
])
async
{
final
MethodCall
call
=
MethodCall
(
method
,
arguments
);
outgoingCalls
.
add
(
call
);
return
await
outgoing
(
call
)
as
T
;
}
@override
String
get
name
=>
'flutter/textinput'
;
@override
void
setMethodCallHandler
(
Future
<
void
>
Function
(
MethodCall
call
)
handler
)
{
incoming
=
handler
;
}
@override
void
setMockMethodCallHandler
(
Future
<
void
>
Function
(
MethodCall
call
)
handler
)
=>
throw
UnimplementedError
();
void
validateOutgoingMethodCalls
(
List
<
MethodCall
>
calls
)
{
expect
(
outgoingCalls
.
length
,
calls
.
length
);
bool
hasError
=
false
;
for
(
int
i
=
0
;
i
<
calls
.
length
;
i
++)
{
final
ByteData
outgoingData
=
codec
.
encodeMethodCall
(
outgoingCalls
[
i
]);
final
ByteData
expectedData
=
codec
.
encodeMethodCall
(
calls
[
i
]);
final
String
outgoingString
=
utf8
.
decode
(
outgoingData
.
buffer
.
asUint8List
());
final
String
expectedString
=
utf8
.
decode
(
expectedData
.
buffer
.
asUint8List
());
if
(
outgoingString
!=
expectedString
)
{
print
(
'Index
$i
did not match:
\n
'
' actual:
${outgoingCalls[i]}
\n
'
' expected:
${calls[i]}
'
);
hasError
=
true
;
}
}
if
(
hasError
)
{
fail
(
'Calls did not match.'
);
}
}
}
packages/flutter/test/services/text_input_test.dart
View file @
e31f7089
...
...
@@ -200,6 +200,9 @@ class FakeTextInputClient implements TextInputClient {
@override
TextEditingValue
currentTextEditingValue
;
@override
AutofillScope
get
currentAutofillScope
=>
null
;
@override
void
performAction
(
TextInputAction
action
)
{
latestMethodCall
=
'performAction'
;
...
...
packages/flutter/test/widgets/autofill_group_test.dart
0 → 100644
View file @
e31f7089
// Copyright 2014 The Flutter 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/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'AutofillGroup has the right clients'
,
(
WidgetTester
tester
)
async
{
const
Key
outerKey
=
Key
(
'outer'
);
const
Key
innerKey
=
Key
(
'inner'
);
const
TextField
client1
=
TextField
(
autofillHints:
<
String
>[
'1'
]);
const
TextField
client2
=
TextField
(
autofillHints:
<
String
>[
'2'
]);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
AutofillGroup
(
key:
outerKey
,
child:
Column
(
children:
<
Widget
>[
client1
,
AutofillGroup
(
key:
innerKey
,
child:
Column
(
children:
const
<
Widget
>[
client2
,
TextField
()]),
),
]),
),
),
),
);
final
AutofillGroupState
innerState
=
tester
.
state
<
AutofillGroupState
>(
find
.
byKey
(
innerKey
));
final
AutofillGroupState
outerState
=
tester
.
state
<
AutofillGroupState
>(
find
.
byKey
(
outerKey
));
final
EditableTextState
clientState1
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byWidget
(
client1
),
matching:
find
.
byType
(
EditableText
)),
);
final
EditableTextState
clientState2
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byWidget
(
client2
),
matching:
find
.
byType
(
EditableText
)),
);
expect
(
outerState
.
autofillClients
,
<
EditableTextState
>[
clientState1
]);
expect
(
innerState
.
autofillClients
,
<
EditableTextState
>[
clientState2
]);
});
testWidgets
(
'new clients can be added & removed to a scope'
,
(
WidgetTester
tester
)
async
{
const
Key
scopeKey
=
Key
(
'scope'
);
final
List
<
String
>
hints
=
<
String
>[];
const
TextField
client1
=
TextField
(
autofillHints:
<
String
>[
'1'
]);
final
TextField
client2
=
TextField
(
autofillHints:
hints
);
StateSetter
setState
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
AutofillGroup
(
key:
scopeKey
,
child:
StatefulBuilder
(
builder:
(
BuildContext
context
,
StateSetter
setter
)
{
setState
=
setter
;
return
Column
(
children:
<
Widget
>[
client1
,
client2
]);
},
),
),
),
),
);
final
AutofillGroupState
scopeState
=
tester
.
state
<
AutofillGroupState
>(
find
.
byKey
(
scopeKey
));
final
EditableTextState
clientState1
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byWidget
(
client1
),
matching:
find
.
byType
(
EditableText
)),
);
final
EditableTextState
clientState2
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byWidget
(
client2
),
matching:
find
.
byType
(
EditableText
)),
);
expect
(
scopeState
.
autofillClients
,
<
EditableTextState
>[
clientState1
]);
// Add to scope.
setState
(()
{
hints
.
add
(
'2'
);
});
await
tester
.
pump
();
expect
(
scopeState
.
autofillClients
.
length
,
2
);
expect
(
scopeState
.
autofillClients
,
contains
(
clientState1
));
expect
(
scopeState
.
autofillClients
,
contains
(
clientState2
));
// Remove from scope again.
setState
(()
{
hints
.
clear
();
});
await
tester
.
pump
();
expect
(
scopeState
.
autofillClients
,
<
EditableTextState
>[
clientState1
]);
});
testWidgets
(
'AutofillGroup has the right clients after reparenting'
,
(
WidgetTester
tester
)
async
{
const
Key
outerKey
=
Key
(
'outer'
);
const
Key
innerKey
=
Key
(
'inner'
);
final
GlobalKey
keyClient3
=
GlobalKey
();
const
TextField
client1
=
TextField
(
autofillHints:
<
String
>[
'1'
]);
const
TextField
client2
=
TextField
(
autofillHints:
<
String
>[
'2'
]);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
AutofillGroup
(
key:
outerKey
,
child:
Column
(
children:
<
Widget
>[
client1
,
AutofillGroup
(
key:
innerKey
,
child:
Column
(
children:
<
Widget
>[
client2
,
TextField
(
key:
keyClient3
,
autofillHints:
const
<
String
>[
'3'
]),
]),
),
]),
),
),
),
);
final
AutofillGroupState
innerState
=
tester
.
state
<
AutofillGroupState
>(
find
.
byKey
(
innerKey
));
final
AutofillGroupState
outerState
=
tester
.
state
<
AutofillGroupState
>(
find
.
byKey
(
outerKey
));
final
EditableTextState
clientState1
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byWidget
(
client1
),
matching:
find
.
byType
(
EditableText
)),
);
final
EditableTextState
clientState2
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byWidget
(
client2
),
matching:
find
.
byType
(
EditableText
)),
);
final
EditableTextState
clientState3
=
tester
.
state
<
EditableTextState
>(
find
.
descendant
(
of:
find
.
byKey
(
keyClient3
),
matching:
find
.
byType
(
EditableText
)),
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
AutofillGroup
(
key:
outerKey
,
child:
Column
(
children:
<
Widget
>[
client1
,
TextField
(
key:
keyClient3
,
autofillHints:
const
<
String
>[
'3'
]),
AutofillGroup
(
key:
innerKey
,
child:
Column
(
children:
const
<
Widget
>[
client2
]),
),
]),
),
),
),
);
expect
(
outerState
.
autofillClients
.
length
,
2
);
expect
(
outerState
.
autofillClients
,
contains
(
clientState1
));
expect
(
outerState
.
autofillClients
,
contains
(
clientState3
));
expect
(
innerState
.
autofillClients
,
<
EditableTextState
>[
clientState2
]);
});
}
packages/flutter/test/widgets/editable_text_test.dart
View file @
e31f7089
...
...
@@ -4116,8 +4116,17 @@ void main() {
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
// TextInput.show should be before TextInput.setEditingState
final
List
<
String
>
logOrder
=
<
String
>[
'TextInput.setClient'
,
'TextInput.show'
,
'TextInput.setEditableSizeAndTransform'
,
'TextInput.setStyle'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.show'
];
expect
(
tester
.
testTextInput
.
log
.
length
,
7
);
final
List
<
String
>
logOrder
=
<
String
>[
'TextInput.setClient'
,
'TextInput.show'
,
'TextInput.setEditableSizeAndTransform'
,
'TextInput.requestAutofill'
,
'TextInput.setStyle'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
'TextInput.show'
,
];
expect
(
tester
.
testTextInput
.
log
.
length
,
8
);
int
index
=
0
;
for
(
final
MethodCall
m
in
tester
.
testTextInput
.
log
)
{
expect
(
m
.
method
,
logOrder
[
index
]);
...
...
@@ -4156,6 +4165,7 @@ void main() {
'TextInput.setClient'
,
'TextInput.show'
,
'TextInput.setEditableSizeAndTransform'
,
'TextInput.requestAutofill'
,
'TextInput.setStyle'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
...
...
@@ -4203,6 +4213,7 @@ void main() {
'TextInput.setClient'
,
'TextInput.show'
,
'TextInput.setEditableSizeAndTransform'
,
'TextInput.requestAutofill'
,
'TextInput.setStyle'
,
'TextInput.setEditingState'
,
'TextInput.setEditingState'
,
...
...
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