Skip to content

Flutter TV应用的开发尝试

fantasy edited this page Mar 16, 2019 · 2 revisions

Flutter主要是进行移动应用开发外,最近尝试了下Flutter开发TV应用。虽然写出来了,效果也还可以,体验流畅,自动适配。不过开发成本还是挺高的,按键监听、焦点处理和焦点框处理比较麻烦,由于Google官方并没有推出Flutter TV应用的SDK,所以暂时还是不要用Flutter编写TV应用了,使用原生leanback或者B/S结构开发吧,等官方推出后可以继续尝试对比使用。接下来,就分享下其中的技术点。本文将主要介绍:

  • Flutter TV应用开发主要难点
  • Flutter TV应用开发按键监听
  • Flutter TV应用开发焦点处理
  • Flutter TV应用开发焦点框效果处理

开发的运行效果图

由于Google官方并没有退出TV版Flutter SDK,所以用Flutter尝试编写TV应用,主要是焦点框和焦点的处理,其他的和手机应用差别不大。按键监听、焦点框和焦点处理比较麻烦,所以Flutter的TV应用开发还不成熟,体验还不错,很流畅,开发成本比较高,还是用原生leanback开发可能要快一些。

Fast to Study Flutter And Dart. QQ群:979966470

下面为效果图:

下面为效果图:

运行效果

运行效果

运行效果

手机上也可以自动适配:

手机端运行效果

运行视频:https://github.com/flutteranddart/flutterTV/blob/master/device.webm

APK下载体验地址:https://github.com/flutteranddart/flutterTV/blob/master/app-release.apk?raw=true

开发的主要难点

其实其他地方和Flutter开发移动应用基本没区别,主要就是按键监听、焦点框和焦点处理。原生Android的控件就默认有焦点的处理属性,直接配置使用即可。

//焦点处理
android:focusable="true"
//触摸模式下是否可以点击,可选可不选
android:focusableInTouchMode="true"

Flutter开发TV应用就要自己处理按键监听、焦点和焦点框了,比较的麻烦,处理好了这几个问题,开发起来也就没太大难度了。

Flutter TV应用开发按键监听

Flutter Widget能够监听到我们的遥控器或者手机端的按键事件的前提是这个Widget已经获取了焦点才可以。获取焦点后面会讲到,这里暂时不提了。 按键监听需要使用RawKeyboardListener这个Widget,构造方法如下:

const RawKeyboardListener({
    Key key,
    @required this.focusNode,//焦点结点
    @required this.onKey,//按键接收处理事件
    @required this.child,//接收焦点的子控件
  })

很简单给个例子:

FocusNode focusNode0 = FocusNode();

... ...

RawKeyboardListener(
      focusNode: focusNode0,
      child: Container(
        decoration: getCircleDecoration(color0),
        child: Padding(
          child: Card(
            elevation: 5,
            shape: CircleBorder(),
            child: CircleAvatar(
              child: Text(''),
              backgroundImage: AssetImage("assets/icon_tv.png"),
              radius: radius,
            ),
          ),
          padding: EdgeInsets.all(padding),
        ),
      ),
      onKey: (RawKeyEvent event) {
        if (event is RawKeyDownEvent && event.data is RawKeyEventDataAndroid) {
          RawKeyDownEvent rawKeyDownEvent = event;
          RawKeyEventDataAndroid rawKeyEventDataAndroid = rawKeyDownEvent.data;
          print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
          switch (rawKeyEventDataAndroid.keyCode) {
            case 19: //KEY_UP
              FocusScope.of(context).requestFocus(_focusNode);
              break;
            case 20: //KEY_DOWN
              break;
            case 21: //KEY_LEFT
              FocusScope.of(context).requestFocus(focusNode4);
              break;
            case 22: //KEY_RIGHT
              FocusScope.of(context).requestFocus(focusNode1);
              break;
            case 23: //KEY_CENTER
              break;
            default:
              break;
          }
        }
      },
    )

这样就实现了我们的Card Widget监听我们的按键事件,遥控器、手机的按键都能监听到。

Flutter TV应用开发焦点处理

Flutter TV的Widget获取焦点的处理通过FocusScope这个Widget处理。 主动获取焦点代码如下:

FocusNode focusNode0 = FocusNode();
... ...
//主动获取焦点
FocusScope.of(context).requestFocus(focusNode0);
//自动获取焦点
FocusScope.of(context).autofocus(focusNode0);

这样就可以了进行焦点获取处理了。FocusNode这个类也很重要,负责监听焦点的工作。

Flutter TV应用开发焦点框效果处理

有了焦点、按键事件监听,剩下的就是选中的焦点框效果的实现了,主要原理这里使用的是用边框,然后动态设置边框颜色就实现了焦点框选中显示,移走不显示的效果。例如选中后焦点框颜色设置为黄色、未选中时就设置为透明色,通过setState({...})进行刷新页面。

  FocusNode focusNode0 = FocusNode();
  ...

//改变颜色状态,刷新页面
  _setDecorationBorder0() {
    setState(() {
      if (focusNode0.hasFocus) {
        colorB0 = Colors.orange;
      } else {
        colorB0 = Colors.transparent;
      }
    });
  }

...
//为FocusNode添加监听事件方法
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    focusNode0.addListener(_setDecorationBorder0);
  }
...
//销毁时要注销监听
  @override
  void dispose() {
    super.dispose();
    focusNode0.removeListener(_setDecorationBorder0);
    focusNode0.dispose();
  }

RawKeyboardListener(
    //焦点监听
      focusNode: focusNode0,
      child: Container(
        //这里就是设置焦点框效果的地方
        decoration: getCircleDecoration(color0),
        child: Padding(
          child: Card(
            elevation: 5,
            shape: CircleBorder(),
            child: CircleAvatar(
              child: Text(''),
              backgroundImage: AssetImage("assets/icon_tv.png"),
              radius: radius,
            ),
          ),
          padding: EdgeInsets.all(padding),
        ),
      ),
      onKey: (RawKeyEvent event) {
        print('监听');
        if (event is RawKeyDownEvent && event.data is RawKeyEventDataAndroid) {
          RawKeyDownEvent rawKeyDownEvent = event;
          RawKeyEventDataAndroid rawKeyEventDataAndroid = rawKeyDownEvent.data;
          print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
          switch (rawKeyEventDataAndroid.keyCode) {
            case 19: //KEY_UP
              FocusScope.of(context).requestFocus(_focusNode);
              break;
            case 20: //KEY_DOWN
              break;
            case 21: //KEY_LEFT
              FocusScope.of(context).requestFocus(focusNode4);
              break;
            case 22: //KEY_RIGHT
              FocusScope.of(context).requestFocus(focusNode1);
              break;
            case 23: //KEY_CENTER
              break;
            default:
              break;
          }
        }
      },
    );

...
//焦点框颜色变换
Decoration getCircleDecoration(Color colors) {
  return BoxDecoration(
      border: Border.all(width: borderWidth, color: colors),
      shape: BoxShape.circle);
}

扩充一点,如果你想获取某个Widget或者布局的宽高信息,可以通过如下代码获取:

//这里的context为你要获取宽高信息的Widget的context
RenderObject renderObject = context.findRenderObject();
...
//这个context可以通过设置key来进行获取
GlobalKey _bodyKey = new GlobalKey();
...
Widget body1() {
    return Expanded(
      key: _bodyKey,
      child: Container(
          ...         

我们需要在页面渲染完毕后获取Widget的宽高信息,所以还要监听页面渲染刷新完毕的事件,用with WidgetsBindingObserver来实现:

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
    ...
  @override
  void initState() {
    //注册监听
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    //取消监听
    WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeDependencies() {
    WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(Widget oldWidget) {
    WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);
    super.didUpdateWidget(oldWidget);
  }

  void _onAfterRendering(Duration timeStamp) {
    //这里编写获取元素大小和位置的方法
    RenderObject renderObject = context.findRenderObject();
    Size size = renderObject.paintBounds.size;
    print('onAfterRendering:');
    print(size.height);
    //获取对应key的context所在的Widget的宽高信息
    RenderObject _renderObject = _bodyKey.currentContext.findRenderObject();
    Size _size = _renderObject.paintBounds.size;
    print('_onAfterRendering:');
    print(_size.height);
    bodyHeight = _size.height;
    setState(() {});
  }

最后给一个完整的Flutter TV的应用开发示例代码:

/*
 * @Author: Tan Dong 
 * @Date: 2019-03-16 12:39:05 
 * @Last Modified by:   Tan Dong 
 * @Last Modified time: 2019-03-16 12:39:05 
 */
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TV Page',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blueGrey,
        backgroundColor: Colors.teal,
      ),
      routes: {
        // '/home': (context) =>` VideoPlay(),
      },
      home: MyHomePage(title: 'TV Page'),
    );
  }
}

const double radius = 30;
const double padding = 2;
const double borderWidth = 2;
double itemWidth = 0;
FocusNode focusNode = null;

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  FocusNode focusNodeB0 = FocusNode();
  FocusNode focusNodeB1 = FocusNode();
  FocusNode focusNodeB2 = FocusNode();
  FocusNode focusNodeB3 = FocusNode();
  FocusNode focusNodeB4 = FocusNode();
  Color colorB0 = Colors.transparent;
  Color colorB1 = Colors.transparent;
  Color colorB2 = Colors.transparent;
  Color colorB3 = Colors.transparent;
  Color colorB4 = Colors.transparent;
  bool init = false;

  _setDecorationBorderB0() {
    setState(() {
      if (focusNodeB0.hasFocus) {
        colorB0 = Colors.orange;
      } else {
        colorB0 = Colors.transparent;
      }
    });
  }

  _setDecorationBorderB1() {
    setState(() {
      if (focusNodeB1.hasFocus) {
        colorB1 = Colors.orange;
      } else {
        colorB1 = Colors.transparent;
      }
    });
  }

  _setDecorationBorderB2() {
    setState(() {
      if (focusNodeB2.hasFocus) {
        colorB2 = Colors.orange;
      } else {
        colorB2 = Colors.transparent;
      }
    });
  }

  _setDecorationBorderB3() {
    setState(() {
      if (focusNodeB3.hasFocus) {
        colorB3 = Colors.orange;
      } else {
        colorB3 = Colors.transparent;
      }
    });
  }

  _setDecorationBorderB4() {
    setState(() {
      if (focusNodeB4.hasFocus) {
        colorB4 = Colors.orange;
      } else {
        colorB4 = Colors.transparent;
      }
    });
  }

  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    // SystemChrome.setEnabledSystemUIOverlays([]);
    // SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top, SystemUiOverlay.bottom]);
    super.initState();
    focusNodeB0.addListener(_setDecorationBorderB0);
    focusNodeB1.addListener(_setDecorationBorderB1);
    focusNodeB2.addListener(_setDecorationBorderB2);
    focusNodeB3.addListener(_setDecorationBorderB3);
    focusNodeB4.addListener(_setDecorationBorderB4);
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
    focusNodeB0.removeListener(_setDecorationBorderB0);
    focusNodeB0.dispose();
    focusNodeB1.removeListener(_setDecorationBorderB1);
    focusNodeB1.dispose();
    focusNodeB2.removeListener(_setDecorationBorderB2);
    focusNodeB2.dispose();
    focusNodeB3.removeListener(_setDecorationBorderB3);
    focusNodeB3.dispose();
    focusNodeB4.removeListener(_setDecorationBorderB4);
    focusNodeB4.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    itemWidth = MediaQuery.of(context).size.width / 3;
    return Scaffold(
      backgroundColor: Color(0xff277188),
      body:
          view1(), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Widget view1() {
    return Column(
      children: <Widget>[
        SizedBox(
          height: 20,
        ),
        TopWidget(),
        body1(),
        SizedBox(
          height: 20,
        ),
        BottomWidget(focusNodeB0),
        SizedBox(
          height: 20,
        ),
      ],
    );
  }

  @override
  void didChangeDependencies() {
    WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(Widget oldWidget) {
    WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);
    super.didUpdateWidget(oldWidget);
  }

  void _onAfterRendering(Duration timeStamp) {
    //这里编写获取元素大小和位置的方法
    RenderObject renderObject = context.findRenderObject();
    Size size = renderObject.paintBounds.size;
    print('onAfterRendering:');
    print(size.height);
    RenderObject _renderObject = _bodyKey.currentContext.findRenderObject();
    Size _size = _renderObject.paintBounds.size;
    print('_onAfterRendering:');
    print(_size.height);
    bodyHeight = _size.height;
    setState(() {});
    // Navigator.of(context).push(MaterialPageRoute(builder: (_) {
    //   return PermissionSamples();
    // }));
  }

  GlobalKey _bodyKey = new GlobalKey();
  double bodyHeight = 600;
  Widget image1() {
    return RawKeyboardListener(
      focusNode: focusNodeB0,
      onKey: (event) {
        if (event is RawKeyDownEvent && event.data is RawKeyEventDataAndroid) {
          RawKeyDownEvent rawKeyDownEvent = event;
          RawKeyEventDataAndroid rawKeyEventDataAndroid = rawKeyDownEvent.data;
          print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
          switch (rawKeyEventDataAndroid.keyCode) {
            case 19: //KEY_UP
              break;
            case 20: //KEY_DOWN
              FocusScope.of(context).requestFocus(focusNode);
              break;
            case 21: //KEY_LEFT
              FocusScope.of(context).requestFocus(focusNodeB4);
              break;
            case 22: //KEY_RIGHT
              FocusScope.of(context).requestFocus(focusNodeB1);
              break;
            case 23: //KEY_CENTER
              break;
            default:
              break;
          }
        }
      },
      child: Expanded(
          child: Container(
        height: bodyHeight,
        decoration: getRectangleDecoration(colorB0),
        child: Card(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(10)),
          ),
          elevation: 10,
          child: ClipRRect(
            child: Image.asset(
              'assets/hmjz.jpg',
              fit: BoxFit.cover,
            ),
            borderRadius: BorderRadius.all(
              Radius.circular(10),
            ),
          ),
        ),
      )),
    );
  }

  Widget body1() {
    return Expanded(
      key: _bodyKey,
      child: Container(
        margin: EdgeInsets.all(5),
        child: Row(
          children: <Widget>[
            image1(),
            Expanded(
              child: Column(
                children: <Widget>[
                  RawKeyboardListener(
                    focusNode: focusNodeB1,
                    onKey: (event) {
                      if (event is RawKeyDownEvent &&
                          event.data is RawKeyEventDataAndroid) {
                        RawKeyDownEvent rawKeyDownEvent = event;
                        RawKeyEventDataAndroid rawKeyEventDataAndroid =
                            rawKeyDownEvent.data;
                        print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                        switch (rawKeyEventDataAndroid.keyCode) {
                          case 19: //KEY_UP
                            break;
                          case 20: //KEY_DOWN
                            FocusScope.of(context).requestFocus(focusNodeB2);
                            break;
                          case 21: //KEY_LEFT
                            FocusScope.of(context).requestFocus(focusNodeB0);
                            break;
                          case 22: //KEY_RIGHT
                            FocusScope.of(context).requestFocus(focusNodeB3);
                            break;
                          case 23: //KEY_CENTER
                            break;
                          default:
                            break;
                        }
                      }
                    },
                    child: Expanded(
                        child: Container(
                      width: itemWidth,
                      decoration: getRectangleDecoration(colorB1),
                      child: Card(
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.circular(10)),
                        ),
                        elevation: 10,
                        child: ClipRRect(
                          child: Image.asset(
                            'assets/zqc.jpg',
                            fit: BoxFit.cover,
                          ),
                          borderRadius: BorderRadius.all(
                            Radius.circular(10),
                          ),
                        ),
                      ),
                    )),
                  ),
                  RawKeyboardListener(
                    focusNode: focusNodeB2,
                    onKey: (event) {
                      if (event is RawKeyDownEvent &&
                          event.data is RawKeyEventDataAndroid) {
                        RawKeyDownEvent rawKeyDownEvent = event;
                        RawKeyEventDataAndroid rawKeyEventDataAndroid =
                            rawKeyDownEvent.data;
                        print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                        switch (rawKeyEventDataAndroid.keyCode) {
                          case 19: //KEY_UP
                            FocusScope.of(context).requestFocus(focusNodeB1);
                            break;
                          case 20: //KEY_DOWN
                            FocusScope.of(context).requestFocus(focusNode);
                            break;
                          case 21: //KEY_LEFT
                            FocusScope.of(context).requestFocus(focusNodeB0);
                            break;
                          case 22: //KEY_RIGHT
                            FocusScope.of(context).requestFocus(focusNodeB4);
                            break;
                          case 23: //KEY_CENTER
                            break;
                          default:
                            break;
                        }
                      }
                    },
                    child: Expanded(
                        child: Container(
                      decoration: getRectangleDecoration(colorB2),
                      width: itemWidth,
                      child: Card(
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.circular(10)),
                        ),
                        elevation: 10,
                        child: ClipRRect(
                          child: Image.asset(
                            'assets/lifeandpi.jpg',
                            fit: BoxFit.cover,
                          ),
                          borderRadius: BorderRadius.all(
                            Radius.circular(10),
                          ),
                        ),
                      ),
                    )),
                  ),
                ],
              ),
            ),
            Expanded(
              child: Column(
                children: <Widget>[
                  RawKeyboardListener(
                    focusNode: focusNodeB3,
                    onKey: (event) {
                      if (event is RawKeyDownEvent &&
                          event.data is RawKeyEventDataAndroid) {
                        RawKeyDownEvent rawKeyDownEvent = event;
                        RawKeyEventDataAndroid rawKeyEventDataAndroid =
                            rawKeyDownEvent.data;
                        print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                        switch (rawKeyEventDataAndroid.keyCode) {
                          case 19: //KEY_UP
                            break;
                          case 20: //KEY_DOWN
                            FocusScope.of(context).requestFocus(focusNodeB4);
                            break;
                          case 21: //KEY_LEFT
                            FocusScope.of(context).requestFocus(focusNodeB1);
                            break;
                          case 22: //KEY_RIGHT
                            FocusScope.of(context).requestFocus(focusNodeB0);
                            break;
                          case 23: //KEY_CENTER
                            break;
                          default:
                            break;
                        }
                      }
                    },
                    child: Expanded(
                        child: Container(
                      width: itemWidth,
                      decoration: getRectangleDecoration(colorB3),
                      child: Card(
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.circular(10)),
                        ),
                        elevation: 10,
                        child: ClipRRect(
                          child: Image.asset(
                            'assets/zzx.jpg',
                            fit: BoxFit.cover,
                          ),
                          borderRadius: BorderRadius.all(
                            Radius.circular(10),
                          ),
                        ),
                      ),
                    )),
                  ),
                  RawKeyboardListener(
                    focusNode: focusNodeB4,
                    onKey: (event) {
                      if (event is RawKeyDownEvent &&
                          event.data is RawKeyEventDataAndroid) {
                        RawKeyDownEvent rawKeyDownEvent = event;
                        RawKeyEventDataAndroid rawKeyEventDataAndroid =
                            rawKeyDownEvent.data;
                        print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                        switch (rawKeyEventDataAndroid.keyCode) {
                          case 19: //KEY_UP
                            FocusScope.of(context).requestFocus(focusNodeB3);
                            break;
                          case 20: //KEY_DOWN
                            FocusScope.of(context).requestFocus(focusNode);
                            break;
                          case 21: //KEY_LEFT
                            FocusScope.of(context).requestFocus(focusNodeB2);
                            break;
                          case 22: //KEY_RIGHT
                            FocusScope.of(context).requestFocus(focusNodeB0);
                            break;
                          case 23: //KEY_CENTER
                            break;
                          default:
                            break;
                        }
                      }
                    },
                    child: Expanded(
                        child: Container(
                      width: itemWidth,
                      decoration: getRectangleDecoration(colorB4),
                      child: Card(
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.circular(10)),
                        ),
                        elevation: 10,
                        child: ClipRRect(
                          child: Image.asset(
                            'assets/mgdz.jpg',
                            fit: BoxFit.cover,
                          ),
                          borderRadius: BorderRadius.all(
                            Radius.circular(10),
                          ),
                        ),
                      ),
                    )),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class TopWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return TopWidgetState();
  }
}

class TopWidgetState extends State<TopWidget> {
  String time = '';
  Timer timer;
  @override
  void initState() {
    super.initState();
    var now = DateTime.now();
    timer = Timer.periodic(Duration(seconds: 1), (Timer timer) {
      now = DateTime.now();
      setState(() {
        time = now.year.toString() +
            "-" +
            now.month.toString() +
            "-" +
            now.day.toString() +
            "  " +
            now.hour.toString() +
            ":" +
            now.minute.toString() +
            ":" +
            now.second.toString() +
            "  " +
            weekFormat(now.weekday);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      child: Row(
        children: <Widget>[
          Text(
            'Page',
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
          Expanded(
            child: Container(),
          ),
          Text(
            time,
            style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
          ),
        ],
      ),
      padding: EdgeInsets.all(10),
    );
  }
}

class BottomWidget extends StatefulWidget {
  FocusNode _focusNode;

  BottomWidget(this._focusNode);

  @override
  State<StatefulWidget> createState() {
    return BottomWidgetState(_focusNode);
  }
}

class BottomWidgetState extends State<BottomWidget>
    with WidgetsBindingObserver {
  FocusNode _focusNode;
  BottomWidgetState(this._focusNode);
  FocusNode focusNode0 = FocusNode();
  FocusNode focusNode1 = FocusNode();
  FocusNode focusNode2 = FocusNode();
  FocusNode focusNode3 = FocusNode();
  FocusNode focusNode4 = FocusNode();

  Color color0 = Colors.transparent;
  Color color1 = Colors.transparent;
  Color color2 = Colors.transparent;
  Color color3 = Colors.transparent;
  Color color4 = Colors.transparent;
  bool init = false;

  _setDecorationBorder0() {
    setState(() {
      if (focusNode0.hasFocus) {
        color0 = Colors.orange;
      } else {
        color0 = Colors.transparent;
      }
    });
  }

  _setDecorationBorder1() {
    setState(() {
      if (focusNode1.hasFocus) {
        color1 = Colors.orange;
      } else {
        color1 = Colors.transparent;
      }
    });
  }

  _setDecorationBorder2() {
    setState(() {
      if (focusNode2.hasFocus) {
        color2 = Colors.orange;
      } else {
        color2 = Colors.transparent;
      }
    });
  }

  _setDecorationBorder3() {
    setState(() {
      if (focusNode3.hasFocus) {
        color3 = Colors.orange;
      } else {
        color3 = Colors.transparent;
      }
    });
  }

  _setDecorationBorder4() {
    setState(() {
      if (focusNode4.hasFocus) {
        color4 = Colors.orange;
      } else {
        color4 = Colors.transparent;
      }
    });
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    focusNode0.addListener(_setDecorationBorder0);
    focusNode1.addListener(_setDecorationBorder1);
    focusNode2.addListener(_setDecorationBorder2);
    focusNode3.addListener(_setDecorationBorder3);
    focusNode4.addListener(_setDecorationBorder4);
    focusNode = focusNode0;
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
    focusNode0.removeListener(_setDecorationBorder0);
    focusNode0.dispose();
    focusNode1.removeListener(_setDecorationBorder1);
    focusNode1.dispose();
    focusNode2.removeListener(_setDecorationBorder2);
    focusNode2.dispose();
    focusNode3.removeListener(_setDecorationBorder3);
    focusNode3.dispose();
    focusNode4.removeListener(_setDecorationBorder4);
    focusNode4.dispose();
  }

  @override
  void didChangeDependencies() {
    WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);
    super.didChangeDependencies();
  }

  @override
  void didUpdateWidget(Widget oldWidget) {
    WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);
    super.didUpdateWidget(oldWidget);
  }

  void _onAfterRendering(Duration timeStamp) {
    //这里编写获取元素大小和位置的方法
    RenderObject renderObject = context.findRenderObject();
    Size size = renderObject.paintBounds.size;
    print(size.height);
  }

  GlobalKey _myKey = new GlobalKey();

  @override
  Widget build(BuildContext context) {
    if (!init) {
      FocusScope.of(context).requestFocus(focusNode0);
      init = true;
    }
    final sizeM = MediaQuery.of(context).size;
    print(sizeM.width);
    print(sizeM.height);
    return Row(
      key: _myKey,
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: <Widget>[
        SizedBox(
          width: 10,
          height: 2,
        ),
        Column(
          children: <Widget>[
            RawKeyboardListener(
              focusNode: focusNode0,
              child: Container(
                decoration: getCircleDecoration(color0),
                child: Padding(
                  child: Card(
                    elevation: 5,
                    shape: CircleBorder(),
                    child: CircleAvatar(
                      child: Text(''),
                      backgroundImage: AssetImage("assets/icon_tv.png"),
                      radius: radius,
                    ),
                  ),
                  padding: EdgeInsets.all(padding),
                ),
              ),
              onKey: (RawKeyEvent event) {
                print('监听');
                if (event is RawKeyDownEvent &&
                    event.data is RawKeyEventDataAndroid) {
                  RawKeyDownEvent rawKeyDownEvent = event;
                  RawKeyEventDataAndroid rawKeyEventDataAndroid =
                      rawKeyDownEvent.data;
                  print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                  switch (rawKeyEventDataAndroid.keyCode) {
                    case 19: //KEY_UP
                      FocusScope.of(context).requestFocus(_focusNode);
                      break;
                    case 20: //KEY_DOWN
                      break;
                    case 21: //KEY_LEFT
                      FocusScope.of(context).requestFocus(focusNode4);
                      break;
                    case 22: //KEY_RIGHT
                      FocusScope.of(context).requestFocus(focusNode1);
                      break;
                    case 23: //KEY_CENTER
                      break;
                    default:
                      break;
                  }
                }
              },
            ),
            Text(
              'TV',
              style:
                  TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ],
        ),
        Column(
          children: <Widget>[
            RawKeyboardListener(
              focusNode: focusNode1,
              child: Container(
                decoration: getCircleDecoration(color1),
                child: Padding(
                  child: Card(
                    elevation: 5,
                    shape: CircleBorder(),
                    child: CircleAvatar(
                      child: Text(''),
                      backgroundImage: AssetImage("assets/icon_voice.png"),
                      radius: radius,
                    ),
                  ),
                  padding: EdgeInsets.all(padding),
                ),
              ),
              onKey: (RawKeyEvent event) {
                if (event is RawKeyDownEvent &&
                    event.data is RawKeyEventDataAndroid) {
                  RawKeyDownEvent rawKeyDownEvent = event;
                  RawKeyEventDataAndroid rawKeyEventDataAndroid =
                      rawKeyDownEvent.data;
                  print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                  switch (rawKeyEventDataAndroid.keyCode) {
                    case 19: //KEY_UP
                      FocusScope.of(context).requestFocus(_focusNode);
                      break;
                    case 20: //KEY_DOWN
                      break;
                    case 21: //KEY_LEFT
                      FocusScope.of(context).requestFocus(focusNode0);
                      break;
                    case 22: //KEY_RIGHT
                      FocusScope.of(context).requestFocus(focusNode2);
                      break;
                    case 23: //KEY_CENTER
                      break;
                    default:
                      break;
                  }
                }
              },
            ),
            Text(
              'Voice',
              style:
                  TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ],
        ),
        Column(
          children: <Widget>[
            RawKeyboardListener(
              focusNode: focusNode2,
              child: Container(
                decoration: getCircleDecoration(color2),
                child: Padding(
                  child: Card(
                    elevation: 5,
                    shape: CircleBorder(),
                    child: CircleAvatar(
                      child: Text(''),
                      backgroundImage: AssetImage("assets/icon_video.png"),
                      radius: radius,
                    ),
                  ),
                  padding: EdgeInsets.all(padding),
                ),
              ),
              onKey: (RawKeyEvent event) {
                if (event is RawKeyDownEvent &&
                    event.data is RawKeyEventDataAndroid) {
                  RawKeyDownEvent rawKeyDownEvent = event;
                  RawKeyEventDataAndroid rawKeyEventDataAndroid =
                      rawKeyDownEvent.data;
                  print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                  switch (rawKeyEventDataAndroid.keyCode) {
                    case 19: //KEY_UP
                      FocusScope.of(context).requestFocus(_focusNode);
                      break;
                    case 20: //KEY_DOWN
                      break;
                    case 21: //KEY_LEFT
                      FocusScope.of(context).requestFocus(focusNode1);
                      break;
                    case 22: //KEY_RIGHT
                      FocusScope.of(context).requestFocus(focusNode3);
                      break;
                    case 23: //KEY_CENTER
                      break;
                    default:
                      break;
                  }
                }
              },
            ),
            Text(
              'Video',
              style:
                  TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ],
        ),
        Column(
          children: <Widget>[
            RawKeyboardListener(
              focusNode: focusNode3,
              child: Container(
                decoration: getCircleDecoration(color3),
                child: Padding(
                  child: Card(
                    elevation: 5,
                    shape: CircleBorder(),
                    child: CircleAvatar(
                      child: Text(''),
                      backgroundImage: AssetImage("assets/icon_phone.png"),
                      radius: radius,
                    ),
                  ),
                  padding: EdgeInsets.all(padding),
                ),
              ),
              onKey: (RawKeyEvent event) {
                if (event is RawKeyDownEvent &&
                    event.data is RawKeyEventDataAndroid) {
                  RawKeyDownEvent rawKeyDownEvent = event;
                  RawKeyEventDataAndroid rawKeyEventDataAndroid =
                      rawKeyDownEvent.data;
                  print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                  switch (rawKeyEventDataAndroid.keyCode) {
                    case 19: //KEY_UP
                      FocusScope.of(context).requestFocus(_focusNode);
                      break;
                    case 20: //
                      break;
                    case 21: //KEY_LEFT
                      FocusScope.of(context).requestFocus(focusNode2);
                      break;
                    case 22: //KEY_RIGHT
                      FocusScope.of(context).requestFocus(focusNode4);
                      break;
                    case 23: //KEY_CENTER
                      Navigator.of(context).pushNamed('/home');
                      break;
                    default:
                      break;
                  }
                }
              },
            ),
            Text(
              'Phone',
              style:
                  TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ],
        ),
        Column(
          children: <Widget>[
            RawKeyboardListener(
              focusNode: focusNode4,
              child: Container(
                decoration: getCircleDecoration(color4),
                child: Padding(
                  child: Card(
                    elevation: 5,
                    shape: CircleBorder(),
                    child: CircleAvatar(
                      child: Text(''),
                      backgroundImage: AssetImage("assets/icon_pad.png"),
                      radius: radius,
                    ),
                  ),
                  padding: EdgeInsets.all(padding),
                ),
              ),
              onKey: (RawKeyEvent event) {
                if (event is RawKeyDownEvent &&
                    event.data is RawKeyEventDataAndroid) {
                  RawKeyDownEvent rawKeyDownEvent = event;
                  RawKeyEventDataAndroid rawKeyEventDataAndroid =
                      rawKeyDownEvent.data;
                  print("keyCode: ${rawKeyEventDataAndroid.keyCode}");
                  switch (rawKeyEventDataAndroid.keyCode) {
                    case 19: //KEY_UP
                      FocusScope.of(context).requestFocus(_focusNode);
                      break;
                    case 20: //KEY_DOWN
                      break;
                    case 21: //KEY_LEFT
                      FocusScope.of(context).requestFocus(focusNode3);
                      break;
                    case 22: //KEY_RIGHT
                      FocusScope.of(context).requestFocus(focusNode0);
                      break;
                    case 23: //KEY_CENTER
                      break;
                    default:
                      break;
                  }
                }
              },
            ),
            Text(
              'Pad',
              style:
                  TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
            ),
          ],
        ),
        SizedBox(
          width: 10,
          height: 2,
        ),
      ],
    );
  }
}

Decoration getCircleDecoration(Color colors) {
  return BoxDecoration(
      border: Border.all(width: borderWidth, color: colors),
      shape: BoxShape.circle);
}

Decoration getRectangleDecoration(Color colors) {
  List<BoxShadow> boxShadows = List();
  boxShadows.add(BoxShadow(color: Colors.orange[200]));
  boxShadows.add(BoxShadow(color: Colors.yellow[100]));
  boxShadows.add(BoxShadow(color: Colors.yellow[300]));
  List<Color> colorsGradient = List();
  colorsGradient.add(Colors.teal);
  colorsGradient.add(Colors.teal[200]);

  return BoxDecoration(
    border: Border.all(width: borderWidth, color: colors),
    borderRadius: BorderRadius.all(Radius.circular(10)),
    shape: BoxShape.rectangle,
  );
}

String weekFormat(int week) {
  switch (week) {
    case 1:
      return '星期一';
      break;
    case 2:
      return '星期二';
      break;
    case 3:
      return '星期三';
      break;
    case 4:
      return '星期四';
      break;
    case 5:
      return '星期五';
      break;
    case 6:
      return '星期六';
      break;
    case 7:
      return '星期日';
      break;
  }
}

关于Flutter TV开发就讲解这么多。