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

2022-12-09 15:39:03

目录前言开发思路先上效果开发细节使用RawKeyboardListenerProvider层对事件进行处理注意总结文件参考TVkeyCode详解前言最近公司有了新的业务,把现有FlutterAnd...

目录
前言
开发思路
先上效果
开发细节
使用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', '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_LEFT1KEYCODE_SOFT_RIGHT2KEYCODE_HOME3HOME键KEYCODE_BACK4返回键KEYCODE_CALL5拨号键KEYCODE_ENDCALL6挂机键KEYCODE_07KEYCODE_18KEYCODE_29KEYCODE_310KEYCODE_411KEYCODE_512KEYCODE_613KEYCODE_714KEYCODE_815KEYCODE_916KEYCODE_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_CLEAR28KEYCODE_A29KEYCODE_B30KEYCODE_C31KEYCODE_D32KEYCODE_E33KEYCODE_F34KEYCODE_G35KEYCODE_H36KEYCODE_I37KEYCODE_J38KEYCODE_K39KEYCODE_L40KEYCODE_M41KEYCODE_N42KEYCODE_O43KEYCODE_P44KEYCODE_Q45KEYCODE_R46KEYCODE_S47KEYCODE_T48KEYCODE_U49KEYCODE_V50KEYCODE_W51KEYCODE_X52KEYCODE_Y53KEYCODE_Z54KEYCODE_COMMA55按键 ,KEYCODE_PERIOD56按键 .KEYCODE_ALT_LEFT57KEYCODE_ALT_RIGHT58KEYCODE_SHIFT_LEFT59KEYCODE_SHIFT_RIGHT60KEYCODE_TAB61Tab键KEYCODE_SPACE62空格键KEYCODE_SYM63KEYCODE_EXPLORER64KEYCODE_ENVELOPE65KEYCODE_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_NUM78KEYCODE_HEADSETHOOK79KEYCODE_FOCUS80拍照对焦键KEYCODE_PLUS81按键+KEYCODE_MENU82菜单键KEYCODE_NOTIFICATION83通知键KEYCODE_SEARCH84KEYCODE_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_PICTSYMBOLS94KEYCODE_SWITCH_CHARSET95KEYCODE_BUTTON_A96KEYCODE_BUTTON_B97KEYCODpythonE_BUTTON_C98KEYCODE_BUTTON_X99KEYCODE_BUTTON_Y100KEYCODE_BUTTON_Z101KEYCODE_BUTTON_L1102KEYCODE_BUTTON_R1103KEYCODE_BUTTON_L2104KEYCODE_BUTTON_R2105KEYCODE_BUTTON_THUMBL106KEYCODE_BUTTON_THUMBR107KEYCODE_BUTTON_START108KEYCODE_BUTTON_SELECT109KEYCODE_BUTTON_MODE110KEYCODE_ESCAPE111ESC键KEYCODE_FORWARD_DEL112删除键KEYCODE_CTRL_LEFT113KEYCODE_CTRL_RIGHT114KEYCODE_CAPS_LOCK115大写锁定键KEYCODE_SCROLL_LOCK116KEYCODE_META_LEFT117KEYCODE_META_RIGHT118KEYCODE_FUNCTION119KEYCODE_SYSRQ120KEYCODE_BREAK121Break/Pause键KEYCODE_MOVE_HOME122光标移动到开始键KEYCODE_MOVE_END123光标移动到末尾键KEYCODE_INSERT124KEYCODE_FORWARD125KEYCODE_MEDIA_PLAY126多媒体键 播放KEYCODE_MEDIA_PAUSE127多媒体键 暂停KEYCODE_MEDIA_CLOSE128多媒体键 关闭KEYCODE_MEDIA_EJECT129多媒体键 弹出KEYCODE_MEDIA_RECORD130多媒体键 录音KEYCODE_F1131KEYCODE_F2132KEYCODE_F3133KEYCODE_F4134KEYCODE_F5135KEYCODE_F6136KEYCODE_F7137KEYCODE_F8138KEYCODE_F9139KEYCODE_F10140KEYCODE_F11141KEYCODE_F12142KEYCODE_NUM_LOCK143小键盘锁KEYCODE_NUMPAD_0144KEYCODE_NUMPAD_1145KEYCODE_NUMPAD_2146KEYCODE_NUMPAD_3147KEYCODE_NUMPAD_4148KEYCODE_NUMPAD_5149KEYCODE_NUMPAD_6150KEYCODE_NUMPAD_7151KEYCODE_NUMPAD_8152KEYCODE_NUMPAD_9153KEYCODE_NUMPAD_DIVIDE154KEYCODE_NUMPAD_MULTIPLY155KEYCODE_NUMPAD_SUBTRACT156KEYCODE_NUMPAD_ADD157KEYCODE_NUMPAD_DOT158KEYCODE_NUMPAD_COMMA159KEYCODE_NUMPAD_ENTER160KEYCODE_NUMPAD_EQUALS161KEYCODE_NUMPAD_LEFT_PAREN162KEYCODE_NUMPAD_RIGHT_PAREN163KEYCODE_VOLUME_MUTE164扬声器静音键KEYCODE_INFO165KEYCODE_CHANNEL_UP166KEYCODE_CHANNEL_DOWN167KEYCODE_ZOOM_IN168放大键KEYCODE_ZOOM_OUT169缩小键KEYCODE_TV170KEYCODE_WINDOW171KEYCODE_GUIDE172KEYCODE_DVR173KEYCODE_BOOKMARK174KEYCODE_CAPTIONS175KEYCODE_SETTINGS176KEYCODE_TV_POWER177KEYCODE_TV_INPUT178KEYCODE_STB_POWER179KEYCODE_STB_INPUT180KEYCODE_AVR_POWER181KEYCODE_AVR_INPUT182KEYCODE_PROG_RED183KEYCODE_PROG_GREEN184KEYCODE_PROG_YELLOW185KEYCODE_PROG_BLUE186KEYCODE_APP_SWITCH187KEYCODE_BUTTON_1188KEYCODE_BUTTON_2189KEYCODE_BUTTON_3190KEYCODE_BUTTON_4191KEYCODE_BUTTON_5192KEYCODE_BUTTON_6193KEYCODE_BUTTON_7194KEYCODE_BUTTON_8195KEYCODE_BUTTON_9196KEYCODE_BUTTON_10197KEYCODE_BUTTON_11198KEYCODE_BUTTON_12199KEYCODE_BUTTON_13200KEYCODE_BUTTON_14201KEYCODE_BUTTON_15202KEYCODE_BUTTON_16203KEYCODE_LANGUAGE_SWITCH204KEYCODE_MANNER_MODE205KEYCODE_3D_MODE206KEYCODE_CONTACTS207KEYCODE_CALENDAR208KEYCODE_MUSIC209KEYCODE_CALCULATOR210KEYCODE_ZENKAKU_HANKAKU211KEYCODE_EISU212KEYCODE_MUHENKAN213KEYCODE_HENKAN214KEYCODE_KATAKANA_HIRAGANA215KEYCODE_YEN216KEYCODE_RO217KEYCODE_KANA218KEYCODE_ASSIST219KEYCODE_BRIGHTNESS_DOWN220KEYCODE_BRIGHTNESS_UP221KEYCODE_MEDIA_AUDIO_TRACK222KEYCODE_SLEEP223KEYCODE_WAKEUP224KEYCODE_PAIRING225KEYCODE_MEDIA_TOP_MENU226KEYCODE_11227KEYCODE_12228KEYCODE_LAST_CHANNEL229KEYCODE_TV_DATA_SERVICE230KEYCODE_VOICE_ASSIST231KEYCODE_TV_RADIO_SERVICE232KEYCODE_TV_TELETEXT233KEYCODE_TV_NUMBER_ENTRY234KEYCODE_TV_TERRESTRIAL_ANALOG235KEYCODE_TV_TERRESTRIAL_DIGITAL236KEYCODE_TV_SATELLITE237KEYCODE_TV_SATELLITE_BS238KEYCODE_TV_SATELLITE_CS239KEYCODE_TV_SATELLITE_SERVICE240KEYCODE_TV_NETWORK241KEYCODE_TV_ANTENNA_CABLE242KEYCODE_TV_INPUT_HDMI_1243KEYCODE_TV_INPUT_HDMI_2244KEYCODE_TV_INPUT_HDMI_3245KEYCODE_TV_INPUT_HDMI_4246KEYCODE_TV_INPUT_COMPOSITE_1247KEYCODE_TV_INPUT_COMPOSITE_2248KEYCODE_TV_INPUT_COMPONENT_1249KEYCODE_TV_INPUT_COMPONENT_2250KEYCODE_TV_INPUT_VGA_1251KEYCODE_TV_AUDIO_DESCRIPTION252KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP253KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN254KEYCODE_TV_ZOOM_MODE255KEYCODE_TV_CONTENTS_MENU256KEYCODE_TV_MEDIA_CONTEXT_MENU257KEYCODE_TV_TIMER_PROGRAMMING258KEYCODE_HELP259KEYCODE_NAVIGATE_PREVIOUS260KEYCODE_NAVIGATE_NEXT261KEYCODE_NAVIGATE_IN262KEYCODE_NAVIGATE_OUT263KEYCODE_STEM_PRIMARY264KEYCODE_STEM_1265KEYCODE_STEM_2266KEYCODE_STEM_3267KEYCODE_DPAD_UP_LEFT268KEYCODE_DPAD_DOWN_LEFT269KEYCODE_DPAD_UP_RIGHT270KEYCODE_DPAD_DOWN_RIGHT271KEYCODE_MEDIA_SKIP_FORWARD272KEYCODE_MEDIA_SKIP_BACKWARD273KEYCODE_MEDIA_STEP_FORWARD274KEYCODE_MEDIA_STEP_BACKWARD275KEYCODE_SOFT_SLEEP276KEYCODE_CUT277KEYCODE_COPY278KEYCODE_PASTE279KEYCODE_SYSTEM_NAVIGATION_UP280KEYCODE_SYSTEM_NAVIGATION_DOWN281KEYCODE_SYSTEM_NAVIGATION_LEFT282KEYCODE_SYSTEM_NAVIGATION_RIGHT283

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