目录前言开发思路先上效果开发细节使用RawKeyboardListenerProvider层对事件进行处理注意总结文件参考TVkeyCode详解前言最近公司有了新的业务,把现有FlutterAnd...
目录
前言开发思路
先上效果
开发细节
使用RawKandroideyboardListener
Provider层对事件进行处理
注意
总结
文件参考
TV keyCode详解
前言
最近公司有了新的业务,把现有Flutter android项目应用到TV上去,这不,Asscre的活就来了。
本文详细说明Flutter for TV的两种实现方式,能力有限,不足之处欢迎指点,哈哈哈
开发思路
在开发之前,我们先设定一下我们的思路。
即,如何对原有程序代码侵入式最小、性能最佳、可玩性更高做出设定。
那么,通过上面的设定,我们在Flutter Widget中就发现了两个东西:
RawKeyboardListenerInkWell和其他Android TV配置
先上效果
可玩性、可塑性更高的RawKeyboardListener解决方案效果

对原有程序修改最小的InkWell和其他Android TV配置解决方案效果

开发细节
可玩性、可塑性更高的RawKeyboardListener解决方案
使用RawKeyboardListener
RawKeyboardListener(
focusNode: d.focusNode, // 配置focusNode
onKey: (RawKeyEvent event) =>
context.read<HomePageContentWidgetProvider>().focusEventHandler(event, context, d), // 对特殊事件进行监听和处理
child: Container(
height: 190,
width: 190,
decoration: BoxDecoration(
border: Border.all(
width: 2,
color: d.focusNode.hasFocus ? Colors.blue : Colors.transparent),
borderRadius: BorderRadius.circular(20),
color: Colors.white.withAlpha(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
d.img,
height: 80,
),
SizedBox(height: 20),
Text(
d.name,
style: TextStyle(
color: Colors.white,
fontSize: 32,
),
),
],
),
),
),
Provider层对事件进行处理
import 'package:flutter/material.Dart';
import 'package:flutter/services.dart';
import 'package:tv_test/pages/memory_page/memory_page.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class HomePageContentWidgetProvider
with ChangeNotifier {
bool init = false;
double maxWScreen = 0; // 按钮距离屏幕右侧最大边界
double minWScreen = 60.w; // 按钮距离屏幕最左侧距离边界
final List<HomePageMakeBtn> makeBtnList = [
HomePageMakeBtn('lib/assets/img/youtube.png', 'You Tube', javascript'', FocusNode()),
HomePageMakeBtn('lib/assets/img/apple.png', 'Apple', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/facebook.png', 'Facebook', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/douyin.png', 'Tik Tok', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/mi.png', 'MI', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/huawei.png', 'Hua Wei', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/youtube.png', 'TTT', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/apple.png', 'DDDD', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/facebook.png', 'FFFF', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/douyin.png', 'AAAA', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/mi.png', 'QQQQQ', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/huawei.png', 'WWWW', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/youtube.png', 'EEEEE', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/apple.png', 'RRRRR', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/facebook.png', 'YYYYYY', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/douyin.png', 'UUUUUU', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/mi.png', 'SSSSS', '', FocusNode()),
HomePageMakeBtn('lib/assets/img/huawei.png', 'VVVV', '', FocusNode()),
];
HomePageContentWidgetProvider(BuildContext context) {
maxWScreen = MediaQuery.of(context).size.width - 246.w;
// setMakeFocusAddListener();
if (!init) {
makeBtnList.first.focusNode.requestFocus();
init = true;
}
}
setMakeFocusAddListener() {
for (int i = 0; i < makeBtnList.length; i++) {
makeBtnList[i].focusNode.addListener(() {
if (makeBtnList[i].focusNode.hasFocus) {
// notifyListeners();
print(
'====${makeBtnList[i].name} : ${makeBtnList[i].focusNode.hasFocus}');
}
});
}
}
setMakeFocusDispose() {
for (var item in makeBtnList) {
item.focusNode.removeListener(() {});
item.focusNode.dispose();
}
}
focusEventHandler(
RawKeyEvent event, BuildContext context, HomePageMakeBtn param) async {
/// 只处理按键按下的事件
if (event.data is RawKeyEventDataAndroid &&
event.runtimeType.toString() == 'RawKeyDownEvent') {
CustomRawKeyEventDataAndroid _d =
CustomRawKeyEventDataAndroid.format(event.data);
/// 对按下确定键和中心键进行处理
if (_d.keyCode == 23 || _d.keyCode == 66) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => MemoryPage(title: param.name)));
} else {
// for (var e in makeBtnList) {
// print('${e.name} : ${e.focusNode.hasFocus}');
// }
/// 对左键进行处理
if (_d.keyCode == 21) {
await keyCodeDpadLeft(context, param);
}
/// 对右键进行处理
if (_d.keyCode == 22) {
await keyCodeDpadRight(context, param);
}
notifyListeners();
}
}
}
/// 对左键进行处理
keyCodeDpadLeft(BuildContext context, HomePageMakeBtn param) async {
/// 首位边界处理
final int _idx = makeBtnList.indexWhere((e) => e == param);
if (_idx == 0) return;
final int _nextIndex = _idx + 1;
if ((_nextIndex % 7) == 1) {
HomePageMakeBtn _nextNode = makeBtnList[_idx - 1];
print(_nextNode.name);
await Future.delayed(const Duration(milliseconds: 20));
_nextNode.focusNode.requestFocus();
}
}
/// 对右键进行处理
keyCodeDpadRight(BuildContext context, HomePageMakeBtn param) async {
final int _idx = makeBtnList.indexWhere((e) => e == param);
/// 末位边界处理
if (_idx == (makeBtnList.length - 1)) return;
final int _nextIndex = _idx + 1;
if ((_nextIndex % 7) == 0) {
HomePageMakeBtn _nextNode = makeBtnList[_nextIndex];
await Future.delayed(const Duration(milliseconds: 20));
_nextNode.focusNode.requestFocus();
}
}
@override
void dispose() {
setMakeFocusDispose();
super.dispose();
}
}
class HomePageMakeBtn {
final String img;
final String name;
final String routerName;
final FocusNode focusNode;
HomePageMakeBtn(this.img, this.name, this.routerName, this.focusNode);
}
class CustomRawKeyEventDataAndroid {
final int flags;
final int codePoint;
final int plainCodePoint;
/// case 19: KEY_UP
/// case 20: KEY_DOWN
/// case 21: KEY_LEFT
/// case 22: KEY_RIGHT
/// case 23: KEY_CENTER
final int keyCode;
final int scanCode;
final int metaState;
CustomRawKeyEventDataAndroid(this.flags, this.codePoint, this.plainCodePoint,
this.keyCode, this.scanCode, this.metaState);
static CustomRawKeyEventDataAndroid format(d) {
return CustomRawKeyEventDataAndroid(d.flags, d.codePoint, d.plainCodePoint,
d.keyCode, d.scanCode, d.metaState);
}
}
注意
我们可以看到在处理左键和右键的时候我们用了

这是为什么呢?
那是因为在实际效果中,我们requestFocus操作的时候,Flutter的机制会首先触发一次requestFocus,然后再触发一次requestFocus,一共两次,这就与我们的预想就有冲突了。
例如:
使用按键末尾向右时,系统触发的focus到UUUUU这个按钮,我们的实际预想的是到YYYYY即可。

使用按键首位向左时,同样会跨两个focus。

目前Asscre并没有找到很好的解决方案,但使用await Future delayed可以舒缓一下这不人性的操作。
对原有程序修改最小的InkWell和其他Android TV配置解决方案
首先,我们需要在AndroidManifest.XML 设置LEANBACK_LAUNCHER告诉平台我们的程序是一个电视应用程序
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/> // 新增这一句
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
然后,我们在Main入口文件中添加 Shortcuts用于我们的程序响应我们的遥控器指令。
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.select): ActivateIntent(),
},
child: MaterialApp(
...
);
最后,使用InkWell来获取焦点设置用户遥控点击的效果,其中focusColor帮助我们提醒用户此时的按钮位置。
return Material(
color: Colors.white.withAlpha(20),
child: InkWell(
focusColor: Colors.deepOrange.withAlpha(80),
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => MemoryPage(title: d.name))),
child: SizedBox(
height: 190,
width: 190,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
d.img,
height: 80,
),
SizedBox(height: 20),
Text(
d.name,
style: TextStyle(
color: Colors.white,
fontSize: 32,
),
),
],
),
),
),
);
总结
上述两种解决方案中,大家可以根据自己(boss)的喜好或者业务需求选择一种使用。
在需要复杂的自定义的业务情况下,推荐使用RawKeyboardListener的解决方案,可以做出很多酷炫的效果,譬如按键事件触发时,focus住的widget可以做出放大、渐变等等效果,这有助于提升用户的体验。
但,要是在现有的业务逻辑上,在少量调整后就可使用上述中的InkWell的解决方案。
文件参考
TV keyCode详解
以上就是Flutter TV Android端开发技巧详细教程的详细内容,更多关于Flutter TV Android端开发的资料请关注我们其它相关文章!










