Flutter开发Android TV应用竟如此简单
在安卓开发中,为了区别 TV 应用和手机应用,我们需要在 AndroidManifest.xml 中声明 TV Activity。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="flutter_tv_tmdb"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
...>
...
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
...
</application>
...
</manifest>
即在 <intent-filter>
中添加 <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
。在添加了该行后,我们可以发现 AndroidManifest.xml 会提示有两个错误:
我们把鼠标箭头移动错误处,会提示解决方法,让它自己解决后会添加如下内容:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<application
...
android:banner="@mipmap/banner">
...
</application>
...
</manifest>
banner 是什么?banner 是 Android 开发中的自适应图标,我们在 Android Studio 中创建的 TV 虚拟机中,默认 APP 显示的图标就是 banner 设置的值。官网 TV 应用图标设计准则 有关于这个的详细说明。
在 TV 应用开发中,最重要的是获取焦点,而决定一个元素是否能够获取焦点最简单的一点就是,该元素是否添加了事件。当一个元素添加了事件,它就能被遥控器的方向键选中。
在 TV 应用中,最基本的 TabBar
无法实现我们预想的效果,要想 TabBar
选中就切换,我们需要在外面包裹一层 Focus
或 FocusableActionDetector
,使用它们的 onFocusChange
事件来切换 TabBarView
:
TabBar(
controller: controller,
tabs: List.generate(
tabs.length,
(index) => FocusableActionDetector(
focusNode: tabs[index].focusNode,
autofocus: index == 0,
onFocusChange: (focus) {
controller.animateTo(index, duration: const Duration(milliseconds: 300), curve: Curves.ease);
},
child: Tab(text: tabs[index].text),
),
).toList(),
)
记住一点,如果不需要使用 AppBar
的话,要把 TabBar
放到 body
里面,如果直接把 TabBar
放到 AppBar
的 title
中,onFocusChange
只有前几(我测试时只有前3)个被选中才会调用。
为了获取用户的遥控器按键事件,我们需要使用 KeyboardListener
这个组件,对我们来说主要有 onKeyEvent
中的以下几个事件可以获取:
focusEventHandle(KeyEvent e) {
switch (e.logicalKey) {
case LogicalKeyboardKey.arrowRight:
print('按了右键');
break;
case LogicalKeyboardKey.arrowLeft:
print('按了左键');
break;
case LogicalKeyboardKey.arrowUp:
print('按了上键');
break;
case LogicalKeyboardKey.arrowDown:
print('按了下键');
break;
case LogicalKeyboardKey.select:
print('按了确认键');
break;
case LogicalKeyboardKey.goBack:
print('按了返回键');
break;
case LogicalKeyboardKey.contextMenu:
print('按了菜单键');
break;
case LogicalKeyboardKey.audioVolumeUp:
print('按了音量加键');
break;
case LogicalKeyboardKey.audioVolumeDown:
print('按了音量减键');
break;
case LogicalKeyboardKey.f5:
// 不同品牌的可能不一样
print('按了语音键');
break;
}
}
对于遥控器按键事件的一大难点——并不是所有遥控器都有菜单键(例如 Android Studio 中的 TV 模拟器就没有),即使菜单键在有些需求开发时很重要。好在国内的智能电视都有菜单键,如果只注重国内市场的话可以不用担心。不过上下左右和确定键是所有遥控器的标配。
在 KeyboardListener
中,每次按下都会调用两遍我们的方法。
这是因为每次按下都会调用两个事件:KeyDownEvent
和 KeyUpEvent
。
通过这两个事件,我们可以监听到用户是否长按了某个键,以此来添加长按事件。
late DateTime start;
void updateStart(DateTime time) {
start = time;
notifyListeners();
}
focusEventHandle(KeyEvent e) {
if (e is KeyDownEvent) {
updateStart(DateTime.now());
print('按下了$start');
}
if (e is KeyUpEvent) {
int duration = DateTime.now().difference(start).inMilliseconds;
bool longPress = duration > 500;
print('${DateTime.now()}');
print('持续了$duration');
if (longPress) {
print('长按了');
} else {
print('没长按');
}
}
我们不需要在 focusEventHandle
中添加上下左右的选择事件,因为当一个组件拥有焦点时(所有添加了点击事件的组件都会拥有焦点),系统通过按钮的选择会自动切换到有焦点的组件上。我们唯一要关心的是组件选中时应该呈现的样式。
class ContentView extends StatelessWidget {
const ContentView({super.key});
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, provider, _) {
List<HomeBtn> list = context.watch<AppProvider>().btnList;
return Column(
children: [
TextButton(
onPressed: () => context.read<AppProvider>().toTabView(context),
child: const Text('下一个界面'),
),
InkWell(onTap: () {}, child: const Text('这是自定义按钮')),
Expanded(
child: GridView.builder(
itemCount: context.watch<AppProvider>().btnList.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 16 / 9,
),
itemBuilder: (context, index) {
HomeBtn btn = list[index];
return KeyboardListener(
focusNode: btn.focusNode,
onKeyEvent: (e) => context
.read<AppProvider>()
.focusEventHandle(e, context, btn),
child: AnimatedContainer(
duration: const Duration(milliseconds: 120),
padding: EdgeInsets.all(btn.focusNode.hasFocus ? 6 : 12),
decoration: BoxDecoration(
color: list[index].focusNode.hasFocus
? Colors.deepOrangeAccent.withOpacity(.5)
: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Image.asset(
list[index].image,
fit: BoxFit.cover,
),
),
);
},
),
),
],
);
},
);
}
}
对于 Flutter 关于开发 Android TV 应用要分享的知识点就这么多,希望对你有帮助。
原文链接:https://juejin.cn/post/7353280369359896627 作者:菠萝橙子丶