flutter tv開發之按鍵消息分發機制(上)

在Android開發中,咱們知道用戶消息分爲按鍵消息和觸摸消息,對於TV應用,咱們只考慮按鍵消息。javascript

分析源碼能夠看出,Android是將按鍵的數據獲取和消息處理放在Native層,並提供回調接口給應用層。因爲Flutter框架也是Google團隊寫的,因此對於按鍵消息的處理方式,原理上是同樣的,只不過爲了實現跨平臺,原先android native層扮演的角色變成了各個平臺應用層按鍵消息回調接口,在此基礎上,又作了一層消息封裝,並將按鍵事件回調接口提供給Flutter UI層。html

以android平臺爲例,首先咱們從MainActivity開始分析,這個是應用的主界面。在flutter中,該類繼承FlutterActivity,FlutterActivity存在於flutter專門爲android系統打包的庫裏面,這個庫叫flutter.jar,負責flutter與android創建聯繫。java

這裏寫圖片描述

這個庫還有一個重要的類:FlutterView,flutter跨平臺跨的就是界面,UI繪製不依賴系統組件,那麼這個FlutterView就是將dart編寫的界面封裝成android平臺可使用的控件,通常狀況下,這個控件完成了android應用界面的全部繪製工做,ios也有相同的一套機制,從而實現了不一樣平臺共用一套繪製界面的代碼。android

繼續往下看,FlutterActivity繼承於Activity,同時實現了Provider, PluginRegistry, ViewFactory這幾個接口,其中Provider接口有一個抽象方法getFlutterView(),返回一個FlutterView對象,FlutterActivity實現了這個方法,具體過程爲:ios

//實例化一個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接口,裏面有兩個抽象方法:web

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接口,默認構造函數以下:api

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對象,並進行了非空判斷。app

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開發之按鍵消息分發機制(下)會詳細介紹。