Fixes for search screen and for new "playlists" screen with search field

This commit is contained in:
kilowatt 2020-11-22 13:17:37 +03:00
parent abbd795a35
commit 79ad6992d9
3 changed files with 87 additions and 53 deletions

View File

@ -32,7 +32,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 28 compileSdkVersion 29
lintOptions { lintOptions {
disable 'InvalidPackage' disable 'InvalidPackage'
@ -42,7 +42,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "f.f.freezer" applicationId "f.f.freezer"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 29
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@ -163,11 +163,12 @@ class MainScreen extends StatefulWidget {
_MainScreenState createState() => _MainScreenState(); _MainScreenState createState() => _MainScreenState();
} }
class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin{ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()]; List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
int _selected = 0; int _selected = 0;
StreamSubscription _urlLinkStream; StreamSubscription _urlLinkStream;
int _keyPressed = 0; int _keyPressed = 0;
bool textFieldVisited = false;
@override @override
void initState() { void initState() {
@ -184,6 +185,7 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
}); });
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
} }
void _prepareQuickActions() { void _prepareQuickActions() {
@ -226,9 +228,19 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
void dispose() { void dispose() {
if (_urlLinkStream != null) if (_urlLinkStream != null)
_urlLinkStream.cancel(); _urlLinkStream.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
textFieldVisited = false;
});
}
}
void _setupUniLinks() async { void _setupUniLinks() async {
//Listen to URLs //Listen to URLs
_urlLinkStream = getUriLinksStream().listen((Uri uri) { _urlLinkStream = getUriLinksStream().listen((Uri uri) {
@ -242,20 +254,28 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
} catch (e) {} } catch (e) {}
} }
ValueChanged<RawKeyEvent> _handleKey(FocusScopeNode navigatorFocusNode, FocusNode rootFocusNode){ ValueChanged<RawKeyEvent> _handleKey(FocusScopeNode navigationBarFocusNode, FocusNode screenFocusNode){
return (event) { 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; int keyCode = (event.data as RawKeyEventDataAndroid).keyCode;
// Movement to navigation bar and back
switch (keyCode) { switch (keyCode) {
case 127: // Menu on Android TV case 127: // Menu on Android TV
case 327: // EPG on Hisense TV case 327: // EPG on Hisense TV
focusToNavbar(navigatorFocusNode); focusToNavbar(navigationBarFocusNode);
break; break;
case 22: // LEFT + RIGHT case 22: // LEFT + RIGHT
case 21: case 21:
if (_keyPressed == 21 && keyCode == 22 || _keyPressed == 22 && keyCode == 21) { if (_keyPressed == 21 && keyCode == 22 || _keyPressed == 22 && keyCode == 21) {
focusToNavbar(navigatorFocusNode); focusToNavbar(navigationBarFocusNode);
} }
_keyPressed = keyCode; _keyPressed = keyCode;
Future.delayed(Duration(milliseconds: 100), () => { Future.delayed(Duration(milliseconds: 100), () => {
@ -264,31 +284,36 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
break; break;
case 20: // DOWN case 20: // DOWN
// If it's bottom row, go to navigation bar // If it's bottom row, go to navigation bar
var row = FocusManager.instance.primaryFocus.parent; var row = primaryFocus.parent;
var column = row.parent; if (row != null) {
var column = row.parent;
if (column.children.last == row) { if (column.children.last == row) {
focusToNavbar(navigatorFocusNode); focusToNavbar(navigationBarFocusNode);
}
} }
break; break;
case 19: // UP case 19: // UP
if (navigatorFocusNode.hasFocus) { if (navigationBarFocusNode.hasFocus) {
rootFocusNode.focusInDirection(TraversalDirection.up); 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
if (navigatorFocusNode.parent.hasPrimaryFocus || navigatorFocusNode.parent.parent.hasPrimaryFocus) {
navigatorFocusNode.parent.children.first.children.first.requestFocus();
} }
break; break;
} }
} }
// WA for returning from search: focus on first child if parent is focused // After visiting text field, something goes wrong and KeyDown events are not sent, only KeyUp-s.
if (event.runtimeType.toString() == 'RawKeyUpEvent') { // Focus moving works only on KeyDown events, so here we simulate keys handling as it's done in Flutter
LogicalKeyboardKey key = event.data.logicalKey; if (textFieldVisited && event.runtimeType.toString() == 'RawKeyUpEvent') {
var modalFocusNode = navigatorFocusNode.parent.parent.children.first.children.first Map<LogicalKeySet, Intent> shortcuts = Shortcuts.of(context).shortcuts;
.children.first; final BuildContext primaryContext = primaryFocus?.context;
if (key == LogicalKeyboardKey.arrowRight && modalFocusNode.hasPrimaryFocus) { Intent intent = shortcuts[LogicalKeySet(event.logicalKey)];
modalFocusNode.unfocus(); if (intent != null) {
modalFocusNode.focusInDirection(TraversalDirection.right); 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<MainScreen> with SingleTickerProviderStateM
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FocusScopeNode navigatorFocusNode = FocusScopeNode(); // for bottom navigator FocusScopeNode navigationBarFocusNode = FocusScopeNode(); // for bottom navigation bar
FocusNode rootFocusNode = FocusNode(); // for Scaffold FocusNode screenFocusNode = FocusNode(); // for CustomNavigator
return RawKeyboardListener( return RawKeyboardListener(
focusNode: rootFocusNode, focusNode: FocusNode(),
onKey: _handleKey(navigatorFocusNode, rootFocusNode), onKey: _handleKey(navigationBarFocusNode, screenFocusNode),
child: Scaffold( child: Scaffold(
bottomNavigationBar: bottomNavigationBar:
FocusScope( FocusScope(
node: navigatorFocusNode, node: navigationBarFocusNode,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -348,8 +373,13 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
body: AudioServiceWidget( body: AudioServiceWidget(
child: CustomNavigator( child: CustomNavigator(
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
home: _screens[_selected], home: Focus(
pageRoute: PageRoutes.materialPageRoute, focusNode: screenFocusNode,
skipTraversal: true,
canRequestFocus: false,
child: _screens[_selected]
),
pageRoute: PageRoutes.materialPageRoute
), ),
))); )));
} }

View File

@ -125,7 +125,8 @@ class _SearchScreenState extends State<SearchScreen> {
var textFielFocusNode = FocusNode(); var textFielFocusNode = FocusNode();
return Scaffold( return Scaffold(
appBar: FreezerAppBar('Search'.i18n), appBar: FreezerAppBar('Search'.i18n),
body: ListView( body: FocusScope(
child: ListView(
children: <Widget>[ children: <Widget>[
Container(height: 4.0), Container(height: 4.0),
Padding( Padding(
@ -167,30 +168,32 @@ class _SearchScreenState extends State<SearchScreen> {
onSubmitted: (String s) => _submit(context, query: s), onSubmitted: (String s) => _submit(context, query: s),
) )
), ),
Row( Focus(
mainAxisSize: MainAxisSize.min, canRequestFocus: false, // Focus is moving to cross, and hangs out there,
children: [ descendantsAreFocusable: false, // so we disable focusing on it at all
Container( child: Row(
width: 40.0, mainAxisSize: MainAxisSize.min,
child: IconButton( children: [
splashRadius: 20.0, Container(
icon: Icon(Icons.clear), width: 40.0,
onPressed: () { child: IconButton(
setState(() { splashRadius: 20.0,
_suggestions = []; icon: Icon(Icons.clear),
_query = ''; onPressed: () {
}); setState(() {
_controller.clear(); _suggestions = [];
}, _query = '';
});
_controller.clear();
},
),
), ),
), ],
], )
) )
], ],
) )
), ),
], ],
), ),
), ),
@ -299,6 +302,7 @@ class _SearchScreenState extends State<SearchScreen> {
}, },
)) ))
], ],
)
), ),
); );
} }