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

Posted 谷歌开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter TV 应用的开发尝试 | 开发者说·DTalk相关的知识,希望对你有一定的参考价值。

本文原作者: 谭东 ,原文发布于公众号谭东 jay: https://mp.weixin.qq.com/s/GI5g-zdeRwc8_E2zPN8pMA




我们之前一直在做 Flutter 在移动端的应用,今天这里我们拓展一下 Flutter 的应用场景,我们将拓展到 TV 应用的开发上来。我们知道目前的智能电视和机顶盒都是基于 android 系统的,所以一般的 TV 应用开发都是采用 Android 原生进行开发,Google 对 Android TV 的开发也进行了一些规范和库的制定。当然也有的是采用的 B/S 架构进行设计的。这里我们将进行尝试 Flutter 开发 TV 应用。虽然写出来了,效果也还可以,体验流畅,自动适配,但其中按键监听、焦点处理和焦点框处理比较麻烦。由于 Google 官方并没有推出 Flutter TV 应用的 SDK,所以我们这里只是给大家拓展下思路。接下来,就分享下其中的技术点。本文将主要介绍:

  • Flutter TV 应用开发主要难点

  • Flutter TV 应用开发按键监听

  • Flutter TV 应用开发焦点处理

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


在进行讲解前,我们先看下 Flutter TV 开发实现的效果图:


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

Flutter TV 应用开发主要难点

由于 Google Flutter 官方并没有推出 TV 版 Flutter SDK,所以用 Flutter 尝试编写 TV 应用,主要是焦点框和焦点移动、焦点顺序的处理,其他的和手机应用差别不大。按键监听、焦点框和焦点处理比较麻烦,这里我们只是作为研究拓展。
原生 Android 的控件就默认有焦点的处理属性,直接配置使用即可。还支持指定下一个焦点的 id。
 
   
   
 
  1. //焦点处理

  2. android:focusable="true"

  3. //触摸模式下是否可以点击,可选可不选

  4. android:focusableInTouchMode="true"

Flutter 开发 TV 应用就要自己处理按键监听、焦点和焦点框、焦点移动顺序了,比较的麻烦,处理好了这几个问题,开发起来也就没太大难度了。
不过最新版的 Flutter 多了一个 DefaultFocusTraversal 这个类,我们可以进行指定方向自动移动焦点了,相对简单了一些。

Flutter TV 应用开发按键监听

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

  2. Key key,

  3. @required this.focusNode,//焦点结点

  4. @required this.onKey,//按键接收处理事件

  5. @required this.child,//接收焦点的子控件

  6. })

很简单给个例子:

 
   
   
 
  1. FocusNode focusNode0 = FocusNode();


  2. ... ...


  3. RawKeyboardListener(

  4. focusNode: focusNode0,

  5. child: Container(

  6. decoration: getCircleDecoration(color0),

  7. child: Padding(

  8. child: Card(

  9. elevation: 5,

  10. shape: CircleBorder(),

  11. child: CircleAvatar(

  12. child: Text(''),

  13. backgroundImage: AssetImage("assets/icon_tv.png"),

  14. radius: radius,

  15. ),

  16. ),

  17. padding: EdgeInsets.all(padding),

  18. ),

  19. ),

  20. onKey: (RawKeyEvent event) {

  21. if (event is RawKeyDownEvent && event.data is RawKeyEventDataAndroid) {

  22. RawKeyDownEvent rawKeyDownEvent = event;

  23. RawKeyEventDataAndroid rawKeyEventDataAndroid = rawKeyDownEvent.data;

  24. print("keyCode: ${rawKeyEventDataAndroid.keyCode}");

  25. switch (rawKeyEventDataAndroid.keyCode) {

  26. case 19: //KEY_UP

  27. FocusScope.of(context).requestFocus(_focusNode);

  28. break;

  29. case 20: //KEY_DOWN

  30. break;

  31. case 21: //KEY_LEFT

  32. FocusScope.of(context).requestFocus(focusNode4);

  33. break;

  34. case 22: //KEY_RIGHT

  35. FocusScope.of(context).requestFocus(focusNode1);

  36. break;

  37. case 23: //KEY_CENTER

  38. break;

  39. case 66: //KEY_ENTER

  40. break;

  41. default:

  42. break;

  43. }

  44. }

  45. },

  46. )

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

Flutter TV 应用开发焦点处理

Flutter TV 的 Widget 获取焦点的处理通过 FocusScope 这个 Widget 处理。主动获取焦点代码如下:
 
   
   
 
  1. FocusNode focusNode0 = FocusNode();

  2. ... ...

  3. //主动获取焦点

  4. FocusScope.of(context).requestFocus(focusNode0);

  5. //自动获取焦点

  6. FocusScope.of(context).autofocus(focusNode0);

这样就可以了进行焦点获取处理了。FocusNode 这个类也很重要,负责监听焦点的工作。
焦点的移动我们就是用最新的 DefaultFocusTraversal  行自动指定方向进行搜索下一个焦点:
 
   
   
 
  1. FocusScope.of(context)

  2. .focusInDirection(TraversalDirection.up);

  3. // 或者像下面这样使用

  4. DefaultFocusTraversal.of(context).inDirection(

  5. FocusScope.of(context).focusedChild, TraversalDirection.up);


  6. DefaultFocusTraversal.of(context)

  7. .inDirection(_focusNode, TraversalDirection.right);

支持上下左右四个方向。如果想手动指定下一个焦点是哪个的话,可以像下面这样用:

 
   
   
 
  1. FocusScope.of(context).requestFocus(focusNode);

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

有了焦点、按键事件监听,剩下的就是选中的焦点框效果的实现了,主要原理这里使用的是用边框,然后动态设置边框颜色或者边框宽度、边框装饰就实现了焦点框选中显示和隐藏的效果。例如选中后焦点框颜色设置为黄色、未选中时就设置为透明色,通过 setState({...}) 进行刷新页面。
例如我们可以在最外层的 Container 里设置 BoxDecoration 进行边框效果的设置实现。
 
   
   
 
  1. var default_decoration = BoxDecoration(

  2. border: Border.all(width: 3, color: Colors.deepOrange),

  3. borderRadius: BorderRadius.all(

  4. Radius.circular(5),

  5. ));


  6. ... ...


  7. child: Container(

  8. margin: EdgeInsets.all(8),

  9. decoration: default_decoration,

  10. child: widget.child,

  11. ));

最后给大家一个完整的最新的技术方案的例子代码:

先绘制欢迎页,效果图如下:

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

代码如下:

 
   
   
 
  1. // 启动欢迎页


  2. import 'dart:async';


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

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


  5. import 'ui/tv_page.dart';


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


  7. class MyApp extends StatelessWidget {

  8. @override

  9. Widget build(BuildContext context) {

  10. SystemChrome.setEnabledSystemUIOverlays([]);

  11. // 强制横屏

  12. SystemChrome.setPreferredOrientations([

  13. DeviceOrientation.landscapeLeft,

  14. DeviceOrientation.landscapeRight

  15. ]);

  16. return MaterialApp(

  17. title: 'Flutter TV',

  18. debugShowCheckedModeBanner: false,

  19. theme: ThemeData(

  20. primarySwatch: Colors.blue,

  21. ),

  22. home: MyHomePage(),

  23. );

  24. }

  25. }


  26. class MyHomePage extends StatefulWidget {

  27. @override

  28. _MyHomePageState createState() => _MyHomePageState();

  29. }


  30. class _MyHomePageState extends State<MyHomePage> {

  31. Timer timer;


  32. @override

  33. void initState() {

  34. startTimeout();

  35. super.initState();

  36. }


  37. @override

  38. Widget build(BuildContext context) {

  39. return Scaffold(

  40. primary: true,

  41. backgroundColor: Colors.black54,

  42. body: Center(

  43. child: Text(

  44. '芒果TV',

  45. style: TextStyle(

  46. fontSize: 50,

  47. color: Colors.deepOrange,

  48. fontWeight: FontWeight.normal),

  49. ),

  50. ),

  51. );

  52. }


  53. _toPage() {

  54. Navigator.pushAndRemoveUntil(

  55. context,

  56. MaterialPageRoute(builder: (context) => TVPage()),

  57. (route) => route == null,

  58. );

  59. }


  60. //倒计时处理

  61. static const timeout = const Duration(seconds: 3);


  62. startTimeout() {

  63. timer = Timer(timeout, handleTimeout);

  64. return timer;

  65. }


  66. void handleTimeout() {

  67. _toPage();

  68. }


  69. @override

  70. void dispose() {

  71. if (timer != null) {

  72. timer.cancel();

  73. timer = null;

  74. }

  75. super.dispose();

  76. }

  77. }

应用首页,效果图如下:

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

代码如下:

 
   
   
 
  1. // 应用首页

  2. import 'dart:async';


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

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

  5. import 'package:flutter_tv/utils/time_utils.dart';

  6. import 'package:flutter_tv/widgets/tv_widget.dart';


  7. import 'home_page.dart';

  8. import 'list_page.dart';


  9. class TVPage extends StatefulWidget {

  10. @override

  11. State<StatefulWidget> createState() {

  12. SystemChrome.setEnabledSystemUIOverlays([]);

  13. // 强制横屏

  14. SystemChrome.setPreferredOrientations(

  15. [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);

  16. return TVPageState();

  17. }

  18. }


  19. class TVPageState extends State<TVPage> with SingleTickerProviderStateMixin {

  20. TabController _tabController;

  21. Timer timer;

  22. var timeString = TimeUtils.getTime();


  23. bool init = false;

  24. FocusNode focusNodeB0 = FocusNode();

  25. FocusNode focusNodeB1 = FocusNode();


  26. @override

  27. void initState() {

  28. super.initState();

  29. //initialIndex为初始选中第几个,length为数量

  30. _tabController = TabController(initialIndex: 0, length: 8, vsync: this);

  31. // 监听

  32. _tabController.addListener(() {

  33. switch (_tabController.index) {

  34. case 0:

  35. break;

  36. case 1:

  37. break;

  38. }

  39. });


  40. focusNodeB0.addListener(() {

  41. if (focusNodeB0.hasFocus) {

  42. setState(() {

  43. _tabController.animateTo(0);

  44. });

  45. }

  46. });

  47. focusNodeB1.addListener(() {

  48. if (focusNodeB1.hasFocus) {

  49. setState(() {

  50. _tabController.animateTo(1);

  51. });

  52. }

  53. });

  54. }


  55. @override

  56. Widget build(BuildContext context) {

  57. return Container(

  58. color: Colors.black87,

  59. padding: EdgeInsets.all(30),

  60. child: Scaffold(

  61. appBar: AppBar(

  62. backgroundColor: Colors.black87,

  63. leading: Icon(

  64. Icons.live_tv,

  65. color: Colors.deepOrange,

  66. size: 50,

  67. ),

  68. title: Text(

  69. '芒果TV',

  70. style: TextStyle(

  71. fontSize: 30, color: Colors.white, fontStyle: FontStyle.italic),

  72. ),

  73. primary: true,

  74. actions: <Widget>[

  75. FlatButton(

  76. child: Text(

  77. '$timeString',

  78. style: TextStyle(color: Colors.white),

  79. ),

  80. ),

  81. ],

  82. // 设置TabBar

  83. bottom: TabBar(

  84. controller: _tabController,

  85. indicatorColor: Colors.deepOrange,

  86. labelColor: Colors.deepOrange,

  87. unselectedLabelColor: Colors.white,

  88. tabs: <Widget>[

  89. Tab(

  90. child: TVWidget(

  91. hasDecoration: false,

  92. focusChange: (hasFocus) {

  93. if (hasFocus) {

  94. setState(() {

  95. _tabController.animateTo(0);

  96. });

  97. }

  98. },

  99. child: Text(

  100. '首页',

  101. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  102. ),

  103. requestFocus: true,

  104. ),

  105. ),

  106. Tab(

  107. child: TVWidget(

  108. hasDecoration: false,

  109. focusChange: (hasFocus) {

  110. if (hasFocus) {

  111. setState(() {

  112. _tabController.animateTo(1);

  113. });

  114. }

  115. },

  116. child: Text(

  117. '精选',

  118. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  119. ),

  120. )),

  121. Tab(

  122. child: TVWidget(

  123. hasDecoration: false,

  124. focusChange: (hasFocus) {

  125. if (hasFocus) {

  126. setState(() {

  127. _tabController.animateTo(2);

  128. });

  129. }

  130. },

  131. onclick: () {

  132. print("点击");

  133. },

  134. child: Text(

  135. '国产',

  136. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  137. ),

  138. )),

  139. Tab(

  140. child: TVWidget(

  141. hasDecoration: false,

  142. focusChange: (hasFocus) {

  143. if (hasFocus) {

  144. setState(() {

  145. _tabController.animateTo(3);

  146. });

  147. }

  148. },

  149. child: Text(

  150. '欧美',

  151. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  152. ),

  153. )),

  154. Tab(

  155. child: TVWidget(

  156. hasDecoration: false,

  157. focusChange: (hasFocus) {

  158. if (hasFocus) {

  159. setState(() {

  160. _tabController.animateTo(4);

  161. });

  162. }

  163. },

  164. child: Text(

  165. '日漫',

  166. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  167. ),

  168. ),

  169. ),

  170. Tab(

  171. child: TVWidget(

  172. hasDecoration: false,

  173. focusChange: (hasFocus) {

  174. if (hasFocus) {

  175. setState(() {

  176. _tabController.animateTo(5);

  177. });

  178. }

  179. },

  180. child: Text(

  181. '亲子',

  182. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  183. ),

  184. ),

  185. ),

  186. Tab(

  187. child: TVWidget(

  188. hasDecoration: false,

  189. focusChange: (hasFocus) {

  190. if (hasFocus) {

  191. setState(() {

  192. _tabController.animateTo(6);

  193. });

  194. }

  195. },

  196. child: Text(

  197. '少综',

  198. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  199. ),

  200. ),

  201. ),

  202. Tab(

  203. child: TVWidget(

  204. focusChange: (hasFocus) {

  205. if (hasFocus) {

  206. setState(() {

  207. _tabController.animateTo(7);

  208. });

  209. }

  210. },

  211. hasDecoration: false,

  212. child: Text(

  213. '分类',

  214. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

  215. ),

  216. ),

  217. ),

  218. ],

  219. ),

  220. ),

  221. body: TabBarView(

  222. controller: _tabController,

  223. children: <Widget>[

  224. HomePage(),

  225. ListPage(),

  226. HomePage(),

  227. ListPage(),

  228. HomePage(),

  229. ListPage(),

  230. HomePage(),

  231. ListPage(),

  232. ],

  233. ),

  234. ),

  235. );

  236. }


  237. startTimeout() {

  238. timer = Timer.periodic(Duration(minutes: 1), (t) {

  239. setState(() {

  240. timeString = TimeUtils.getTime();

  241. });

  242. });

  243. }


  244. @override

  245. void dispose() {

  246. if (timer != null) {

  247. timer.cancel();

  248. timer == null;

  249. }

  250. super.dispose();

  251. }

  252. }



  253. // TAB页面中的其中一个页面,其他类似

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

  255. import 'package:flutter/widgets.dart';

  256. import 'package:flutter_tv/widgets/tv_widget.dart';


  257. class HomePage extends StatefulWidget {

  258. const HomePage({

  259. Key key,

  260. @required this.index,

  261. }) : super(key: key);


  262. final int index;


  263. @override

  264. State<StatefulWidget> createState() {

  265. return HomePageState();

  266. }

  267. }


  268. class HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {

  269. @override

  270. void initState() {

  271. super.initState();

  272. }


  273. @override

  274. Widget build(BuildContext context) {

  275. return Container(

  276. color: Colors.black87,

  277. child: Row(

  278. children: <Widget>[

  279. Flexible(

  280. child: Column(

  281. children: <Widget>[

  282. _buildItem(0),

  283. _buildItem(1),

  284. _buildItem(2),

  285. ],

  286. ),

  287. flex: 1,

  288. ),

  289. Flexible(

  290. child: Column(

  291. children: <Widget>[

  292. _buildImageItem(3, 2),

  293. Expanded(

  294. flex: 1,

  295. child: Row(

  296. children: <Widget>[

  297. _buildImageItem(4, 1),

  298. _buildImageItem(5, 1),

  299. ],

  300. )),

  301. ],

  302. ),

  303. flex: 4,

  304. ),

  305. Flexible(

  306. child: Column(

  307. children: <Widget>[

  308. _buildImageItem(6, 2),

  309. _buildImageItem(7, 1),

  310. ],

  311. ),

  312. flex: 2,

  313. ),

  314. Flexible(

  315. child: Column(

  316. children: <Widget>[

  317. _buildImageItem(8, 2),

  318. _buildImageItem(9, 1),

  319. ],

  320. ),

  321. flex: 2,

  322. ),

  323. ],

  324. ),

  325. );

  326. }


  327. _buildItem(int index) {

  328. return Expanded(

  329. child: TVWidget(

  330. focusChange: (hasfocus) {},

  331. child: Container(

  332. width: MediaQuery.of(context).size.width,

  333. child: GestureDetector(

  334. child: Card(

  335. elevation: 5,

  336. margin: EdgeInsets.all(0),

  337. color: _colors.elementAt(index),

  338. child: Container(

  339. child: Column(

  340. crossAxisAlignment: CrossAxisAlignment.center,

  341. mainAxisAlignment: MainAxisAlignment.center,

  342. children: <Widget>[

  343. _icons.elementAt(index),

  344. _title.elementAt(index),

  345. ],

  346. ),

  347. ),

  348. ),

  349. onTap: () {

  350. _click(index);

  351. },

  352. ),

  353. )),

  354. flex: 1,

  355. );

  356. }


  357. _buildImageItem(int index, int flex) {

  358. return Expanded(

  359. child: TVWidget(

  360. child: Container(

  361. width: MediaQuery.of(context).size.width,

  362. child: GestureDetector(

  363. child: Card(

  364. elevation: 5,

  365. margin: EdgeInsets.all(0),

  366. color: _colors.elementAt(index),

  367. child: Container(

  368. child: Stack(

  369. alignment: Alignment.bottomLeft,

  370. children: <Widget>[

  371. ClipRRect(

  372. child: Image.asset(

  373. _images.elementAt(index),

  374. fit: BoxFit.fill,

  375. width: MediaQuery.of(context).size.width,

  376. height: MediaQuery.of(context).size.height,

  377. ),

  378. borderRadius: BorderRadius.all(

  379. Radius.circular(5),

  380. ),

  381. ),

  382. Container(

  383. width: MediaQuery.of(context).size.width,

  384. child: Column(

  385. mainAxisSize: MainAxisSize.min,

  386. crossAxisAlignment: CrossAxisAlignment.start,

  387. children: <Widget>[

  388. _title.elementAt(index),

  389. index == 3

  390. ? _des.elementAt(index)

  391. : SizedBox(

  392. height: 0,

  393. ),

  394. ],

  395. ),

  396. color: _colors.elementAt(index).withAlpha(240),

  397. padding: EdgeInsets.all(5),

  398. ),

  399. ],

  400. ),

  401. ),

  402. ),

  403. onTap: () {

  404. _click(index);

  405. },

  406. ),

  407. ),

  408. focusChange: (hasfocus) {},

  409. ),

  410. flex: flex,

  411. );

  412. }


  413. void _click(int index) {

  414. switch (index) {

  415. case 0:

  416. break;

  417. case 4:

  418. // Navigator.push(context, MaterialPageRoute(builder: (context) {

  419. // return AboutPage();

  420. // }));

  421. break;

  422. }

  423. }


  424. List<Icon> _icons = [

  425. Icon(

  426. Icons.search,

  427. size: 38,

  428. color: Colors.white,

  429. ),

  430. Icon(

  431. Icons.history,

  432. size: 38,

  433. color: Colors.white,

  434. ),

  435. Icon(

  436. Icons.event,

  437. size: 38,

  438. color: Colors.white,

  439. ),

  440. Icon(

  441. Icons.share,

  442. size: 38,

  443. color: Colors.deepPurpleAccent,

  444. ),

  445. Icon(

  446. Icons.error_outline,

  447. size: 38,

  448. color: Colors.orange,

  449. ),

  450. Icon(

  451. Icons.settings,

  452. size: 38,

  453. color: Colors.red,

  454. )

  455. ];


  456. List<String> _images = [

  457. 'assets/htpy.jpg',

  458. 'assets/htpy.jpg',

  459. 'assets/htpy.jpg',

  460. 'assets/htpy.jpg',

  461. 'assets/agzz.jpg',

  462. 'assets/amypj.jpg',

  463. 'assets/hmjz.jpg',

  464. 'assets/dxflqm.jpg',

  465. 'assets/lifeandpi.jpg',

  466. 'assets/nanasqc.jpg',

  467. ];


  468. List<Color> _colors = [

  469. Colors.red,

  470. Colors.orange,

  471. Colors.green,

  472. Colors.red,

  473. Colors.orange,

  474. Colors.green,

  475. Colors.orange,

  476. Colors.orange,

  477. Colors.orange,

  478. Colors.orange,

  479. ];


  480. List<Text> _title = [

  481. Text(

  482. "搜索",

  483. style: TextStyle(color: Colors.white, fontSize: 18),

  484. ),

  485. Text(

  486. "历史",

  487. style: TextStyle(color: Colors.white, fontSize: 18),

  488. ),

  489. Text(

  490. "专题",

  491. style: TextStyle(color: Colors.white, fontSize: 18),

  492. ),

  493. Text(

  494. "环太平洋",

  495. style: TextStyle(color: Colors.white, fontSize: 18),

  496. ),

  497. Text(

  498. "阿甘正传",

  499. style: TextStyle(color: Colors.white, fontSize: 18),

  500. ),

  501. Text(

  502. "傲慢与偏见",

  503. style: TextStyle(color: Colors.white, fontSize: 18),

  504. ),

  505. Text(

  506. "黑猫警长",

  507. style: TextStyle(color: Colors.white, fontSize: 18),

  508. ),

  509. Text(

  510. "当幸福来敲门",

  511. style: TextStyle(color: Colors.white, fontSize: 18),

  512. ),

  513. Text(

  514. "Life Or PI",

  515. style: TextStyle(color: Colors.white, fontSize: 18),

  516. ),

  517. Text(

  518. "哪啊哪啊神去村",

  519. style: TextStyle(color: Colors.white, fontSize: 18),

  520. ),

  521. ];


  522. List<Text> _des = [

  523. Text(

  524. "非常好看的电影",

  525. style: TextStyle(color: Colors.white, fontSize: 12),

  526. ),

  527. Text(

  528. "设置密码锁",

  529. style: TextStyle(color: Colors.white, fontSize: 12),

  530. ),

  531. Text(

  532. "吐槽反馈你的想法",

  533. style: TextStyle(color: Color.fromRGBO(162, 162, 162, 1), fontSize: 16),

  534. ),

  535. Text(

  536. "非常好看的电影",

  537. style: TextStyle(color: Colors.white, fontSize: 12),

  538. ),

  539. Text(

  540. "版本信息",

  541. style: TextStyle(color: Color.fromRGBO(162, 162, 162, 1), fontSize: 16),

  542. ),

  543. Text(

  544. "系统相关设置",

  545. style: TextStyle(color: Color.fromRGBO(162, 162, 162, 1), fontSize: 16),

  546. ),

  547. Text(

  548. "系统相关设置",

  549. style: TextStyle(color: Color.fromRGBO(162, 162, 162, 1), fontSize: 16),

  550. ),

  551. ];


  552. @override

  553. // TODO: implement wantKeepAlive

  554. bool get wantKeepAlive => true;

  555. }

封装的核心类:

 
   
   
 
  1. // 封装的核心焦点处理类


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

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

  4. import 'package:flutter/widgets.dart';


  5. class TVWidget extends StatefulWidget {

  6. TVWidget(

  7. {Key key,

  8. @required this.child,

  9. @required this.focusChange,

  10. @required this.onclick,

  11. @required this.decoration,

  12. @required this.hasDecoration = true,

  13. @required this.requestFocus = false})

  14. : super(key: key);


  15. Widget child;

  16. onFocusChange focusChange;

  17. onClick onclick;

  18. bool requestFocus;

  19. BoxDecoration decoration;

  20. bool hasDecoration;


  21. @override

  22. State<StatefulWidget> createState() {

  23. return TVWidgetState();

  24. }

  25. }


  26. typedef void onFocusChange(bool hasFocus);

  27. typedef void onClick();


  28. class TVWidgetState extends State<TVWidget> {

  29. FocusNode _focusNode;

  30. bool init = false;

  31. var default_decoration = BoxDecoration(

  32. border: Border.all(width: 3, color: Colors.deepOrange),

  33. borderRadius: BorderRadius.all(

  34. Radius.circular(5),

  35. ));

  36. var decoration = null;


  37. @override

  38. void initState() {

  39. super.initState();

  40. _focusNode = FocusNode();

  41. _focusNode.addListener(() {

  42. if (widget.focusChange != null) {

  43. widget.focusChange(_focusNode.hasFocus);

  44. }

  45. if (_focusNode.hasFocus) {

  46. setState(() {

  47. if (widget.hasDecoration) {

  48. decoration = widget.decoration == null

  49. ? default_decoration

  50. : widget.decoration;

  51. }

  52. });

  53. } else {

  54. setState(() {

  55. decoration = null;

  56. });

  57. }

  58. });

  59. }


  60. @override

  61. Widget build(BuildContext context) {

  62. if (widget.requestFocus && !init) {

  63. FocusScope.of(context).requestFocus(_focusNode);

  64. init = true;

  65. }

  66. return RawKeyboardListener(

  67. focusNode: _focusNode,

  68. onKey: (event) {

  69. if (event is RawKeyDownEvent &&

  70. event.data is RawKeyEventDataAndroid) {

  71. RawKeyDownEvent rawKeyDownEvent = event;

  72. RawKeyEventDataAndroid rawKeyEventDataAndroid =

  73. rawKeyDownEvent.data;

  74. print("keyCode: ${rawKeyEventDataAndroid.keyCode}");

  75. switch (rawKeyEventDataAndroid.keyCode) {

  76. case 19: //KEY_UP

  77. // DefaultFocusTraversal.of(context).inDirection(

  78. // FocusScope.of(context).focusedChild, TraversalDirection.up);

  79. FocusScope.of(context)

  80. .focusInDirection(TraversalDirection.up);

  81. break;

  82. case 20: //KEY_DOWN

  83. FocusScope.of(context)

  84. .focusInDirection(TraversalDirection.down);

  85. break;

  86. case 21: //KEY_LEFT

  87. // FocusScope.of(context).requestFocus(focusNodeB0);

  88. FocusScope.of(context)

  89. .focusInDirection(TraversalDirection.left);

  90. // 手动指定下一个焦点

  91. // FocusScope.of(context).requestFocus(focusNode);

  92. break;

  93. case 22: //KEY_RIGHT

  94. // FocusScope.of(context).requestFocus(focusNodeB1);

  95. FocusScope.of(context)

  96. .focusInDirection(TraversalDirection.right);

  97. // DefaultFocusTraversal.of(context)

  98. // .inDirection(_focusNode, TraversalDirection.right);

  99. // if(_focusNode.nextFocus()){

  100. // FocusScope.of(context)

  101. // .focusInDirection(TraversalDirection.right);

  102. // }

  103. break;

  104. case 23: //KEY_CENTER

  105. widget.onclick();

  106. break;

  107. case 66: //KEY_ENTER

  108. widget.onclick();

  109. break;

  110. default:

  111. break;

  112. }

  113. }

  114. },

  115. child: Container(

  116. margin: EdgeInsets.all(8),

  117. decoration: decoration,

  118. child: widget.child,

  119. ));

  120. }

  121. }


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


好了,关于Flutter TV开发就讲解这么多。
在前面实现过一个比较旧的版本的 Flutter TV 开发,Github 项目地址: https://github.com/flutteranddart/flutterTV
新版的技术方案的 Flutter TV 的 Github 地址: https://github.com/jaychou2012/flutter_tv
新版的技术方案里面有些细节约束处理并没有仔细处理,细节大家可以进行自己处理下,后续也会更新完善。

总结

这里主要是给大家拓展讲解了 Flutter TV 的应用开发,拓展一下思路和技术方向,内容很新颖,希望有更好的技术方向可以一起共享和研究学习。



"开发者说·DTalk" 面向中国开发者们征集 Google Flutter TV 应用的开发尝试 | 开发者说·DTalk移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。


Flutter TV 应用的开发尝试 | 开发者说·DTalk 点击屏末 |  | 了解更多 "开发者说·DTalk" 活动详情与参与方式


长按右侧二维码

报名参与




以上是关于Flutter TV 应用的开发尝试 | 开发者说·DTalk的主要内容,如果未能解决你的问题,请参考以下文章

谷歌Flutter跨平台应用开发SDK,迎来首个发行预览版本

亮相 Google I/O,字节跳动是这样应用 Flutter 的 | 开发者说·DTalk

Flutter 中的 RTCTokenBuilder Agora 保持说 invalidAppID

Flutter 应用程序在 Android TV 上安装但无法打开,为啥?

Android TV 应用程序使用任何带有代码的设备使用网站登录激活

我们为什么选择了Flutter Desktop | 开发者说·DTalk