我正在参加「掘金·启航计划」
prefácio
Na prática do Flutter em dispositivos desktop, implementamos vários projetos para verificar totalmente a viabilidade de sua tecnologia e negócios. Ao mesmo tempo, mais requisitos são apresentados para o Flutter, incluindo: a adaptação do controle remoto!
Infelizmente, o funcionário do Flutter não considerou a adaptação do controle remoto, e até mesmo seu controle de foco tem muitos problemas [como: oscilação de comutação, o foco não pode responder em operação rápida], então temos que quebrar essa barreira por nós mesmos.
Como funciona o controle remoto?
O controle remoto é na verdade um evento de botão no nível do software, que é equivalente ao nosso teclado . O próprio Android possui equipamento de TV, portanto já possui um conjunto completo de mecanismos de controle.
No Android, precisamos apenas de pares view声明可以获取焦点,指定焦点关系即可
. A estrutura nos ajudará a focar automaticamente no controle correspondente; o estado simples da interface do usuário pode ser resolvido com um SelectDrawable.
Comparado com a IU declarativa do Flutter,Android命名式UI有天然的优势,通过生成的R文件,框架层可以通过FocusFinder轻易的找到对应的view,处理好焦点的分发流转和事件处理。
Dificuldades do controle remoto Flutter
- Flutter é uma IU declarativa e é difícil gerar classes de recursos em tempo de compilação;
- Widget é apenas uma informação de configuração, se quisermos obter as informações do widget durante o processo de desenvolvimento, devemos obter o objeto Rander através do valor Key. É muito trabalhoso obter as informações de cada componente para determinar a direção e executar o evento;
- Com a prática, descobrimos que ainda existem muitos problemas no focusNode do Flutter. Durante o processo de solicitação, o foco ocasionalmente repete a solicitação e a troca rápida do foco responderá a erros; é mais óbvio em dispositivos Android;
- Alguns controles na biblioteca de componentes de materiais do Flutter mantêm o próprio focusNode e respondem a eventos de teclado por padrão , o que entrará em conflito com nosso próprio gerenciamento de foco.
princípio de realização
Abandone o mecanismo de transferência FocusNode integrado do Flutter , porque o Flutter não nos ajuda em nada com essa transferência.
Ouça os eventos sozinho, distribua o foco e o lado comercial se junta ao ControlManager para saber qual foco está acionado no momento.
Etapas de implementação
1. Componente de recebimento de evento de controle remoto global
既然Flutter无法轻松的生成焦点的流转关系,那我们就自行处理,定义一个全局的事件接收器。这个组件需要接收一个child,一般把MaterialApp
传入。
组件内部接收按键事件,解析后进行事件分发。
class ControlApp extends StatelessWidget {
ControlApp({Key? key, required this.app}) : super(key: key);
final Widget app;
final FocusNode remoteControl = FocusNode(debugLabel: 'Flutter遥控器');
final EventBus _eventBus = EventBus.instance;
@override
Widget build(BuildContext context) {
return RawKeyboardListener(
onKey: (RawKeyEvent event) {
if (event is RawKeyDownEvent) {
RawKeyEventDataWindows rawKeyEventData =
event.data as RawKeyEventDataWindows;
switch (rawKeyEventData.keyCode) {
case 38: //KEY_UP
_eventBus.fire(EventType.up);
break;
case 40: //KEY_DOWN
_eventBus.fire(EventType.down);
break;
case 37: //KEY_LEFT
_eventBus.fire(EventType.left);
break;
case 39: //KEY_RIGHT
_eventBus.fire(EventType.right);
break;
case 13: //KEY_ENTER
_eventBus.fire(EventType.enter);
break;
case 8: //KEY_BACK
_eventBus.fire(EventType.back);
break;
default:
break;
}
}
},
focusNode: remoteControl,
child: app,
);
}
}
复制代码
2. 统一焦点接收类管理
ControlApp
统一分发后,ControlManager
会进行接收,同时ControlManager
将管理所有的遥控器监听者,提供addListener
、removeListener
、syncFocusKey
、changeTopFocus
等接口;保证遥控事件只发送给最后加入的监听者。
final controlManager = ControlManager.instance;
class ControlManager {
ControlManager._() {
initEventListen();
}
static final ControlManager instance = ControlManager._();
final EventBus _eventBus = EventBus.instance;
final ObserverList<ControlListener> _listeners =
ObserverList<ControlListener>();
bool receiveEvents = true;
int _currListenerIndex = 0;
// 记录所有listener的焦点集合
final List<List<FocusCollection>> _allCollectionList = [];
// 记录每个listener选择的焦点集合
final List<FocusCollection?> _selectedCollectionList = [];
void addListener(ControlListener listener,
{required List<FocusCollection> focusList}) {
_listeners.add(listener);
_allCollectionList.add(focusList);
_currListenerIndex++;
_selectedCollectionList.add(null);
}
void removeListener(ControlListener listener) {
_listeners.remove(listener);
_selectedCollectionList.removeLast();
_allCollectionList.removeLast();
_currListenerIndex--;
}
/// 修改顶层焦点集合
void changeTopFocus(List<FocusCollection> focusList,
{FocusCollection? curr}) {
_allCollectionList.last = focusList;
if (curr == null) {
_selectedCollectionList[_currListenerIndex - 1] = null;
_listeners.last.onCancelSelect();
} else {
_selectedCollectionList[_currListenerIndex - 1] = curr;
_listeners.last.onWidgetSelect(curr);
}
}
/// 是否暂停处理遥控器事件
setReceiveStatus(bool isReceive) {
receiveEvents = isReceive;
}
/// 同步焦点的建值
syncFocusKey(FocusCollection focus) {
_selectedCollectionList[_currListenerIndex - 1] = focus;
}
bool get focusIsEmpty => _allCollectionList[_currListenerIndex - 1].isEmpty;
initEventListen() {
_eventBus.on().listen((event) {
if (!receiveEvents) return;
switch (event) {
case EventType.up:
case EventType.left:
if (focusIsEmpty) return;
_listeners.last.onWidgetSelect(getFocusCollection(event));
break;
case EventType.enter:
if (focusIsEmpty) return;
if (_selectedCollectionList[_currListenerIndex - 1] == null) return;
_listeners.last.onEnterEvent(getFocusCollection(event));
break;
case EventType.down:
case EventType.right:
if (focusIsEmpty) return;
_listeners.last.onWidgetSelect(getFocusCollection(event));
break;
case EventType.back:
if (focusIsEmpty) {
_listeners.last.onBackEvent(null);
} else {
_listeners.last.onBackEvent(getFocusCollection(event));
}
break;
}
});
}
/// 获取命中的焦点集合
FocusCollection getFocusCollection(EventType type) {
if (_selectedCollectionList[_currListenerIndex - 1] == null) {
var c = _allCollectionList[_currListenerIndex - 1].first;
_selectedCollectionList[_currListenerIndex - 1] = c;
return c;
} else {
FocusCollection c = _selectedCollectionList[_currListenerIndex - 1]!;
List<FocusCollection> l = _allCollectionList[_currListenerIndex - 1];
switch (type) {
case EventType.up:
if (c.up != null) c = l.firstWhere((e) => e.currKey == c.up);
break;
case EventType.down:
if (c.down != null) c = l.firstWhere((e) => e.currKey == c.down);
break;
case EventType.left:
if (c.left != null) c = l.firstWhere((e) => e.currKey == c.left);
break;
case EventType.right:
if (c.right != null) c = l.firstWhere((e) => e.currKey == c.right);
break;
case EventType.enter:
case EventType.back:
break;
}
_selectedCollectionList[_currListenerIndex - 1] = c;
return c;
}
}
}
复制代码
3. 提供遥控器监听抽象类
需要监听遥控事件的页面(或组件),需要混入这个类,然后在对应生命钩子中调用ControlManager
中的addListener和removeListener事件。
abstract class ControlListener {
void onWidgetSelect(FocusCollection collection) {}
void onCancelSelect() {}
void onEnterEvent(FocusCollection collection) {}
void onBackEvent(FocusCollection? collection) {}
}
复制代码
重写ControlListener中提供的接口,就能准确接收到ControlManager发出的事件拉!
class _MyHomePageState extends State<MyHomePage> with ControlListener {
int _counter = 0;
String selectKey = ""; // 选择的焦点key
// 自行维护焦点的关系
List<FocusCollection> focusList = [
FocusCollection("one", right: "two", down: "three"),
FocusCollection("two", left: "three", up: "one"),
FocusCollection("three", up: "one", right: "two")
];
@override
void initState() {
super.initState();
controlManager.addListener(this, focusList: focusList);
}
@override
void dispose() {
super.dispose();
controlManager.removeListener(this);
}
@override
void onWidgetSelect(FocusCollection collection) {
super.onWidgetSelect(collection);
setState(() {
selectKey = collection.currKey;
});
print(">>>>> onWidgetSelect ${collection.currKey}");
}
@override
void onBackEvent(FocusCollection? collection) {
super.onBackEvent(collection);
print(">>>>> onBackEvent ${collection?.currKey}");
}
@override
void onEnterEvent(FocusCollection collection) {
super.onEnterEvent(collection);
int i = focusList.indexOf(collection);
if (i == 2) {
_incrementCounter();
} else if (i == 0) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => SecondPage()));
}
}
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(children: [
Positioned(
top: 20,
left: 20,
child: FocusWidget(
hasFocus: selectKey == "one",
child: const Text('You have pushed the button this many times:'),
),
),
Positioned(
top: 60,
right: 20,
child: FocusWidget(
hasFocus: selectKey == "two",
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
),
),
Positioned(
top: 60,
left: 20,
child: FocusWidget(
hasFocus: selectKey == "three",
child: GestureDetector(
onTap: _incrementCounter,
child: Icon(Icons.add, color: Theme.of(context).primaryColor),
),
),
),
]),
);
}
}
复制代码
- 从上面的例子可以看到,业务端是需要提供焦点列表,自行指定好上下左右的焦点关键字;这个步骤其实就跟Android中,在xml指定焦点关系一样。
android:nextFocusDown="@id/button1"
android:nextFocusUp="@id/button2"
android:nextFocusLeft="@id/button3"
android:nextFocusRight="@id/button4"
复制代码
- 同时例子中也有FocusWidget这个控件,我目前是完全摈弃了Flutter自带的这套FoucusNode,而是明确告诉Widget你被选中了、被点击了。这能解决FoucusNode本身的一些缺陷。
- 当遇到带有FocusNode的组件,比如Input、Button、InkWell等,请一定要自主传入FoucusNode,并且设置skipTraversal为true,这样这个焦点就不会自动接收键盘事件,不会导致焦点冲突。
FocusNode focusNode_1 = FocusNode(skipTraversal: true);
复制代码
继续优化
在上面的方案中,我们发现一个最通过的点就是:接入相对复杂,特别是点击事件还要由页面级别通知到对应组件自行去执行。对于封装程度高的项目,这是很致命的。
因此我们把FocusWidget抽象出来,页面级别不再维护复杂的焦点关系列表
;而是在需要处理焦点的控件外部包裹FocusWidget,传入需要的focus、nextFocusDown、nextFocusUp、nextFocusLeft、nextFocusRight;以及需要的UI状态、点击事件等。
之后遥控器插件会自动找寻对应的FocusWidget,显示其选中状态,并触发传入的点击事件!
将焦点关系和事件处理完全融合在一起,不需要单独声明和维护,从而快速的让我们的应用适配遥控器 ~
写在最后
这个方案目前在业务上已经得到实践认可,但是细节以及优化方案还有很多需要注意的。
总之遥控器在Flutter上是个相对难啃的坑,只能不断往前爬,往前填。