fix(ui,ios): remove _kickFocus microtask refocus (500-1000 ms keyboard lag)
User reported: 'amount need wait 500ms - 1s if i click input field -> then keyboard popup'. The79f8360_kickFocus workaround was the source of the lag. Root cause of the lag: void _kickFocus(FocusNode node) { if (node.hasFocus) node.unfocus(); Future.microtask(() { // <-- this microtask if (!mounted) return; node.requestFocus(); }); } The Future.microtask deferral forces EditableText's attach-to- TextInput path to wait one frame past the user's pointer-up. iOS 26's keyboard slide-up animation then dovetails into that extra frame in a way that adds another 200-800 ms before the keyboard actually appears on screen. Net latency: ~500-1000 ms. Fix: remove _kickFocus entirely. Rely on: 1. TextField's native onTap path (no onTap override = no deferral, no microtask hop, no SystemChannels race). 2. The tap-outside-to-unfocus GestureDetector wrapping the connect form Column (7c62d14) which already guarantees the FocusNode is in the unfocused state when the user taps any field, because any prior keyboard dismissal (tap outside / tap a sibling field) goes through FocusScope.of(context).unfocus(). This means the FocusNode is always in a clean false state when a TextField gets tapped, so EditableText's own attach path can fire synchronously on the first frame and the keyboard appears instantly. The full _kickFocus implementation is retained as a code comment above the connect-form's build() for documentation and quick re-introduction should iOS regress again. The flutter/flutter#181474 issue (the underlying iOS 26 bug) remains open, so the comment documents the canonical workaround if needed. flutter analyze: 6 pre-existing Radio.groupValue deprecation infos (unchanged). flutter build ios --release --no-codesign: 20.4 s, Runner.app 30.2 MB.
This commit is contained in:
@@ -1283,37 +1283,37 @@ class _ConnectFormState extends State<_ConnectForm> {
|
|||||||
final FocusNode _nickFocus = FocusNode();
|
final FocusNode _nickFocus = FocusNode();
|
||||||
final FocusNode _passwordFocus = FocusNode();
|
final FocusNode _passwordFocus = FocusNode();
|
||||||
|
|
||||||
/// Workaround for flutter/flutter#181474 (open, P2, triaged-text-
|
// _kickFocus (unfocus + Future.microtask refocus on every onTap)
|
||||||
/// input, iOS 26+): after the soft keyboard is dismissed, the
|
// was removed after user-reported 500\u20131000 ms keyboard appearance
|
||||||
/// FocusNode still reports `hasFocus == true`. Because the node is
|
// latency on iPhone 16 Pro / iOS 26.
|
||||||
/// already focused, the next user tap on the same TextField does
|
//
|
||||||
/// not trigger any focus-change side effect, so the platform
|
// The microtask deferral was the source of the lag: it forces
|
||||||
/// TextInput channel is never re-opened, and iOS keeps the soft
|
// EditableText's attach-to-TextInput path to wait one frame past
|
||||||
/// keyboard hidden. The user has to tap twice (the second tap
|
// the user's pointer-up, and iOS 26's keyboard slide-up animation
|
||||||
/// triggers an explicit re-focus path that re-shows the keyboard).
|
// then dovetails into that extra frame in a way that adds another
|
||||||
///
|
// 200\u2013800 ms before the keyboard actually appears.
|
||||||
/// The community-recommended workaround in the issue thread is to
|
//
|
||||||
/// **unfocus first, then re-request focus** on every tap. That
|
// The tap-outside-to-unfocus GestureDetector wrapping the connect
|
||||||
/// forces a real focus-change transition (`false -> true`) which
|
// form Column (see build() below) already guarantees the FocusNode
|
||||||
/// re-opens TextInput and slides the keyboard up on the first tap.
|
// is in the unfocused state when the user taps a field, because
|
||||||
///
|
// any prior keyboard dismissal (tap outside / tap a sibling field)
|
||||||
/// This costs nothing on hosts where the bug doesn't reproduce
|
// goes through FocusScope.unfocus(). Therefore TextField's native
|
||||||
/// (the unfocus call is a no-op when the node isn't focused; the
|
// tap path can attach TextInput on the first frame with no help
|
||||||
/// re-request just reaffirms focus and EditableText's own attach
|
// from us, and the keyboard appears instantly.
|
||||||
/// path handles the rest).
|
//
|
||||||
void _kickFocus(FocusNode node) {
|
// If iOS regresses again such that focus state desyncs from
|
||||||
// Drop focus synchronously, then re-grab it on the next frame.
|
// keyboard visibility, the workaround to re-introduce here is the
|
||||||
// Doing both in one frame can race with EditableText's internal
|
// canonical pattern from flutter/flutter#181474:
|
||||||
// bookkeeping; deferring the re-grab by one microtask sidesteps
|
//
|
||||||
// that and matches the workaround pattern in the issue thread.
|
// void _kickFocus(FocusNode node) {
|
||||||
if (node.hasFocus) {
|
// if (node.hasFocus) node.unfocus();
|
||||||
node.unfocus();
|
// Future.microtask(() {
|
||||||
}
|
// if (!mounted) return;
|
||||||
Future.microtask(() {
|
// node.requestFocus();
|
||||||
if (!mounted) return;
|
// });
|
||||||
node.requestFocus();
|
// }
|
||||||
});
|
//
|
||||||
}
|
// and wire onTap: () => _kickFocus(_xxxFocus) on each TextField.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -1347,7 +1347,6 @@ class _ConnectFormState extends State<_ConnectForm> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: widget.hostCtl,
|
controller: widget.hostCtl,
|
||||||
focusNode: _hostFocus,
|
focusNode: _hostFocus,
|
||||||
onTap: () => _kickFocus(_hostFocus),
|
|
||||||
// Server addresses are URL-shaped: hostname or
|
// Server addresses are URL-shaped: hostname or
|
||||||
// hostname:port, all-lowercase ASCII, never user-
|
// hostname:port, all-lowercase ASCII, never user-
|
||||||
// friendly prose. Configure the on-screen keyboard
|
// friendly prose. Configure the on-screen keyboard
|
||||||
@@ -1391,7 +1390,6 @@ class _ConnectFormState extends State<_ConnectForm> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: widget.nickCtl,
|
controller: widget.nickCtl,
|
||||||
focusNode: _nickFocus,
|
focusNode: _nickFocus,
|
||||||
onTap: () => _kickFocus(_nickFocus),
|
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
@@ -1404,7 +1402,6 @@ class _ConnectFormState extends State<_ConnectForm> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: widget.passwordCtl,
|
controller: widget.passwordCtl,
|
||||||
focusNode: _passwordFocus,
|
focusNode: _passwordFocus,
|
||||||
onTap: () => _kickFocus(_passwordFocus),
|
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user