diff --git a/android/app/build.gradle b/android/app/build.gradle index b6b7500..09f1d8c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -42,7 +42,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "f.f.freezer" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/lib/main.dart b/lib/main.dart index e65a2b1..b838459 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -163,11 +163,12 @@ class MainScreen extends StatefulWidget { _MainScreenState createState() => _MainScreenState(); } -class _MainScreenState extends State with SingleTickerProviderStateMixin{ +class _MainScreenState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver { List _screens = [HomeScreen(), SearchScreen(), LibraryScreen()]; int _selected = 0; StreamSubscription _urlLinkStream; int _keyPressed = 0; + bool textFieldVisited = false; @override void initState() { @@ -184,6 +185,7 @@ class _MainScreenState extends State with SingleTickerProviderStateM }); super.initState(); + WidgetsBinding.instance.addObserver(this); } void _prepareQuickActions() { @@ -226,9 +228,19 @@ class _MainScreenState extends State with SingleTickerProviderStateM void dispose() { if (_urlLinkStream != null) _urlLinkStream.cancel(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + setState(() { + textFieldVisited = false; + }); + } + } + void _setupUniLinks() async { //Listen to URLs _urlLinkStream = getUriLinksStream().listen((Uri uri) { @@ -242,20 +254,28 @@ class _MainScreenState extends State with SingleTickerProviderStateM } catch (e) {} } - ValueChanged _handleKey(FocusScopeNode navigatorFocusNode, FocusNode rootFocusNode){ + ValueChanged _handleKey(FocusScopeNode navigationBarFocusNode, FocusNode screenFocusNode){ return (event) { - if (event.runtimeType.toString() == 'RawKeyDownEvent') { + FocusNode primaryFocus = FocusManager.instance.primaryFocus; + // After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s. + // So, set this flag to indicate a transition to other "mode" + if (primaryFocus.context.widget.runtimeType.toString() == 'EditableText') { + setState(() { + textFieldVisited = true; + }); + } + // Movement to navigation bar and back + if (event.runtimeType.toString() == (textFieldVisited ? 'RawKeyUpEvent' : 'RawKeyDownEvent')) { int keyCode = (event.data as RawKeyEventDataAndroid).keyCode; - // Movement to navigation bar and back switch (keyCode) { case 127: // Menu on Android TV case 327: // EPG on Hisense TV - focusToNavbar(navigatorFocusNode); + focusToNavbar(navigationBarFocusNode); break; case 22: // LEFT + RIGHT case 21: if (_keyPressed == 21 && keyCode == 22 || _keyPressed == 22 && keyCode == 21) { - focusToNavbar(navigatorFocusNode); + focusToNavbar(navigationBarFocusNode); } _keyPressed = keyCode; Future.delayed(Duration(milliseconds: 100), () => { @@ -264,31 +284,36 @@ class _MainScreenState extends State with SingleTickerProviderStateM break; case 20: // DOWN // If it's bottom row, go to navigation bar - var row = FocusManager.instance.primaryFocus.parent; - var column = row.parent; - - if (column.children.last == row) { - focusToNavbar(navigatorFocusNode); + var row = primaryFocus.parent; + if (row != null) { + var column = row.parent; + if (column.children.last == row) { + focusToNavbar(navigationBarFocusNode); + } } break; case 19: // UP - if (navigatorFocusNode.hasFocus) { - rootFocusNode.focusInDirection(TraversalDirection.up); - } - if (navigatorFocusNode.parent.hasPrimaryFocus || navigatorFocusNode.parent.parent.hasPrimaryFocus) { - navigatorFocusNode.parent.children.first.children.first.requestFocus(); + if (navigationBarFocusNode.hasFocus) { + screenFocusNode.parent.parent.children.last // children.last is used for handling "playlists" screen in library. Under CustomNavigator 2 screens appears. + .nextFocus(); // nextFocus is used instead of requestFocus because it focuses on last, bottom, non-visible tile of main page + } break; } } - // WA for returning from search: focus on first child if parent is focused - if (event.runtimeType.toString() == 'RawKeyUpEvent') { - LogicalKeyboardKey key = event.data.logicalKey; - var modalFocusNode = navigatorFocusNode.parent.parent.children.first.children.first - .children.first; - if (key == LogicalKeyboardKey.arrowRight && modalFocusNode.hasPrimaryFocus) { - modalFocusNode.unfocus(); - modalFocusNode.focusInDirection(TraversalDirection.right); + // After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s. + // Focus moving works only on KeyDown events, so here we simulate keys handling as it's done in Flutter + if (textFieldVisited && event.runtimeType.toString() == 'RawKeyUpEvent') { + Map shortcuts = Shortcuts.of(context).shortcuts; + final BuildContext primaryContext = primaryFocus?.context; + Intent intent = shortcuts[LogicalKeySet(event.logicalKey)]; + if (intent != null) { + Actions.invoke(primaryContext, intent, nullOk: true); + } + // WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging. + FocusNode newFocus = FocusManager.instance.primaryFocus; + if (newFocus is FocusScopeNode) { + navigationBarFocusNode.requestFocus(); } } }; @@ -301,16 +326,16 @@ class _MainScreenState extends State with SingleTickerProviderStateM @override Widget build(BuildContext context) { - FocusScopeNode navigatorFocusNode = FocusScopeNode(); // for bottom navigator - FocusNode rootFocusNode = FocusNode(); // for Scaffold + FocusScopeNode navigationBarFocusNode = FocusScopeNode(); // for bottom navigation bar + FocusNode screenFocusNode = FocusNode(); // for CustomNavigator return RawKeyboardListener( - focusNode: rootFocusNode, - onKey: _handleKey(navigatorFocusNode, rootFocusNode), + focusNode: FocusNode(), + onKey: _handleKey(navigationBarFocusNode, screenFocusNode), child: Scaffold( bottomNavigationBar: FocusScope( - node: navigatorFocusNode, + node: navigationBarFocusNode, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -348,8 +373,13 @@ class _MainScreenState extends State with SingleTickerProviderStateM body: AudioServiceWidget( child: CustomNavigator( navigatorKey: navigatorKey, - home: _screens[_selected], - pageRoute: PageRoutes.materialPageRoute, + home: Focus( + focusNode: screenFocusNode, + skipTraversal: true, + canRequestFocus: false, + child: _screens[_selected] + ), + pageRoute: PageRoutes.materialPageRoute ), ))); } diff --git a/lib/ui/search.dart b/lib/ui/search.dart index d7d3293..63b31a4 100644 --- a/lib/ui/search.dart +++ b/lib/ui/search.dart @@ -125,7 +125,8 @@ class _SearchScreenState extends State { var textFielFocusNode = FocusNode(); return Scaffold( appBar: FreezerAppBar('Search'.i18n), - body: ListView( + body: FocusScope( + child: ListView( children: [ Container(height: 4.0), Padding( @@ -167,30 +168,32 @@ class _SearchScreenState extends State { onSubmitted: (String s) => _submit(context, query: s), ) ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40.0, - child: IconButton( - splashRadius: 20.0, - icon: Icon(Icons.clear), - onPressed: () { - setState(() { - _suggestions = []; - _query = ''; - }); - _controller.clear(); - }, + Focus( + canRequestFocus: false, // Focus is moving to cross, and hangs out there, + descendantsAreFocusable: false, // so we disable focusing on it at all + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40.0, + child: IconButton( + splashRadius: 20.0, + icon: Icon(Icons.clear), + onPressed: () { + setState(() { + _suggestions = []; + _query = ''; + }); + _controller.clear(); + }, + ), ), - ), - ], + ], + ) ) - ], ) ), - ], ), ), @@ -299,6 +302,7 @@ class _SearchScreenState extends State { }, )) ], + ) ), ); }