preface

The company recently added a new business, applying the existing Flutter Android project to TV, and that’s where Asscre’s job came in.

This article describes two ways to implement Flutter for TV. There are limited capabilities of Flutter

That what, big brother, first click “like” + favorites! Hey hey

The development train of thought

Before development, let’s set our thinking.

That is, how to make the Settings for the least intrusive, best performance and higher playability of the original program code.

So, based on the Settings above, we find two things in the Flutter Widget:

  • RawKeyboardListener
  • InkWell and other Android TV configurations

The first effect

More playable and malleableRawKeyboardListenerSolution effect

Minimal modification to the original programInkWellAnd other Android TV configuration solutions

The development details

More playable and malleableRawKeyboardListenerThe solution

Using RawKeyboardListener
RawKeyboardListener(
  focusNode: d.focusNode, / / configuration focusNode
  onKey: (RawKeyEvent event) =>
      context.read<HomePageContentWidgetProvider>().focusEventHandler(event, context, d), // Monitor and handle special events
  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() [() [() [() [()Copy the code
The Provider layer handles events
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; // The maximum distance from the button to the right of the screen
  double minWScreen = 60.w; // The button is far from the left edge of the screen

  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 - 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 {
    /// Handles only events when a key is pressed
    if (event.data is RawKeyEventDataAndroid &&
        event.runtimeType.toString() == 'RawKeyDownEvent') {
      CustomRawKeyEventDataAndroid _d =
          CustomRawKeyEventDataAndroid.format(event.data);

      /// Handles pressing the OK key and the center key
      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}');
        // }

        /// The left key is processed
        if (_d.keyCode == 21) {
          await keyCodeDpadLeft(context, param);
        }

        /// Right click processing
        if (_d.keyCode == 22) {
          awaitkeyCodeDpadRight(context, param); } notifyListeners(); }}}/// The left key is processed
  keyCodeDpadLeft(BuildContext context, HomePageMakeBtn param) async {
    /// Primary boundary processing
    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(); }}/// Right click processing
  keyCodeDpadRight(BuildContext context, HomePageMakeBtn param) async {
    final int _idx = makeBtnList.indexWhere((e) => e == param);

    /// Last bit boundary processing
    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) {
    returnCustomRawKeyEventDataAndroid(d.flags, d.codePoint, d.plainCodePoint, d.keyCode, d.scanCode, d.metaState); }}Copy the code
Pay attention to

We can see that we use it for left and right keys

Why is that?

This is because the Flutter mechanism will first trigger the requestFocus once and then the requestFocus twice, which conflicts with our expectation.

Such as:

  • When using the end of the button to the right, the system triggers focus to UUUUU this button, our actual expectation is to YYYYY.

  • When using the key head to the left, it will also cross two focuses.

So far Asscre has not found a good solution, but using await Future Delayed can relieve the inhuman operation.

Minimal modification to the original programInkWellAnd other Android TV configuration solutions

First, we need to set up in androidmanifest.xmlLEANBACK_LAUNCHERTell the platform that our application is a TV application
<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>// Add this sentence<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
Copy the code
We then add in the Main entry fileShortcutsFor our program to respond to our remote control commands.
returnShortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.select): ActivateIntent(), }, child: MaterialApp( ... ) ;Copy the code
Finally, useInkWellTo get the focus setting user remote click effect, whichfocusColorHelp us remind the user of the location of the button at this time.
    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,),),),),),),);Copy the code

conclusion

You can choose one of the two solutions according to your own (boss) preferences or business needs.

The RawKeyboardListener solution is recommended for complex customizations. For example, when a key event is triggered, the focus widget can zoom in, fade out, and so on, which can help improve the user experience.

However, with existing business logic, InkWell’s solution described above could be used with a few adjustments.

File reference

TV keyCode,

name keycode instructions
KEYCODE_UNKNOWN 0
————————————— —– ————–
KEYCODE_SOFT_LEFT 1
KEYCODE_SOFT_RIGHT 2
KEYCODE_HOME 3 HOME button
KEYCODE_BACK 4 Return key
KEYCODE_CALL 5 dial
KEYCODE_ENDCALL 6 Hang up the key
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 Keys *
KEYCODE_POUND 18 Button #
KEYCODE_DPAD_UP 19 upward
KEYCODE_DPAD_DOWN 20 down
KEYCODE_DPAD_LEFT 21 To the left
KEYCODE_DPAD_RIGHT 22 To the right
KEYCODE_DPAD_CENTER 23 Identify key
KEYCODE_VOLUME_UP 24 Volume increase key
KEYCODE_VOLUME_DOWN 25 Volume reduction button
KEYCODE_POWER 26 Power key
KEYCODE_CAMERA 27 Camera button
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 The key,
KEYCODE_PERIOD 56 Buttons.
KEYCODE_ALT_LEFT 57
KEYCODE_ALT_RIGHT 58
KEYCODE_SHIFT_LEFT 59
KEYCODE_SHIFT_RIGHT 60
KEYCODE_TAB 61 The Tab key
KEYCODE_SPACE 62 The blank space key
KEYCODE_SYM 63
KEYCODE_EXPLORER 64
KEYCODE_ENVELOPE 65
KEYCODE_ENTER 66 The enter key
KEYCODE_DEL 67 backspace
KEYCODE_GRAVE 68 The key `
KEYCODE_MINUS 69 The key –
KEYCODE_EQUALS 70 Key =
KEYCODE_LEFT_BRACKET 71 The key [
KEYCODE_RIGHT_BRACKET 72 The key]
KEYCODE_BACKSLASH 73 Button \
KEYCODE_SEMICOLON 74 The key,
KEYCODE_APOSTROPHE 75 Key ” single quotes
KEYCODE_SLASH 76 The key /
KEYCODE_AT 77 Button @
KEYCODE_NUM 78
KEYCODE_HEADSETHOOK 79
KEYCODE_FOCUS 80 Photo focus button
KEYCODE_PLUS 81 Keys +
KEYCODE_MENU 82 menu
KEYCODE_NOTIFICATION 83 Inform the key
KEYCODE_SEARCH 84
KEYCODE_MEDIA_PLAY_PAUSE 85 Multimedia key Play/pause
KEYCODE_MEDIA_STOP 86 Multimedia key pause
KEYCODE_MEDIA_NEXT 87 Multimedia key next
KEYCODE_MEDIA_PREVIOUS 88 Multimedia key on a song
KEYCODE_MEDIA_REWIND 89 The multimedia key resets quickly
KEYCODE_MEDIA_FAST_FORWARD 90 Multimedia key fast forward
KEYCODE_MUTE 91 Microphone mute button
KEYCODE_PAGE_UP 92 Page up key
KEYCODE_PAGE_DOWN 93 Scroll down key
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 The delete key
KEYCODE_CTRL_LEFT 113
KEYCODE_CTRL_RIGHT 114
KEYCODE_CAPS_LOCK 115 Caps lock key
KEYCODE_SCROLL_LOCK 116
KEYCODE_META_LEFT 117
KEYCODE_META_RIGHT 118
KEYCODE_FUNCTION 119
KEYCODE_SYSRQ 120
KEYCODE_BREAK 121 Break/Pause button
KEYCODE_MOVE_HOME 122 The cursor moves to the Start key
KEYCODE_MOVE_END 123 Move the cursor to the end key
KEYCODE_INSERT 124
KEYCODE_FORWARD 125
KEYCODE_MEDIA_PLAY 126 Multimedia Key Play
KEYCODE_MEDIA_PAUSE 127 Multimedia key pause
KEYCODE_MEDIA_CLOSE 128 Multimedia Key Off
KEYCODE_MEDIA_EJECT 129 Multimedia key pop up
KEYCODE_MEDIA_RECORD 130 Multimedia key recording
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 Keypad lock
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 Speaker mute button
KEYCODE_INFO 165
KEYCODE_CHANNEL_UP 166
KEYCODE_CHANNEL_DOWN 167
KEYCODE_ZOOM_IN 168 Zoom in key
KEYCODE_ZOOM_OUT 169 Narrowing the key
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