开发者

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

目录
  • 前言
  • 开发思路
  • 先上效果
  • 开发细节
    • 使用RawKandroideyboardListener
    • Provider层对事件进行处理
    • 注意
  • 总结
    • 文件参考
      • TV keyCode详解

    前言

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

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

    开发思路

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

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

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

    • RawKeyboardListener
    • InkWell和其他Android TV配置

    先上效果

    可玩性、可塑性更高的RawKeyboardListener解决方案效果

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

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

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

    开发细节

    可玩性、可塑性更高的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'开发者_Python教程, '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);
      }
    }
    

    注意

    我们可以看到在处理左键和右键的时候我们用了

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

    这是为什么呢?

    那是因为在实际效果中,我们requestFocus操作的时候,Flutter的机制会首先触发一次requestFocus,然后再触发一次requestFocus,一共两次,这就与我们的预想就有冲突了。

    例如:

    使用按键末尾向右时,系统触发的focus到UUUUU这个按钮,我们的实际预想的是到YYYYY即可。

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

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

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

    目前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详解

    namekeycode说明
    KEYCODE_UNKNOWN0
    ----------------------------------------------------------
    KEYCODE_SOFT_LEFT1
    KEYCODE_SOFT_RIGHT2
    KEYCODE_HOME3HOME键
    KEYCODE_BACK4返回键
    KEYCODE_CALL5拨号键
    KEYCODE_ENDCALL6挂机键
    KEYCODE_07
    KEYCODE_18
    KEYCODE_29
    KEYCODE_310
    KEYCODE_411
    KEYCODE_512
    KEYCODE_613
    KEYCODE_714
    KEYCODE_815
    KEYCODE_916
    KEYCODE_STAR17按键 *
    KEYCODE_POUND18按键 #
    KEYCODE_DPAD_UP19向上
    KEYCODE_DPAD_DOWN20向下
    KEYCODE_DPAD_LEFT21向左
    KEYCODE_DPAD_RIGHT22向右
    KEYCODE_DPAD_CENTER23确定键
    KEYCODE_VOLUME_UP24音量增加键
    KEYCODE_VOLUME_DOWN25音量减小键
    KEYCODE_POWER26电源键
    KEYCODE_CAMERA27拍照键
    KEYCODE_CLEAR28
    KEYCODE_A29
    KEYCODE_B30
    KEYCODE_C31
    KEYCODE_D32
    KEYCODE_E33
    KEYCODE_F34
    KEYCODE_G35
    KEYCODE_H36
    KEYCODE_I37
    KEYCODE_J38
    KEYCODE_K39
    KEYCODE_L40
    KEYCODE_M41
    KEYCODE_N42
    KEYCODE_O43
    KEYCODE_P44
    KEYCODE_Q45
    KEYCODE_R46
    KEYCODE_S47
    KEYCODE_T48
    KEYCODE_U49
    KEYCODE_V50
    KEYCODE_W51
    KEYCODE_X52
    KEYCODE_Y53
    KEYCODE_Z54
    KEYCODE_COMMA55按键 ,
    KEYCODE_PERIOD56按键 .
    KEYCODE_ALT_LEFT57
    KEYCODE_ALT_RIGHT58
    KEYCODE_SHIFT_LEFT59
    KEYCODE_SHIFT_RIGHT60
    KEYCODE_TAB61Tab键
    KEYCODE_SPACE62空格键
    KEYCODE_SYM63
    KEYCODE_EXPLORER64
    KEYCODE_ENVELOPE65
    KEYCODE_ENTER66回车键
    KEYCODE_DEL67退格键
    KEYCODE_GRAVE68按键 `
    KEYCODE_MINUS69按键-
    KEYCODE_EQUALS70按键 =
    KEYCODE_LEFT_BRACKET71按键 [
    KEYCODE_RIGHT_BRACKET72按键 ]
    KEYCODE_BACKSLASH73按键 \
    KEYCODE_SEMICOLON74按键 ,
    KEYCODE_APOSTROPHE75按键 ''单引号
    KEYCODE_SLASH76按键 /
    KEYCODE_AT77按键 @
    KEYCODE_NUM78
    KEYCODE_HEADSETHOOK79
    KEYCODE_FOCUS80拍照对焦键
    KEYCODE_PLUS81按键+
    KEYCODE_MENU82菜单键
    KEYCODE_NOTIFICATION83通知键
    KEYCODE_SEARCH84
    KEYCODE_MEDIA_PLAY_PAUSE85多媒体键 播放/暂停
    KEYCODE_MEDIA_STOP86多媒体键 暂停
    KEYCODE_MEDIA_NEXT87多媒体键 下一首
    KEYCODE_MEDIA_PREVIOUS88多媒体键 上一首
    KEYCODE_MEDIA_REWIND89多媒体键 快退
    KEYCODE_MEDIA_FAST_FORWARD90多媒体键 快进
    KEYCODE_MUTE91话筒静音键
    KEYCODE_PAGE_UP92向上翻页键
    KEYCODE_PAGE_DOWN93向下翻页键
    KEYCODE_PICTSYMBOLS94
    KEYCODE_SWITCH_CHARSET95
    KEYCODE_BUTTON_A96
    KEYCODE_BUTTON_B97
    KEYCODpythonE_BUTTON_C98
    KEYCODE_BUTTON_X99
    KEYCODE_BUTTON_Y100
    KEYCODE_BUTTON_Z101
    KEYCODE_BUTTON_L1102
    KEYCODE_BUTTON_R1103
    KEYCODE_BUTTON_L2104
    KEYCODE_BUTTON_R2105
    KEYCODE_BUTTON_THUMBL106
    KEYCODE_BUTTON_THUMBR107
    KEYCwww.devze.comODE_BUTTON_START108
    KEYCODE_BUTTON_SELECT109
    KEYCODE_BUTTON_MODE110
    KEYCODE_ESCAPE111ESC键
    KEYCODE_FORWARD_DEL112删除键
    KEYCODE_CTRL_LEFT113
    KEYCODE_CTRL_RIGHT114
    KEYCODE_CAPS_LOCK115大写锁定键
    KEYCODE_SCROLL_LOCK116
    KEYCODE_META_LEFT117
    KEYCODE_META_RIGHT118
    KEYCODE_FUNCTION119
    KEYCODE_SYSRQ120
    KEYCODE_BREAK121Break/Pause键
    KEYCODE_MOVE_HOME122光标移动到开始键
    KEYCODE_MOVE_END123光标移动到末尾键
    KEYCODE_INSERT124
    KEYCODE_FORWARD125
    KEYCODE_MEDIA_PLAY126多媒体键 播放
    KEYCODE_MEDIA_PAUSE127多媒体键 暂停
    KEYCODE_MEDIA_CLOSE128多媒体键 关闭
    KEYCODE_MEDIA_EJECT129多媒体键 弹出
    KEYCODE_MEDIA_RECORD130多媒体键 录音
    KEYCODE_F1131
    KEYCODE_F2132
    KEYCODE_F3133
    KEYCODE_F4134
    KEYCODE_F5135
    KEYCODE_F6136
    KEYCODE_F7137
    KEYCODE_F8138
    KEYCODE_F9139
    KEYCODE_F10140
    KEYCODE_F11141
    KEYCODE_F12142
    KEYCODE_NUM_LOCK143小键盘锁
    KEYCODE_NUMPAD_0144
    KEYCODE_NUMPAD_1145
    KEYCODE_NUMPAD_2146
    KEYCODE_NUMPAD_3147
    KEYCODE_NUMPAD_4148
    KEYCODE_NUMPAD_5149
    KEYCODE_NUMPAD_6150
    KEYCODE_NUMPAD_7151
    KEYCODE_NUMPAD_8152
    KEYCODE_NUMPAD_9153
    KEYCODE_NUMPAD_DIVIDE154
    KEYCODE_NUMPAD_MULTIPLY155
    KEYCODE_NUMPAD_SUBTRACT156
    KEYCODE_NUMPAD_ADD157
    KEYCODE_NUMPAD_DOT158
    KEYCODE_NUMPAD_COMMA159
    KEYCODE_NUMPAD_ENTER160
    KEYCODE_NUMPAD_EQUALS161
    KEYCODE_NUMPAD_LEFT_PAREN162
    KEYCODE_NUMPAD_RIGHT_PAREN163
    KEYCODE_VOLUME_MUTE164扬声器静音键
    KEYCODE_INFO165
    KEYCODE_CHANNEL_UP166
    KEYCODE_CHANNEL_DOWN167
    KEYCODE_ZOOM_IN168放大键
    KEYCODE_ZOOM_OUT169缩小键
    KEYCODE_TV170
    KEYCODE_WINDOW171
    KEYCODE_GUIDE172
    KEYCODE_DVR173
    KEYCODE_BOOKMARK174
    KEYCODE_CAPTIONS175
    KEYCODE_SETTINGS176
    KEYCODE_TV_POWER177
    KEYCODE_TV_INPUT178
    KEYCODE_STB_POWER179
    KEYCODE_STB_INPUT180
    KEYCODE_AVR_POWER181
    KEYCODE_AVR_INPUT182
    KEYCODE_PROG_RED183
    KEYCODE_PROG_GREEN184
    KEYCODE_PROG_YELLOW185
    KEYCODE_PROG_BLUE186
    KEYCODE_APP_SWITCH187
    KEYCODE_BUTTON_1188
    KEYCODE_BUTTON_2189
    KEYCODE_BUTTON_3190
    KEYCODE_BUTTON_4191
    KEYCODE_BUTTON_5192
    KEYCODE_BUTTON_6193
    KEYCODE_BUTTON_7194
    KEYCODE_BUTTON_8195
    KEYCODE_BUTTON_9196
    KEYCODE_BUTTON_10197
    KEYCODE_BUTTON_11198
    KEYCODE_BUTTON_12199
    KEYCODE_BUTTON_13200
    KEYCODE_BUTTON_14201
    KEYCODE_BUTTON_15202
    KEYCODE_BUTTON_16203
    KEYCODE_LANGUAGE_SWITCH204
    KEYCODE_MANNER_MODE205
    KEYCODE_3D_MODE206
    KEYCODE_CONTACTS207
    KEYCODE_CALENDAR208
    KEYCODE_MUSIC209
    KEYCODE_CALCULATOR210
    KEYCODE_ZENKAKU_HANKAKU211
    KEYCODE_EISU212
    KEYCODE_MUHENKAN213
    KEYCODE_HENKAN214
    KEYCODE_KATAKANA_HIRAGANA215
    KEYCODE_YEN216
    KEYCODE_RO217
    KEYCODE_KANA218
    KEYCODE_ASSIST219
    KEYCODE_BRIGHTNESS_DOWN220
    KEYCODE_BRIGHTNESS_UP221
    KEYCODE_MEDIA_AUDIO_TRACK222
    KEYCODE_SLEEP223
    KEYCODE_WAKEUP224
    KEYCODE_PAIRING225
    KEYCODE_MEDIA_TOP_MENU226
    KEYCODE_11227
    KEYCODE_12228
    KEYCODE_LAST_CHANNEL229
    KEYCODE_TV_DATA_SERVICE230
    KEYCODE_VOICE_ASSIST231
    KEYCODE_TV_RADIO_SERVICE232
    KEYCODE_TV_TELETEXT233
    KEYCODE_TV_NUMBER_ENTRY234
    KEYCODE_TV_TERRESTRIAL_ANwww.devze.comALOG235
    KEYCODE_TV_TERRESTRIAL_DIGITAL236
    KEYCODE_TV_SATELLITE237
    KEYCODE_TV_SATELLITE_BS238
    KEYCODE_TV_SATELLITE_CS239
    KEYCODE_TV_SATELLITE_SERVICE240
    KEYCODE_TV_NETWORK241
    KEYCODE_TV_ANTENNA_CABLE242
    KEYCODE_TV_INPUT_HDMI_1243
    KEYCODE_TV_INPUT_HDMI_2244
    KEYCODE_TV_INPUT_HDMI_3245
    KEYCODE_TV_INPUT_HDMI_4246
    KEYCODE_TV_INPUT_COMPOSITE_1247
    KEYCODE_TV_INPUT_COMPOSITE_2248
    KEYCODE_TV_INPUT_COMPONENT_1249
    KEYCODE_TV_INPUT_COMPONENT_2250
    KEYCODE_TV_INPUT_VGA_1251
    KEYCODE_TV_AUDIO_DESCRIPTION252
    KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP253
    KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN254
    KEYCODE_TV_ZOOM_MODE255
    KEYCODE_TV_CONTENTS_MENU256
    KEYCODE_TV_MEDIA_CONTEXT_MENU257
    KEYCODE_TV_TIMER_PROGRAMMING258
    KEYCODE_HELP259
    KEYCODE_NAVIGATE_PREVIOUS260
    KEYCODE_NAVIGATE_NEXT261
    KEYCODE_NAVIGATE_IN262
    KEYCODE_NAVIGATE_OUT263
    KEYCODE_STEM_PRIMARY264
    KEYCODE_STEM_1265
    KEYCODE_STEM_2266
    KEYCODE_STEM_3267
    KEYCODE_DPAD_UP_LEFT268
    KEYCODE_DPAD_DOWN_LEFT269
    KEYCODE_DPAD_UP_RIGHT270
    KEYCODE_DPAD_DOWN_RIGHT271
    KEYCODE_MEDIA_SKIP_FORWARD272
    KEYCODE_MEDIA_SKIP_BACKWARD273
    KEYCODE_MEDIA_STEP_FORWARD274
    KEYCODE_MEDIA_STEP_BACKWARD275
    KEYCODE_SOFT_SLEEP276
    KEYCODE_CUT277
    KEYCODE_COPY278
    KEYCODE_PASTE279
    KEYCODE_SYSTEM_NAVIGATION_UP280
    KEYCODE_SYSTEM_NAVIGATION_DOWN281
    KEYCODE_SYSTEM_NAVIGATION_LEFT282
    KEYCODE_SYSTEM_NAVIGATION_RIGHT283

    以上就是Flutter TV Android端开发技巧详细教程的详细内容,更多关于Flutter TV Android端开发的资料请关注我们其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜