在Android开发中,我们知道用户消息分为按键消息和触摸消息,对于TV应用,我们只考虑按键消息。
分析源码可以看出,Android是将按键的数据获取和消息处理放在Native层,并提供回调接口给应用层。由于Flutter框架也是Google团队写的,所以对于按键消息的处理方式,原理上是一样的,只不过为了实现跨平台,原先android native层扮演的角色变成了各个平台应用层按键消息回调接口,在此基础上,又做了一层消息封装,并将按键事件回调接口提供给Flutter UI层。
以android平台为例,首先我们从MainActivity开始分析,这个是应用的主界面。在flutter中,该类继承FlutterActivity,FlutterActivity存在于flutter专门为android系统打包的库里面,这个库叫flutter.jar,负责flutter与android建立联系。
这个库还有一个重要的类:FlutterView,flutter跨平台跨的就是界面,UI绘制不依赖系统组件,那么这个FlutterView就是将dart编写的界面封装成android平台可以使用的控件,一般情况下,这个控件完成了android应用界面的所有绘制工作,ios也有相同的一套机制,从而实现了不同平台共用一套绘制界面的代码。
继续往下看,FlutterActivity继承于Activity,同时实现了Provider, PluginRegistry, ViewFactory这几个接口,其中Provider接口有一个抽象方法getFlutterView(),返回一个FlutterView对象,FlutterActivity实现了这个方法,具体过程为:
//实例化一个FlutterActivityDelegate对象并定义一个Provider对象viewProvider
private final FlutterActivityDelegate delegate = new FlutterActivityDelegate(this, this);
private final Provider viewProvider;
//在空构造函数里面对viewProvider初始化
public FlutterActivity() {
this.eventDelegate = this.delegate;
this.viewProvider = this.delegate;
this.pluginRegistry = this.delegate;
}
// 实现getFlutterView()接口
public FlutterView getFlutterView() {
return this.viewProvider.getFlutterView();
}
通过getFlutterView()方法,Android平台应用就拿到了Flutter绘制的界面。接下来,我们看看这个特殊的控件FlutterView具体怎么创建的。在FlutterActivityDelegate类里面,定义了一个ViewFactory接口,里面有两个抽象方法:
public interface ViewFactory {
FlutterView createFlutterView(Context var1);
FlutterNativeView createFlutterNativeView();
}
我们看到FlutterActivity虽然implements ViewFactory,但是并没有真正实现接口,两个方法都返回空:
public FlutterView createFlutterView(Context context) {
return null;
}
public FlutterNativeView createFlutterNativeView() {
return null;
}
那么具体实现在哪里呢?往回看,我们发现Provider和PluginRegistry这两个接口对象实例化都是直接将创建的FlutterActivityDelegate对象赋值,查阅FlutterActivityDelegate这个类,我们发现该类同时实现了FlutterActivityEvents, Provider, PluginRegistry接口,默认构造函数如下:
public FlutterActivityDelegate(Activity activity, FlutterActivityDelegate.ViewFactory viewFactory) {
this.activity = (Activity)Preconditions.checkNotNull(activity);
this.viewFactory = (FlutterActivityDelegate.ViewFactory)Preconditions.checkNotNull(viewFactory);
}
再结合FlutterActivity创建FlutterActivityDelegate对象过程发现,FlutterActivityDelegate类里面的activity和viewFactory对象都是指向FlutterActivity对象,并进行了非空判断。
FlutterActivity实现了PluginRegistry这个插件注册接口,打通了Android平台和flutter界面的通讯通道,FlutterActivityDelegate实现的FlutterActivityEvents接口里面定义了和Android 系统Activity相似的生命周期方法:
public interface FlutterActivityEvents extends ComponentCallbacks2, ActivityResultListener, RequestPermissionResultListener {
void onCreate(Bundle var1);
void onNewIntent(Intent var1);
void onPause();
void onResume();
void onPostResume();
void onDestroy();
boolean onBackPressed();
void onUserLeaveHint();
}
追踪代码,我们会看到,在FlutterView类里面,flutter通过发消息的形式将Android平台应用的生命周期通知给dart绘制的flutter界面。flutter对不同平台的应用生命周期数据交互专门定义了一个消息系统:
private final BasicMessageChannel<String> mFlutterLifecycleChannel;
因为此处是讲按键消息机制,对于生命周期相关的,就不再细述。
继续往下分析,我们发现在FlutterActivityDelegate类重写的onCreate()方法里,进行了初始化和创建FlutterView的操作:
public void onCreate(Bundle savedInstanceState) {
if(VERSION.SDK_INT >= 21) {
Window window = this.activity.getWindow();
window.addFlags(-2147483648);
window.setStatusBarColor(1073741824);
window.getDecorView().setSystemUiVisibility(1280);
}
String[] args = getArgsFromIntent(this.activity.getIntent());
FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(), args);
this.flutterView = this.viewFactory.createFlutterView(this.activity);
if(this.flutterView == null) {
FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView();
this.flutterView = new FlutterView(this.activity, (AttributeSet)null, nativeView);
this.flutterView.setLayoutParams(matchParent);
this.activity.setContentView(this.flutterView);
this.launchView = this.createLaunchView();
if(this.launchView != null) {
this.addLaunchView();
}
}
boolean reuseIsolate = true;
if(!this.loadIntent(this.activity.getIntent(), true)) {
String appBundlePath = FlutterMain.findAppBundlePath(this.activity.getApplicationContext());
if(appBundlePath != null) {
this.flutterView.runFromBundle(appBundlePath, (String)null, "main", true);
}
}
}
首先,系统调用了viewFactory的createFlutterView()接口,如果创建失败,再调用viewFactory的createFlutterNativeView()方法实例化一个FlutterNativeView对象,然后将这个对象作为参数调用FlutterView的构造函数进行实例化。
打开FlutterView这个类,我们看到,如果传入的FlutterNativeView对象为空,系统会去调用带一个Context对象的FlutterView构造函数对其持有的FlutterNativeView对象进行初始化。
终于重量级人物登场:FlutterView,这个类是实现Android应用界面交给flutter框架绘制的核心类,界面能做到平台无关,是因为FlutterView继承SurfaceView。
我们知道Android的View是绘制在”表层”上面,对于SurfaceView来说,它自己就是充当表层本身。SurfaceView就是在窗口上挖一个洞,它自己显示在这个洞里,其他的View是显示在窗口上,所以View可以显示在 SurfaceView之上,你也可以添加一些层在SurfaceView之上。
从API中可以看出SurfaceView属于View的子类, 它是专门为制作游戏而产生的,它的功能非常强大,最重要的是它支持OpenGL ES库,2D和3D的效果都可以实现。这就是为什么我们说Flutter应用跨平台机制可以看做是一个游戏App,实现了一个类似的代码引擎。
既然flutter界面是直接继承于SurfaceView的,它的绘制过程就不再依赖于系统平台,解耦了系统控件的调用,flutter编写的界面就可以在Android、IOS等平台上运行。
并且,SurfaceView实现了双缓冲机制。我们知道Android系统提供了View进行绘图处理,我们通过自定义的View可以满足大部分的绘图需求。但是我们通常自定义的View是用于主动更新情况的,用户无法控制其绘制的速度。
由于View是通过invalidate方法通知系统去调用view.onDraw方法进行重绘,而Android系统是通过发出VSYNC信号来进行屏幕的重绘,刷新的时间是16ms,如果在16ms内View完成不了执行的操作,用户就会看着卡顿。
比如当draw方法里执行的逻辑过多,需要频繁刷新的界面上,例如游戏界面,那么就会不断的阻塞主线程,从而导致画面卡顿。而SurfaceView相当于是另一个绘图线程,它是不会阻碍主线程的。
简单介绍完SurfaceView,我们继续往下走。flutter跨平台开发,除了关注界面的绘制,更为关心的是怎么跟不同的平台进行通讯,主要是数据和部分业务逻辑的通信。
说到这里,我们不得不提flutter的消息系统,这个消息系统目前分为7个模块管理:多语言、路由导航、按键消息、生命周期、系统信息、用户设置和平台插件,这几乎涵盖了不同平台所有差异化最大的功能,这几个模块非常依赖原生系统。
我们看下代码中的定义:
private final MethodChannel mFlutterLocalizationChannel;
private final MethodChannel mFlutterNavigationChannel;
private final BasicMessageChannel<Object> mFlutterKeyEventChannel;
private final BasicMessageChannel<String> mFlutterLifecycleChannel;
private final BasicMessageChannel<Object> mFlutterSystemChannel;
private final BasicMessageChannel<Object> mFlutterSettingsChannel;
//这个插件消息管理对象被定义为局部的变量,上面几个都是在很多地方使用的
MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);
严格来说,这7个消息接口还要分为两类:一类是通过反射原生系统的api进行数据通讯,一类是真正意义上的将数据打包成特殊格式以消息的形式和使用dart编写的flutter控件交互。消息数据流都是以二进制形式传输的,对于不同语言编写的平台系统,二进制格式想必都认识。
再看下这7个消息管理对象的初始化:
this.mFlutterLocalizationChannel = new MethodChannel(this, "flutter/localization", JSONMethodCodec.INSTANCE);
this.mFlutterNavigationChannel = new MethodChannel(this, "flutter/navigation", JSONMethodCodec.INSTANCE);
this.mFlutterKeyEventChannel = new BasicMessageChannel(this, "flutter/keyevent", JSONMessageCodec.INSTANCE);
this.mFlutterLifecycleChannel = new BasicMessageChannel(this, "flutter/lifecycle", StringCodec.INSTANCE);
this.mFlutterSystemChannel = new BasicMessageChannel(this, "flutter/system", JSONMessageCodec.INSTANCE);
this.mFlutterSettingsChannel = new BasicMessageChannel(this, "flutter/settings", JSONMessageCodec.INSTANCE);
//platformPlugin负责处理平台插件消息,并提供回调接口
PlatformPlugin platformPlugin = new PlatformPlugin(activity);
MethodChannel flutterPlatformChannel = new MethodChannel(this, "flutter/platform", JSONMethodCodec.INSTANCE);
//监听平台插件消息回调接口
flutterPlatformChannel.setMethodCallHandler(platformPlugin);
这里,我们只看按键消息处理流程。
FlutterView类有两个处理按键事件的接口,一个是onKeyUp(),一个是onKeyDown(),分别对应按键松开和按下事件,两个方法流程一样,在tv交互中,我们一般只关心遥控按键按下事件,所以我们这里只分析onKeyDown()。
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(!this.isAttached()) {
return super.onKeyDown(keyCode, event);
} else {
Map<String, Object> message = new HashMap();
message.put("type", "keydown");
message.put("keymap", "android");
this.encodeKeyEvent(event, message);
this.mFlutterKeyEventChannel.send(message);
return super.onKeyDown(keyCode, event);
}
}
分析代码可知,当FlutterView没有绑定到Activity时,直接返回父类View的onKeyDown()方法返回值,否则会创建一个Map对象,key为String类型,value为Object,因为消息发送时类型是被定义为泛型的,而Object类是所有类的父类,所以value没有具体指定某一个子类对象类型。
这个map存放了按键处理需要的数据,第一组键值对key是type,这个value有两个可取值,如果是onKeyDown(), 值为”keydown”,同理,onKeyUp(),值为”keyup”。第二组键值对key字段是keymap, 用于区分按键消息是哪个平台发来的,如果应用是运行在Android系统,那么传的值为”android”,如果运行在Fuchsia系统,那么传的值为”fuchsia”,目前flutter api只处理了这两个系统的按键消息,至于IOS,由于不太熟悉这个系统框架,或许按键处理有另一套机制?
其余的几个字段因为对于onKeyUp()和onKeyDown()是一样的,所以系统封了一个方法encodeKeyEvent():
private void encodeKeyEvent(KeyEvent event, Map<String, Object> message) {
message.put("flags", Integer.valueOf(event.getFlags()));
message.put("codePoint", Integer.valueOf(event.getUnicodeChar()));
message.put("keyCode", Integer.valueOf(event.getKeyCode()));
message.put("scanCode", Integer.valueOf(event.getScanCode()));
message.put("metaState", Integer.valueOf(event.getMetaState()));
}
这几个KeyEvent字段是不是很熟悉?我们看flutter的源码注释:
/// See <https://developer.android.com/reference/android/view/KeyEvent.html#getFlags()>;
final int flags;
/// See <https://developer.android.com/reference/android/view/KeyEvent.html#getUnicodeChar()>;
final int codePoint;
/// See <https://developer.android.com/reference/android/view/KeyEvent.html#getKeyCode()>;
final int keyCode;
/// See <https://developer.android.com/reference/android/view/KeyEvent.html#getScanCode()>;
final int scanCode;
/// See <https://developer.android.com/reference/android/view/KeyEvent.html#getMetaState()>;
final int metaState;
意思很明确,这几个字段就是对应Android的KeyEvent属性,如果不知道是干啥的,让我们自己去Android开发官网查阅相关api文档。
到这里,我们熟悉了Flutter跟Android平台按键消息桥接的过程,顺便了解了Flutter跟Android原生API交互的原理。最后,总结下Flutter在Android平台下按键消息分发流程从底层到UI层几个关键类调用:
Android平台处理按键消息
Goldfish_event.c
处于Linux内核层
负责驱动按键消息
EventHub.cpp
硬件抽象层
用来读取设备文件中的RawEvent
com_android_server_KeyInputQueue.cpp
JNI本地方法
向Java框架层提供了函数android_server_KeyInputQueue_readEvent,用于读取输入设备事件
WindowManagerService.java
Java框架层
窗口管理服务,通过InputManager提供的接口开启一个线程驱动InputReader不断地从/dev/input/目录下面的设备文件读取事件,然后通过InputDispatcher分发给连接到WindowManagerService服务的客户端。
KeyInputQueue.java
Java框架层
创建一个线程,循环读取事件,并把事件放入事件队列里
InputManager.java
Java框架层
监控按键事件
ViewRootImpl.java
Android UI 层
Android的Activity通过该类的setView()接口来注册和销毁键盘消息接收通道
DecorView.java
Android UI层
分发按键事件,重要方法 dispatchKeyEvent()
FlutterView.java
Android和Flutter UI桥接层
封装按键消息
Flutter将平台的按键事件以消息方式拦截
system_channels.dart
dart框架层
flutter消息系统,在按键事件中负责按键消息的传递
raw_keyboard.dart
dart框架层
里面定义了按键事件相关的几个类,包括2个抽象类RawKeyEvent、RawKeyEventData及其他们的子类实现,1个监听按键事件的RawKeyBoard类。这个dart语言喜欢将几个相关类弄在一个文件里,跟Java语言一个类一个文件的规则相悖,需要适应。
RawKeyEvent有一个工厂方法,这个方法返回一个子类实现的对象,也就是继承RawKeyEvent的RawKeyUpEvent或者RawKeyDownEvent对象。具体过程是,先将接收到的不同平台按键数据生成具体某个平台的RawKeyEventData,根据消息中的keymap字段来区分平台,比如Android平台,是将data实例化成RawKeyEventDataAndroid子类对象。同时,会根据消息中的type字段来决定返回RawKeyUpEvent对象还是RawKeyDownEvent对象。
/// Creates a concrete [RawKeyEvent] class from a message in the form received /// on the [SystemChannels.keyEvent] channel. factory RawKeyEvent.fromMessage(Map<String, dynamic> message) { RawKeyEventData data; final String keymap = message['keymap']; switch (keymap) { case 'android': data = new RawKeyEventDataAndroid( flags: message['flags'] ?? 0, codePoint: message['codePoint'] ?? 0, keyCode: message['keyCode'] ?? 0, scanCode: message['scanCode'] ?? 0, metaState: message['metaState'] ?? 0, ); break; case 'fuchsia': data = new RawKeyEventDataFuchsia( hidUsage: message['hidUsage'] ?? 0, codePoint: message['codePoint'] ?? 0, modifiers: message['modifiers'] ?? 0, ); break; default: // We don't yet implement raw key events on iOS, but we don't hit this // exception because the engine never sends us these messages. throw new FlutterError('Unknown keymap for key events: $keymap'); } final String type = message['type']; switch (type) { case 'keydown': return new RawKeyDownEvent(data: data); case 'keyup': return new RawKeyUpEvent(data: data); default: throw new FlutterError('Unknown key event type: $type'); } }
RawKeyBoard类负责监听平台发送的按键消息,并实现消息回调,在消息回调里调用RawKeyEvent的fromMessage()方法实例化一个RawKeyEvent对象,并将该对象作为参数以回调的方式供响应按键事件的控件使用。
class RawKeyboard { RawKeyboard._() { SystemChannels.keyEvent.setMessageHandler(_handleKeyEvent); } /// The shared instance of [RawKeyboard]. static final RawKeyboard instance = new RawKeyboard._(); final List<ValueChanged<RawKeyEvent>> _listeners = <ValueChanged<RawKeyEvent>>[]; /// Calls the listener every time the user presses or releases a key. /// /// Listeners can be removed with [removeListener]. void addListener(ValueChanged<RawKeyEvent> listener) { _listeners.add(listener); } /// Stop calling the listener every time the user presses or releases a key. /// /// Listeners can be added with [addListener]. void removeListener(ValueChanged<RawKeyEvent> listener) { _listeners.remove(listener); } Future<dynamic> _handleKeyEvent(dynamic message) async { if (_listeners.isEmpty) return; final RawKeyEvent event = new RawKeyEvent.fromMessage(message); if (event == null) return; for (ValueChanged<RawKeyEvent> listener in new List<ValueChanged<RawKeyEvent>>.from(_listeners)) if (_listeners.contains(listener)) listener(event); } }
raw_keyboard_listener.dart
flutter ui层
负责定义响应按键的控件容器,监听焦点变化事件、提供按键回调接口
该文件定义了一个RawKeyboardListener类,该类继承StatefulWidget,所以是带状态的控件,在状态初始化时,会监听控件焦点变化:
@override void initState() { super.initState(); widget.focusNode.addListener(_handleFocusChanged); } void _handleFocusChanged() { if (widget.focusNode.hasFocus) _attachKeyboardIfDetached(); else _detachKeyboardIfAttached(); }
只有控件获得焦点时,才会响应按键事件,具体做法是,当控件有焦点时,监听按键事件并实现回调:
void _attachKeyboardIfDetached() { if (_listening) return; RawKeyboard.instance.addListener(_handleRawKeyEvent); _listening = true; }
当失去焦点时,移除事件监听:
void _detachKeyboardIfAttached() { if (!_listening) return; RawKeyboard.instance.removeListener(_handleRawKeyEvent); _listening = false; }
我们再看这个_handleRawKeyEvent()方法,实际上是通过用户的按键操作回调按键事件接口:
void _handleRawKeyEvent(RawKeyEvent event) { if (widget.onKey != null) widget.onKey(event); }
如何使用该控件呢?实际上dart这门语言最大的特色是万物皆控件,所以一个基础控件想要扩展功能,就要被拥有该功能的控件包裹一层,也就是作为别人的孩子节点,在设计模式中,我们称为装饰模式。具体到这里,那就是,你想让某个控件响应按键事件,你就要让该控件的父节点为RawKeyboardListener。
关于Flutter界面接收系统平台的按键消息后如何实现UI交互,下一篇flutter tv开发之按键消息分发机制(下)会详细介绍。