Navigation with Dpad events in flutter tv app
I develop a flutter tv app for android tv
the code for the navigation rail :
class CustomNavigationRail extends StatefulWidget {
const CustomNavigationRail({
super.key,
});
@override
State<CustomNavigationRail> createState() => _CustomNavigationRailState();
}
class _CustomNavigationRailState extends State<CustomNavigationRail> {
int index = 0;
bool isExtended = false;
final selectedColor = const Color(0XFFFD8B2B);
final labelStyleOne = TextStyle(
fontFamily: "Averta",
fontWeight: FontWeight.normal,
fontSize: 60.sp,
);
final labelStyleTwo = TextStyle(
fontFamily: "Averta",
fontWeight: FontWeight.bold,
fontSize: 60.sp,
);
late FocusNode _navigationRailfocusNode;
late FocusNode _selectedPageFocusNode;
late FocusNode firstGridItemFocusNode;
@override
void initState() {
isExtended = false;
super.initState();
_navigationRailfocusNode = FocusNode();
_selectedPageFocusNode = FocusNode();
_selectedPageFocusNode.requestFocus();
firstGridItemFocusNode = FocusNode();
}
@override
void dispose() {
_navigationRailfocusNode.dispose();
_selectedPageFocusNode.dispose();
firstGridItemFocusNode.dispose();
super.dispose();
}
void _handleKeyEvent(RawKeyEvent event) {
if (event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
_updateIndex(-1);
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
_updateIndex(1);
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
setState(() {
isExtended = false;
moveToSelectedPage();
});
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
setState(() {
isExtended = true;
moveToRail();
});
} else if (event.logicalKey == LogicalKeyboardKey.select) {
setState(() {
isExtended = false;
});
}
}
}
void _updateIndex(int direction) {
setState(() {
index = (index + direction) % 5; // Assuming you have 5 destinations
if (index < 0) {
index =
4; // Wrap around to the last index when moving up from the first
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Container(
constraints: const BoxConstraints.expand(),
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/images/bg-snrt-live.webp"),
fit: BoxFit.cover))),
Row(
children: [
Focus(
focusNode: _navigationRailfocusNode,
onKey: (node, event) {
_handleKeyEvent(event);
return KeyEventResult.handled;
},
child: NavigationRail(
backgroundColor: const Color(0XFF354765),
selectedIndex: index,
//labelType: NavigationRailLabelType.all,
selectedLabelTextStyle: labelStyleOne.copyWith(
color: Color(0XFFFD8B2B),
),
unselectedLabelTextStyle:
labelStyleOne.copyWith(color: Color(0XFF94A3BC)),
onDestinationSelected: (index) => setState(
() => {this.index = index, isExtended = !isExtended}),
leading: Image(
image: AssetImage("assets/images/side_bar_image.png"),
height: 300.h,
width: 450.w),
extended: isExtended,
destinations: [
NavigationRailDestination(
icon: SvgPicture.asset(
"assets/images/tv.svg",
color: Colors.white,
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
selectedIcon: SvgPicture.asset(
"assets/images/tv.svg",
color: const Color(0XFFFD8B2B),
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
label: Text(
translate('global_keywords.television'),
),
),
NavigationRailDestination(
icon: SvgPicture.asset(
"assets/images/radio.svg",
color: Colors.white,
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
selectedIcon: SvgPicture.asset(
"assets/images/radio.svg",
color: const Color(0XFFFD8B2B),
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
label: Text(
translate('global_keywords.radio'),
),
),
NavigationRailDestination(
icon: SvgPicture.asset(
"assets/images/apropos.svg",
color: Colors.white,
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
selectedIcon: SvgPicture.asset(
"assets/images/apropos.svg",
color: const Color(0XFFFD8B2B),
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
label: Text(
translate('global_keywords.about'),
),
),
NavigationRailDestination(
icon: SvgPicture.asset(
"assets/images/mentions_legales.svg",
color: Colors.white,
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
selectedIcon: SvgPicture.asset(
"assets/images/mentions_legales.svg",
color: const Color(0XFFFD8B2B),
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
label: Text(
translate('global_keywords.mentions'),
),
),
NavigationRailDestination(
icon: SvgPicture.asset(
"assets/images/exit.svg",
color: Colors.white,
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
selectedIcon: SvgPicture.asset(
"assets/images/exit.svg",
color: const Color(0XFFFD8B2B),
height: 50.h,
width: 50.w,
matchTextDirection: true,
),
label: Text(
translate('global_keywords.exit'),
),
)
],
),
),
Expanded(
child: Focus(
focusNode: _selectedPageFocusNode, child: buildPages()))
],
),
],
));
}
Widget buildPages() {
switch (index) {
case 0:
return ChannelsPage(focusNode: firstGridItemFocusNode,);
case 1:
return RadioPage();
case 2:
return AboutPage();
case 3:
return LegalNoticePage();
case 4:
return ExitPage();
default:
return ChannelsPage(focusNode: firstGridItemFocusNode,);
}
}
void moveToSelectedPage() {
_navigationRailfocusNode.unfocus();
_selectedPageFocusNode.requestFocus();
}
void moveToRail() {
_selectedPageFocusNode.unfocus();
_navigationRailfocusNode.requestFocus();
}
}
the navigation rail works fine but wenn i select ChannelsPage for exemple the focus dosn't work like i expected and also wenn the app start the first item in the grid view displayed in ChannelsPage dosn't have a focus.
the code for ChannelsPage :
class ChannelsPage extends StatefulWidget {
final FocusNode focusNode;
const ChannelsPage({
required this.focusNode,
super.key,
});
@override
State<ChannelsPage> createState() => _ChannelsPageState();
}
class _ChannelsPageState extends State<ChannelsPage> {
late FeedService _feedService;
late Response response;
Dio dio = Dio();
bool loading = false; //for data fetching status
bool refresh = true; //for forcing refreshing cache
String baseUrl = Config.baseUrl;
@override
void initState() {
super.initState();
_feedService = FeedService();
dio.interceptors
.add(DioCacheManager(CacheConfig(baseUrl: baseUrl)).interceptor);
// Setting the initial focus to the first grid item
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.focusNode.requestFocus();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _feedService.getFeeds(dio, 1),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
return Padding(
padding: EdgeInsets.only(
left: 263.w, right: 263.w, top: 190.h, bottom: 150.h),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: snapshot.data!.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 66.w,
mainAxisSpacing: 65.h),
itemBuilder: (BuildContext context, int index) {
final feed = snapshot.data![index];
return Focus(
focusNode: widget.focusNode,
child: RawMaterialButton(
padding: EdgeInsets.only(
left: 25.w, top: 20.h, bottom: 15.h),
focusColor: const Color(0XFF354765).withOpacity(0.8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
//side: BorderSide(color: Colors.white),
),
child: Padding(
padding: EdgeInsets.only(right: 10.w),
child: Container(
//color: const Color(0XFF354765).withOpacity(0.8),
alignment: Alignment.center,
height: 500.h,
width: 500.w,
child: Image.network(
feed.icon,
height: 500.h,
width: 500.w,
)),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CustomVideoPlayer(
feed: feed,
)));
},
),
);
}),
);
}
return Container();
}),
);
//);
}
}
i would really appreciate your help to solve this problem
Answer
You have an onKey
handler in your navigation rail Focus widget, but you don't have any onKey
handler in your ChannelPage's Focus widget. The ChannelPage does have Focus when you switch to it, you can test this by adding an onKey handler to its Focus widget
I also suggest updating to the new version named onKeyEvent
since onKey is now deprecated (see example code: here)