Flutter TV Android端开发技巧详细教程

手机APP/开发
415
0
0
2023-07-01
目录
  • 前言
  • 开发思路
  • 先上效果
  • 开发细节
  • 使用RawKeyboardListener
  • Provider层对事件进行处理
  • 注意
  • 总结
  • 文件参考
  • TV keyCode详解

前言

最近公司有了新的业务,把现有Flutter Android项目应用到TV上去,这不,Asscre的活就来了。

本文详细说明Flutter for TV的两种实现方式,能力有限,不足之处欢迎指点,哈哈哈


开发思路

在开发之前,我们先设定一下我们的思路。

即,如何对原有程序代码侵入式最小、性能最佳、可玩性更高做出设定。

那么,通过上面的设定,我们在Flutter Widget中就发现了两个东西:

  • RawKeyboardListener
  • InkWell和其他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:,
    width:,
    decoration: BoxDecoration(
      border: Border.all(
          width:,
          color: d.focusNode.hasFocus ? Colors.blue : Colors.transparent),
      borderRadius: BorderRadius.circular(),
      color: Colors.white.withAlpha(),
    ),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Image.asset(
          d.img,
          height:,
        ),
        SizedBox(height:),
        Text(
          d.name,
          style: TextStyle(
            color: Colors.white,
            fontSize:,
          ),
        ),
      ],
    ),
  ),
),

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 =; // 按钮距离屏幕右侧最大边界
  double minWScreen =.w; // 按钮距离屏幕最左侧距离边界
  final List<HomePageMakeBtn> makeBtnList = [
    HomePageMakeBtn('lib/assets/img/youtube.png', 'You Tube', '', 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 -.w;
    // setMakeFocusAddListener();
    if (!init) {
      makeBtnList.first.focusNode.requestFocus();
      init = true;
    }
  }
  setMakeFocusAddListener() {
    for (int i =; 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 == || _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 ==) {
          await keyCodeDpadLeft(context, param);
        }
        /// 对右键进行处理
        if (_d.keyCode ==) {
          await keyCodeDpadRight(context, param);
        }
        notifyListeners();
      }
    }
  }
  /// 对左键进行处理
  keyCodeDpadLeft(BuildContext context, HomePageMakeBtn param) async {
    /// 首位边界处理
    final int _idx = makeBtnList.indexWhere((e) => e == param);
    if (_idx ==) return;
    final int _nextIndex = _idx +;
    if ((_nextIndex %) == 1) {
      HomePageMakeBtn _nextNode = makeBtnList[_idx -];
      print(_nextNode.name);
      await Future.delayed(const Duration(milliseconds:));
      _nextNode.focusNode.requestFocus();
    }
  }
  /// 对右键进行处理
  keyCodeDpadRight(BuildContext context, HomePageMakeBtn param) async {
    final int _idx = makeBtnList.indexWhere((e) => e == param);
    /// 末位边界处理
    if (_idx == (makeBtnList.length -)) return;
    final int _nextIndex = _idx +;
    if ((_nextIndex %) == 0) {
      HomePageMakeBtn _nextNode = makeBtnList[_nextIndex];
      await Future.delayed(const Duration(milliseconds:));
      _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: KEY_UP
  /// case: KEY_DOWN
  /// case: KEY_LEFT
  /// case: KEY_RIGHT
  /// case: 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(),
  child: InkWell(
    focusColor: Colors.deepOrange.withAlpha(),
    onTap: () => Navigator.of(context)
        .push(MaterialPageRoute(builder: (_) => MemoryPage(title: d.name))),
    child: SizedBox(
      height:,
      width:,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Image.asset(
            d.img,
            height:,
          ),
          SizedBox(height:),
          Text(
            d.name,
            style: TextStyle(
              color: Colors.white,
              fontSize:,
            ),
          ),
        ],
      ),
    ),
  ),
);

总结

上述两种解决方案中,大家可以根据自己(boss)的喜好或者业务需求选择一种使用。

在需要复杂的自定义的业务情况下,推荐使用RawKeyboardListener的解决方案,可以做出很多酷炫的效果,譬如按键事件触发时,focus住的widget可以做出放大、渐变等等效果,这有助于提升用户的体验。

但,要是在现有的业务逻辑上,在少量调整后就可使用上述中的InkWell的解决方案。

文件参考

TV keyCode详解

name

keycode

说明

KEYCODE_UNKNOWN

0


---------------------------------------

-----

--------------

KEYCODE_SOFT_LEFT

1


KEYCODE_SOFT_RIGHT

2


KEYCODE_HOME

3

HOME键

KEYCODE_BACK

4

返回键

KEYCODE_CALL

5

拨号键

KEYCODE_ENDCALL

6

挂机键

KEYCODE_0

7


KEYCODE_1

8


KEYCODE_2

9


KEYCODE_3

10


KEYCODE_4

11


KEYCODE_5

12


KEYCODE_6

13


KEYCODE_7

14


KEYCODE_8

15


KEYCODE_9

16


KEYCODE_STAR

17

按键 *

KEYCODE_POUND

18

按键 #

KEYCODE_DPAD_UP

19

向上

KEYCODE_DPAD_DOWN

20

向下

KEYCODE_DPAD_LEFT

21

向左

KEYCODE_DPAD_RIGHT

22

向右

KEYCODE_DPAD_CENTER

23

确定键

KEYCODE_VOLUME_UP

24

音量增加键

KEYCODE_VOLUME_DOWN

25

音量减小键

KEYCODE_POWER

26

电源键

KEYCODE_CAMERA

27

拍照键

KEYCODE_CLEAR

28


KEYCODE_A

29


KEYCODE_B

30


KEYCODE_C

31


KEYCODE_D

32


KEYCODE_E

33


KEYCODE_F

34


KEYCODE_G

35


KEYCODE_H

36


KEYCODE_I

37


KEYCODE_J

38


KEYCODE_K

39


KEYCODE_L

40


KEYCODE_M

41


KEYCODE_N

42


KEYCODE_O

43


KEYCODE_P

44


KEYCODE_Q

45


KEYCODE_R

46


KEYCODE_S

47


KEYCODE_T

48


KEYCODE_U

49


KEYCODE_V

50


KEYCODE_W

51


KEYCODE_X

52


KEYCODE_Y

53


KEYCODE_Z

54


KEYCODE_COMMA

55

按键 ,

KEYCODE_PERIOD

56

按键 .

KEYCODE_ALT_LEFT

57


KEYCODE_ALT_RIGHT

58


KEYCODE_SHIFT_LEFT

59


KEYCODE_SHIFT_RIGHT

60


KEYCODE_TAB

61

Tab键

KEYCODE_SPACE

62

空格键

KEYCODE_SYM

63


KEYCODE_EXPLORER

64


KEYCODE_ENVELOPE

65


KEYCODE_ENTER

66

回车键

KEYCODE_DEL

67

退格键

KEYCODE_GRAVE

68

按键 `

KEYCODE_MINUS

69

按键-

KEYCODE_EQUALS

70

按键 =

KEYCODE_LEFT_BRACKET

71

按键 [

KEYCODE_RIGHT_BRACKET

72

按键 ]

KEYCODE_BACKSLASH

73

按键 \

KEYCODE_SEMICOLON

74

按键 ,

KEYCODE_APOSTROPHE

75

按键 ''单引号

KEYCODE_SLASH

76

按键 /

KEYCODE_AT

77

按键 @

KEYCODE_NUM

78


KEYCODE_HEADSETHOOK

79


KEYCODE_FOCUS

80

拍照对焦键

KEYCODE_PLUS

81

按键+

KEYCODE_MENU

82

菜单键

KEYCODE_NOTIFICATION

83

通知键

KEYCODE_SEARCH

84


KEYCODE_MEDIA_PLAY_PAUSE

85

多媒体键 播放/暂停

KEYCODE_MEDIA_STOP

86

多媒体键 暂停

KEYCODE_MEDIA_NEXT

87

多媒体键 下一首

KEYCODE_MEDIA_PREVIOUS

88

多媒体键 上一首

KEYCODE_MEDIA_REWIND

89

多媒体键 快退

KEYCODE_MEDIA_FAST_FORWARD

90

多媒体键 快进

KEYCODE_MUTE

91

话筒静音键

KEYCODE_PAGE_UP

92

向上翻页键

KEYCODE_PAGE_DOWN

93

向下翻页键

KEYCODE_PICTSYMBOLS

94


KEYCODE_SWITCH_CHARSET

95


KEYCODE_BUTTON_A

96


KEYCODE_BUTTON_B

97


KEYCODE_BUTTON_C

98


KEYCODE_BUTTON_X

99


KEYCODE_BUTTON_Y

100


KEYCODE_BUTTON_Z

101


KEYCODE_BUTTON_L1

102


KEYCODE_BUTTON_R1

103


KEYCODE_BUTTON_L2

104


KEYCODE_BUTTON_R2

105


KEYCODE_BUTTON_THUMBL

106


KEYCODE_BUTTON_THUMBR

107


KEYCODE_BUTTON_START

108


KEYCODE_BUTTON_SELECT

109


KEYCODE_BUTTON_MODE

110


KEYCODE_ESCAPE

111

ESC键

KEYCODE_FORWARD_DEL

112

删除键

KEYCODE_CTRL_LEFT

113


KEYCODE_CTRL_RIGHT

114


KEYCODE_CAPS_LOCK

115

大写锁定键

KEYCODE_SCROLL_LOCK

116


KEYCODE_META_LEFT

117


KEYCODE_META_RIGHT

118


KEYCODE_FUNCTION

119


KEYCODE_SYSRQ

120


KEYCODE_BREAK

121

Break/Pause键

KEYCODE_MOVE_HOME

122

光标移动到开始键

KEYCODE_MOVE_END

123

光标移动到末尾键

KEYCODE_INSERT

124


KEYCODE_FORWARD

125


KEYCODE_MEDIA_PLAY

126

多媒体键 播放

KEYCODE_MEDIA_PAUSE

127

多媒体键 暂停

KEYCODE_MEDIA_CLOSE

128

多媒体键 关闭

KEYCODE_MEDIA_EJECT

129

多媒体键 弹出

KEYCODE_MEDIA_RECORD

130

多媒体键 录音

KEYCODE_F1

131


KEYCODE_F2

132


KEYCODE_F3

133


KEYCODE_F4

134


KEYCODE_F5

135


KEYCODE_F6

136


KEYCODE_F7

137


KEYCODE_F8

138


KEYCODE_F9

139


KEYCODE_F10

140


KEYCODE_F11

141


KEYCODE_F12

142


KEYCODE_NUM_LOCK

143

小键盘锁

KEYCODE_NUMPAD_0

144


KEYCODE_NUMPAD_1

145


KEYCODE_NUMPAD_2

146


KEYCODE_NUMPAD_3

147


KEYCODE_NUMPAD_4

148


KEYCODE_NUMPAD_5

149


KEYCODE_NUMPAD_6

150


KEYCODE_NUMPAD_7

151


KEYCODE_NUMPAD_8

152


KEYCODE_NUMPAD_9

153


KEYCODE_NUMPAD_DIVIDE

154


KEYCODE_NUMPAD_MULTIPLY

155


KEYCODE_NUMPAD_SUBTRACT

156


KEYCODE_NUMPAD_ADD

157


KEYCODE_NUMPAD_DOT

158


KEYCODE_NUMPAD_COMMA

159


KEYCODE_NUMPAD_ENTER

160


KEYCODE_NUMPAD_EQUALS

161


KEYCODE_NUMPAD_LEFT_PAREN

162


KEYCODE_NUMPAD_RIGHT_PAREN

163


KEYCODE_VOLUME_MUTE

164

扬声器静音键

KEYCODE_INFO

165


KEYCODE_CHANNEL_UP

166


KEYCODE_CHANNEL_DOWN

167


KEYCODE_ZOOM_IN

168

放大键

KEYCODE_ZOOM_OUT

169

缩小键

KEYCODE_TV

170


KEYCODE_WINDOW

171


KEYCODE_GUIDE

172


KEYCODE_DVR

173


KEYCODE_BOOKMARK

174


KEYCODE_CAPTIONS

175


KEYCODE_SETTINGS

176


KEYCODE_TV_POWER

177


KEYCODE_TV_INPUT

178


KEYCODE_STB_POWER

179


KEYCODE_STB_INPUT

180


KEYCODE_AVR_POWER

181


KEYCODE_AVR_INPUT

182


KEYCODE_PROG_RED

183


KEYCODE_PROG_GREEN

184


KEYCODE_PROG_YELLOW

185


KEYCODE_PROG_BLUE

186


KEYCODE_APP_SWITCH

187


KEYCODE_BUTTON_1

188


KEYCODE_BUTTON_2

189


KEYCODE_BUTTON_3

190


KEYCODE_BUTTON_4

191


KEYCODE_BUTTON_5

192


KEYCODE_BUTTON_6

193


KEYCODE_BUTTON_7

194


KEYCODE_BUTTON_8

195


KEYCODE_BUTTON_9

196


KEYCODE_BUTTON_10

197


KEYCODE_BUTTON_11

198


KEYCODE_BUTTON_12

199


KEYCODE_BUTTON_13

200


KEYCODE_BUTTON_14

201


KEYCODE_BUTTON_15

202


KEYCODE_BUTTON_16

203


KEYCODE_LANGUAGE_SWITCH

204


KEYCODE_MANNER_MODE

205


KEYCODE_3D_MODE

206


KEYCODE_CONTACTS

207


KEYCODE_CALENDAR

208


KEYCODE_MUSIC

209


KEYCODE_CALCULATOR

210


KEYCODE_ZENKAKU_HANKAKU

211


KEYCODE_EISU

212


KEYCODE_MUHENKAN

213


KEYCODE_HENKAN

214


KEYCODE_KATAKANA_HIRAGANA

215


KEYCODE_YEN

216


KEYCODE_RO

217


KEYCODE_KANA

218


KEYCODE_ASSIST

219


KEYCODE_BRIGHTNESS_DOWN

220


KEYCODE_BRIGHTNESS_UP

221


KEYCODE_MEDIA_AUDIO_TRACK

222


KEYCODE_SLEEP

223


KEYCODE_WAKEUP

224


KEYCODE_PAIRING

225


KEYCODE_MEDIA_TOP_MENU

226


KEYCODE_11

227


KEYCODE_12

228


KEYCODE_LAST_CHANNEL

229


KEYCODE_TV_DATA_SERVICE

230


KEYCODE_VOICE_ASSIST

231


KEYCODE_TV_RADIO_SERVICE

232


KEYCODE_TV_TELETEXT

233


KEYCODE_TV_NUMBER_ENTRY

234


KEYCODE_TV_TERRESTRIAL_ANALOG

235


KEYCODE_TV_TERRESTRIAL_DIGITAL

236


KEYCODE_TV_SATELLITE

237


KEYCODE_TV_SATELLITE_BS

238


KEYCODE_TV_SATELLITE_CS

239


KEYCODE_TV_SATELLITE_SERVICE

240


KEYCODE_TV_NETWORK

241


KEYCODE_TV_ANTENNA_CABLE

242


KEYCODE_TV_INPUT_HDMI_1

243


KEYCODE_TV_INPUT_HDMI_2

244


KEYCODE_TV_INPUT_HDMI_3

245


KEYCODE_TV_INPUT_HDMI_4

246


KEYCODE_TV_INPUT_COMPOSITE_1

247


KEYCODE_TV_INPUT_COMPOSITE_2

248


KEYCODE_TV_INPUT_COMPONENT_1

249


KEYCODE_TV_INPUT_COMPONENT_2

250


KEYCODE_TV_INPUT_VGA_1

251


KEYCODE_TV_AUDIO_DESCRIPTION

252


KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP

253


KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN

254


KEYCODE_TV_ZOOM_MODE

255


KEYCODE_TV_CONTENTS_MENU

256


KEYCODE_TV_MEDIA_CONTEXT_MENU

257


KEYCODE_TV_TIMER_PROGRAMMING

258


KEYCODE_HELP

259


KEYCODE_NAVIGATE_PREVIOUS

260


KEYCODE_NAVIGATE_NEXT

261


KEYCODE_NAVIGATE_IN

262


KEYCODE_NAVIGATE_OUT

263


KEYCODE_STEM_PRIMARY

264


KEYCODE_STEM_1

265


KEYCODE_STEM_2

266


KEYCODE_STEM_3

267


KEYCODE_DPAD_UP_LEFT

268


KEYCODE_DPAD_DOWN_LEFT

269


KEYCODE_DPAD_UP_RIGHT

270


KEYCODE_DPAD_DOWN_RIGHT

271


KEYCODE_MEDIA_SKIP_FORWARD

272


KEYCODE_MEDIA_SKIP_BACKWARD

273


KEYCODE_MEDIA_STEP_FORWARD

274


KEYCODE_MEDIA_STEP_BACKWARD

275


KEYCODE_SOFT_SLEEP

276


KEYCODE_CUT

277


KEYCODE_COPY

278


KEYCODE_PASTE

279


KEYCODE_SYSTEM_NAVIGATION_UP

280


KEYCODE_SYSTEM_NAVIGATION_DOWN

281


KEYCODE_SYSTEM_NAVIGATION_LEFT

282


KEYCODE_SYSTEM_NAVIGATION_RIGHT

283