Fixes for search screen and for new "playlists" screen with search field
This commit is contained in:
parent
abbd795a35
commit
79ad6992d9
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue