flutter tv开发之按键消息分发机制(上)

在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层几个关键类调用:

  1. 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桥接层

      • 封装按键消息

  2. 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开发之按键消息分发机制(下)会详细介绍。

猜你喜欢

转载自blog.csdn.net/johnwcheung/article/details/79180732