Flutter TV应用的开发尝试

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

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

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

Flutter Dart QQ技术交流群:979966470

开发的运行效果图

由于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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //KEY_DOWNbreak;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode4);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode1);break;case 23: //KEY_CENTERbreak;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添加监听事件方法@overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);focusNode0.addListener(_setDecorationBorder0);}
...
//销毁时要注销监听@overridevoid 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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //KEY_DOWNbreak;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode4);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode1);break;case 23: //KEY_CENTERbreak;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 {...@overridevoid initState() {//注册监听WidgetsBinding.instance.addObserver(this);super.initState();}@overridevoid dispose() {super.dispose();//取消监听WidgetsBinding.instance.removeObserver(this);}@overridevoid didChangeDependencies() {WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);super.didChangeDependencies();}@overridevoid 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.@overrideWidget 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;}});}@overridevoid 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);}@overridevoid 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();}@overrideWidget 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,),],);}@overridevoid didChangeDependencies() {WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);super.didChangeDependencies();}@overridevoid 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_UPbreak;case 20: //KEY_DOWNFocusScope.of(context).requestFocus(focusNode);break;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNodeB4);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNodeB1);break;case 23: //KEY_CENTERbreak;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_UPbreak;case 20: //KEY_DOWNFocusScope.of(context).requestFocus(focusNodeB2);break;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNodeB0);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNodeB3);break;case 23: //KEY_CENTERbreak;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_UPFocusScope.of(context).requestFocus(focusNodeB1);break;case 20: //KEY_DOWNFocusScope.of(context).requestFocus(focusNode);break;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNodeB0);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNodeB4);break;case 23: //KEY_CENTERbreak;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_UPbreak;case 20: //KEY_DOWNFocusScope.of(context).requestFocus(focusNodeB4);break;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNodeB1);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNodeB0);break;case 23: //KEY_CENTERbreak;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_UPFocusScope.of(context).requestFocus(focusNodeB3);break;case 20: //KEY_DOWNFocusScope.of(context).requestFocus(focusNode);break;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNodeB2);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNodeB0);break;case 23: //KEY_CENTERbreak;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 {@overrideState<StatefulWidget> createState() {return TopWidgetState();}
}class TopWidgetState extends State<TopWidget> {String time = '';Timer timer;@overridevoid 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);});});}@overrideWidget 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);@overrideState<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;}});}@overridevoid initState() {super.initState();WidgetsBinding.instance.addObserver(this);focusNode0.addListener(_setDecorationBorder0);focusNode1.addListener(_setDecorationBorder1);focusNode2.addListener(_setDecorationBorder2);focusNode3.addListener(_setDecorationBorder3);focusNode4.addListener(_setDecorationBorder4);focusNode = focusNode0;}@overridevoid 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();}@overridevoid didChangeDependencies() {WidgetsBinding.instance.addPostFrameCallback(_onAfterRendering);super.didChangeDependencies();}@overridevoid 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();@overrideWidget 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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //KEY_DOWNbreak;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode4);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode1);break;case 23: //KEY_CENTERbreak;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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //KEY_DOWNbreak;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode0);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode2);break;case 23: //KEY_CENTERbreak;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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //KEY_DOWNbreak;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode1);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode3);break;case 23: //KEY_CENTERbreak;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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //break;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode2);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode4);break;case 23: //KEY_CENTERNavigator.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_UPFocusScope.of(context).requestFocus(_focusNode);break;case 20: //KEY_DOWNbreak;case 21: //KEY_LEFTFocusScope.of(context).requestFocus(focusNode3);break;case 22: //KEY_RIGHTFocusScope.of(context).requestFocus(focusNode0);break;case 23: //KEY_CENTERbreak;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开发就讲解这么多。

Flutter TV应用的开发尝试相关推荐

  1. Flutter TV 应用的开发尝试 | 开发者说·DTalk

    本文原作者: 谭东,原文发布于公众号谭东 jay: https://mp.weixin.qq.com/s/GI5g-zdeRwc8_E2zPN8pMA 我们之前一直在做 Flutter 在移动端的应用 ...

  2. 第 4-2 课:开发一个 Flutter TV 应用

    之前一直在讲 Flutter 在移动端的应用尝试,今天这节内容,我们将拓展到 TV 应用的开发上来. 我们知道目前的智能电视和机顶盒都是基于 Android 系统的,所以一般的 TV 应用开发都是采用 ...

  3. 谁告诉你 Flutter 会干掉原生开发?

    如下图所示,今天刚好收到一个知乎问题,而刚刚好这个视频我也在抖音上看到过,播放量还不低,本来是"一笑而过"但是既然这么"有缘份",那就关于 "Flut ...

  4. 【Flutter进阶】 Web开发中如何加载网络图片

    目录 前言 如何加载网络图片 列表中的网络图片 HTML renderer和CanvasKit 文本无法选择 Shadow Root问题 为什么找不到节点? Shadow Dom 问题解决 Flutt ...

  5. android调用flutter aar_Flutter原生混合开发

    混合开发简介 使用Flutter从零开始开发App是一件轻松惬意的事情,但对于一些成熟的产品来说,完全摒弃原有App的历史沉淀,全面转向Flutter是不现实的.因此使用Flutter去统一Andro ...

  6. flutter windows搭建ios开发环境

    flutter windows搭建ios开发环境 具体流程太长了,这里记录下思路过程以及坑点. 原文链接:https://blog.csdn.net/weixin_44259356/article/d ...

  7. 瑜伽教学TV端APP开发搭建教程IPTV系统

    瑜伽作为一种非常受欢迎的锻炼方式,受到越来越多人的喜爱.但因为时间和距离等因素的影响,很多人也喜欢自己在家里做一些瑜伽锻炼,比如饭后或者休息时间打开电视进行瑜伽锻炼,不仅仅可以很好的学习动作,还可以一 ...

  8. swift项目嵌入flutter的module混合开发(官方推荐模式)

    上一篇有讲到项目混合开发配置问题. 上一篇是跟网上的各大网游搜罗的混合过程中的问题.这篇根据官方的介绍做了尝试配置比较简单.这里可以分享一下使用: 首先也是一样,在我们的项目MyApp的根目录同级文件 ...

  9. 初探Flutter跨端游戏开发

    本文作者为奇舞团大前端CodeFarmer 背景 笔者在公司前前后后做了有小一年Flutter 开发,从入门到后面业务方变动,到暂时放弃Flutter.对于Flutter争议不提,我们得承认Flutt ...

  10. android TV端app开发的踩坑之旅

    android studio 出现已久,而种种原因,android开发还是一直使用的eclipse,近几日熟悉android studio,遇到不少问题,相信以后也会遇到不少问题.本着雁过留痕.摔了跟 ...

最新文章

  1. Lab模式的妙用--人像处理
  2. CodeForces 157C Message
  3. 焊缝标注vlx实用程序_【CAD自动标注插件下载】CAD自动标注插件yjbz.VLX v1.0 最新版-开心电玩...
  4. YOLO算法发展史 v1 v2 v3
  5. 关于layui分页组件layPage如何动态调整页数的使用
  6. 快速批量创建文件夹的方法
  7. 分盘后磁盘空间不够,用分区助手增加某个磁盘空间
  8. 画图必备工具:25个常用Matplotlib图的Python代码总结
  9. 【倒计时1天】PPP全球数字资产投资峰会-中国区北京首站之金融科技区块链支持可持续发展...
  10. mapActions
  11. matlab 稀疏随机矩阵,Matlab 稀疏矩阵函数
  12. node.js基于微信小程序的外卖订餐系统 uniapp 小程序
  13. office2016实用论文排版技巧
  14. 四柱八字大全 php,四柱八字查询表 免费四柱八字查询
  15. JAVA 拾遗 --Future 模式与 Promise 模式
  16. pdf转换器免费注册码
  17. [渝粤教育] 北部湾大学 团体心理辅导 参考 资料
  18. Behavior Designer 干货总结
  19. 新手UI设计师必读:火爆海外设计圈的设计资源!
  20. python3网络爬虫开发实战pdf 崔庆才 百度网盘分享

热门文章

  1. 最好最实用的PHP二次开发教程
  2. B - Relatively Prime Graph -CodeForces - 1009D-csdn博客
  3. 微服务实战之高可用性
  4. VMware Workstation Pro的安装详细过程
  5. 破解jQuery插件收费、下载币(单页扒站小工具)
  6. opencv-图像阈值
  7. win7定时关机命令_只需9步教你轻松设置win7系统定时关机,无需任何工具
  8. 磁阻式随机存储器MRAM基本原理
  9. meltdown linux 补丁,谈谈CentOS发布内核安全补丁:修复Meltdown和Spectre漏洞
  10. 空头平仓什么意思_外汇空头平仓是什么意思?外汇如何平仓?