Unverified Commit ff97e255 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

drop/restore focus when app becomes invisible/visible (#24744)

parent ea7d086e
...@@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart';
import 'banner.dart'; import 'banner.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'media_query.dart'; import 'media_query.dart';
...@@ -730,7 +731,33 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -730,7 +731,33 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
} }
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { } void didChangeAppLifecycleState(AppLifecycleState state) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
return;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
// When the application moves to the background, any focus nodes
// need to lose focus. When the application is restored to the
// foreground state, any focused node should regain focus (including)
// restoring the keyboard. This is only important in cases where
// applications in the background are partially visible.
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.suspending:
case AppLifecycleState.inactive: {
final FocusManager focusManager = WidgetsBinding.instance.focusManager;
focusManager.windowDidLoseFocus();
break;
}
case AppLifecycleState.resumed: {
final FocusManager focusManager = WidgetsBinding.instance.focusManager;
focusManager.windowDidGainFocus();
break;
}
}
}
}
@override @override
void didHaveMemoryPressure() { } void didHaveMemoryPressure() { }
......
...@@ -417,6 +417,45 @@ class FocusManager { ...@@ -417,6 +417,45 @@ class FocusManager {
final FocusScopeNode rootScope = FocusScopeNode(); final FocusScopeNode rootScope = FocusScopeNode();
FocusNode _currentFocus; FocusNode _currentFocus;
bool _windowHasFocus = true;
FocusNode _cachedFocus;
FocusScopeNode _cachedScope;
/// Call when the window loses focus.
///
/// The currently focused node and containg scope is cached, then the current
/// focus is removed. For example, this ensures that cursors do not blink
/// while in the background.
///
/// This method is safe to call multiple times.
void windowDidLoseFocus() {
if (!_windowHasFocus) {
return;
}
_windowHasFocus = false;
_cachedFocus = _currentFocus;
_cachedScope = _cachedFocus._parent;
_currentFocus?.unfocus();
}
/// Call when the window regains focus.
///
/// If there is a cached focus node from when the window lost focus, this
/// method will restore focus to that node. For example, this ensures that
/// a focused text node will re-request the keyboard.
///
/// This method is safe to call multiple times.
void windowDidGainFocus() {
if (_windowHasFocus) {
return;
}
_windowHasFocus = true;
if (_cachedFocus != null) {
_cachedScope._setFocus(_cachedFocus);
_cachedFocus = null;
_cachedScope = null;
}
}
void _willDisposeFocusNode(FocusNode node) { void _willDisposeFocusNode(FocusNode node) {
assert(node != null); assert(node != null);
...@@ -434,16 +473,18 @@ class FocusManager { ...@@ -434,16 +473,18 @@ class FocusManager {
FocusNode _findNextFocus() { FocusNode _findNextFocus() {
FocusScopeNode scope = rootScope; FocusScopeNode scope = rootScope;
while (scope._firstChild != null) while (scope._firstChild != null) {
scope = scope._firstChild; scope = scope._firstChild;
}
return scope._focus; return scope._focus;
} }
void _update() { void _update() {
_haveScheduledUpdate = false; _haveScheduledUpdate = false;
final FocusNode nextFocus = _findNextFocus(); final FocusNode nextFocus = _findNextFocus();
if (_currentFocus == nextFocus) if (_currentFocus == nextFocus) {
return; return;
}
final FocusNode previousFocus = _currentFocus; final FocusNode previousFocus = _currentFocus;
_currentFocus = nextFocus; _currentFocus = nextFocus;
previousFocus?._notify(); previousFocus?._notify();
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
// 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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -243,4 +246,29 @@ void main() { ...@@ -243,4 +246,29 @@ void main() {
parentFocusScope.detach(); parentFocusScope.detach();
}); });
testWidgets('Focus is lost/restored when window focus is lost/restored on Fuchsia', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
final FocusNode node = FocusNode();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: TextField(
focusNode: node,
autofocus: true,
)
),
));
expect(node.hasFocus, true);
ByteData message = const StringCodec().encodeMessage('AppLifecycleState.inactive');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
await tester.pump();
// Focus is lost when app goes to background.
expect(node.hasFocus, false);
message = const StringCodec().encodeMessage('AppLifecycleState.resumed');
await BinaryMessages.handlePlatformMessage('flutter/lifecycle', message, (_) {});
await tester.pump();
// Focus is restored.
expect(node.hasFocus, true);
debugDefaultTargetPlatformOverride = null;
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment