移植一個抖音貼紙組件到Flutter

本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。交流qq羣:859640274java

你們很久不見,又有一個多月沒有發文章了,因此今天發一篇來刷刷存在感。最近 Flutter 很是火,我這一個月也不斷的找資料來學習 Flutter。通過一段時間的摸索,我發現如今不少資料都很是」水「。各類 Dart 入門、Flutter 入門、Flutter 資料收集,徹底沒有任何有趣的東西。我不想去寫重複而無聊的文章,因此本篇文章會拋轉引玉的探討一些在學習和開發 Flutter 的過程當中碰見的問題和解決方案。android

閱讀須知:ios

  • 1.WE——>WsElement、ECWS——>ElementContainerWidgetState、EAL——>ElementActionListener

本文分爲如下章節,讀者可按需閱讀:git

  • 1.Flutter之問——以 QA 的形式來闡述我對 Flutter 的見解和學習經驗。
  • 2.移植一個Flutter控件——將仿寫抖音的貼紙控件移植到 Flutter 中。
  • 3.Flutter探究——聊一聊 Flutter 的原理。
  • 4.尾巴

1、Flutter之問

天下事有難易乎?爲之,則難者亦易已!程序員

Q:Flutter 怎麼學?github

A:這是老生常談的問題了。隨便打開一個 Flutter 系列文章,都會爲你鋪平接下來幾周的路。可是幾周以後呢?彷佛不多文章會接着寫下去,**畢竟大腦最喜歡簡單的東西(我也不例外),一件事情的難度與受歡迎程度成反比。**因此 Flutter 怎麼學?所謂:取乎其上,得乎其中。我只有一句話:以讓 Flutter 成爲你最拿手技能爲目標去學。web

Q:能給一些 Flutter 的學習資料嗎?編程

A:我列舉一下我學習 Flutter 過程當中用到的資料:後端

  • 1.Dart官網,啃完官方文檔,Dart 你就入門了。api

  • 2.Flutter實戰,這本開源書的例子不少,所有敲一遍 flutter 你就入門了。特別是最後的 Flutter 原理分析能夠仔細看看。

  • 3.Flutter github 倉庫,如今網絡上 Flutter 原理分析的文章真的很是少,因此真想要成爲 Flutter 專家,你必須做爲開拓者去閱讀 Flutter 在各類層級下的源碼。

Q:Flutter 會幹掉 Native?

A:Flutter 是 Native 的子集。在手機被」革命「以前,但凡業務比較複雜的公司,只會要求 Native 工程師掌握 Flutter。而不會出現拋棄 Native 只作 Flutter 的工程師,由於 Flutter 說一千道一萬隻是一個 ui 框架。畢竟它自身的複雜度很難支撐起比它還複雜的業務。以上只是我的觀點,有分歧能夠在評論區探討

Q:Flutter 哪些地方作的比 Native 好?

A:下面是我總結出來的 Flutter 比 Native 好的地方:

  • 1.ios、android 一把抓,還可能帶上 web、mac、pc。
  • 2.Dart 語言很是現代,比 java、oc 好上太多。
  • 3.新興框架沒有歷史包袱。
  • 4.熱更技術很是誘人。
  • 5.入門很簡單。

2、移植一個FluTter控件

常常讀個人文章的讀者應該看過我上一篇文章:抖音、ins、微信功能大比拼——Story的貼紙文字,這篇文章中詳細比較了各家 Story 的貼紙文字的功能,而後在 Android 端實現了一個貼紙框架。而這一章我就打算將這個貼紙框架移植到 Flutter,相信最後的還原度會超過你的想象。接下來建議配合源碼閱讀文章。注意這一章的大部份內容和上一篇文章中講解 Android 端實現控件的章節是差很少的。

github 地址

使用方式:sticker_framework: ^0.0.1

1.架構方式

咱們第一節先講講文字貼紙控件的架構實現,我會基於下面的 圖1 和 github 上的代碼進行講解。建議你們把代碼 clone 下來,固然別忘了給個 star。

flutter文字貼紙架構.jpg

咱們先來根據圖1來說講整個控件的架構

  • 1.咱們先從總體來看:
    • 1.咱們須要選擇一個 StatefulWidget 做爲基本的容器。因此圖中的 ElementContainerWidgetState 就是一個構造這樣的容器的 State,簡單歸納一下它有這些功能:
      • 1.處理各類手勢事件,這裏的手勢包括單指和雙指。
      • 2.添加和刪除一些子 Widget。這裏的子 Widget 用於繪製各類元素。
      • 3.提供一些 api 讓外部能操控元素。
      • 4.提供一個 listener,讓外部可以監聽內部的各類流程。
    • 2.有了繪製容器,咱們須要向繪製容器裏面添加 Widget。而 Widget 在用戶操做的過程當中須要有各類數據,因此這裏我用了 WE 來封裝須要展現的 Widget,其內部有下面這些東西:
      • 1.各類用戶操做過程當中須要的數據例如:scale、rotate、x、y等等。
      • 2.有一些方法可以經過數據來更新 Widget。
      • 3.提供一些 api 讓 ECWS 能更新 WE 裏面的數據 。
    • 3.由 ECWS 和 WE 就能繼續繼承出各類各樣的擴展控件。
  • 2.總體講完了,咱們就能夠來仔細的講講圖中的流程
    • 1.先講橫着的箭頭:外部/內部調用,外部須要調用 ECWS 來進行對 WE 的增刪改查等操做時會進入這個路徑,這個路徑裏能夠有下面這些操做:
      • 1.addElement:向 ECWS 中添加一個元素。
      • 2.deleteElement:從 ECWS 中刪除一個元素。
      • 3.update:讓 WE 根據當前數構建出一個 Widget。
      • 4.findElementByPosition:找到傳入的座標下的最頂層的 WE。
      • 5.selectElement:選中一個 WE 且將其調到最頂層。
      • 6.unSelectElement:取消選中一個 WE。
    • 2.再來說豎着的箭頭:手勢事件流,這裏中間會經歷一些內部邏輯咱們後面來說,最終事件流會觸發下面的一系列行爲:
      • 1.單指移動的整個流程:當咱們選中了一個 WE 的時候就能夠對它進行移動。這裏移動能夠分爲開始、進行中、結束。每一個事件都會調用 WE 的對應方法以更新其內部數據。
      • 2.雙指旋轉縮放的整個流程:當咱們選中了一個 WE 的時候能夠用雙指對它進行縮放和旋轉。這裏能夠分爲開始、進行中、結束。這裏也會調用 WE 的對應方法更新數據。
      • 3.選中元素再次點擊:當咱們選中了一個 WE 的時候,能夠對其再次點擊。
      • 4.點擊空白區域:當咱們沒有點擊任意 WE 的時候能夠進行一些操做,例如清除當前 WE 的選中狀態。這個行爲是能夠繼承的,能夠交由子類來覆寫。
      • 5.子類事件:咱們看上面其實感受觸發的事件比較少。因此在 down、move、up 的時候會優先調用三個方法 downSelectTapOtherAction、scrollSelectTapOtherAction、upSelectTapOtherAction。這三個方法能夠被子類覆寫,若是返回 true 的話表示事件已經消耗了,ECWS 就不會再觸發其餘事件。這樣一來子類也能夠對手勢進行擴展,例如按住某個地方單指縮放等等。
      • 7.我圖中 ECWS 也實現了一個子類 DECWS,這個類簡單的加兩個手勢:
        • 1.單指移動縮放:相似抖音的隨拍,按住元素的右下角的時候能夠用拖動來對元素進行縮放和旋轉。
        • 2.刪除:相似抖音的隨拍,點擊元素左上角的時候能夠直接刪除元素。
    • 3.圖1中有一個特性其實沒有畫出來由於畫不下了,那就是:ECWS 在1和2中的幾乎全部行爲都能被外部監聽,ElementActionListener 就是負責監聽的接口。ECWS 中存有一個 EAL 的 set 集合因此監聽器能夠添加多個。

2.技術點實現

我在開發整個控件的時候遇到過比較多的技術實現上的難點,因此這一節就選一些來說講,讓讀者在看源碼的時候不會特別困惑。

(1).定義數據結構與繪製座標系

-----代碼塊1----- ws_element.dart

int mZIndex = -1; // 圖像的層級

  double mMoveX = 0.0; // 初始化後相對 ElementContainerWidget 中心的移動距離

  double mMoveY = 0.0; // 初始化後相對 ElementContainerWidget 中心的移動距離

  double mOriginWidth; // 初始化時內容的寬度

  double mOriginHeight; // 初始化時內容的高度

  Rect mEditRect; // 可繪製的區域

  double mRotate = 0.0; // 圖像順時針旋轉的角度,以 π 爲基準

  double mScale = 1.0; // 圖像縮放的大小

  double mAlpha = 1.0; // 圖像的透明度

  bool mIsSelected = false; // 是否處於選中狀態

  bool mIsSingeFingerMove = false; // 是否處於單指移動的狀態

  bool mIsDoubleFingerScaleAndRotate = false; // 是否處於雙指旋轉縮放的狀態

  Widget mElementShowingWidget; // 展現內容的 widget

  Offset mOffset; // ElementContainerWidget 相對屏幕的位移
複製代碼

函數未動數據先行,數據結構是一個框架很是核心的東西,定義了一個好的數據結構能夠省去不少沒必要要的代碼。因此這一小節咱們來根據代碼塊1定義一下數據結構和 Widget 繪製座標系

  • 1.咱們將 WE 所在的 ECWS 做爲 WE 中 view 的可繪製區域,代碼塊1中的 mEditRect 就是這個區域表明的矩形。因此 mEditRect 通常爲**[0, 0, ECWS.getWidth, ECWS.getHeight],mEditRect 的單位爲px**。

  • 2.咱們定義的座標系原點在 mEditRect 的中心點,也就是 ECWS 的中心點。mMoveX、mMoveY 分別表示 view 距離座標系原點的距離。由於它們倆默認爲 0,因此通常 view 被添加到 ECWS 中的時候默認位置就在 ECWS 的中心。這兩個參數的單位爲px

  • 3.咱們的座標系具備 z 軸,mZIndex 就是 z 軸的座標,z 軸表示 view 的層疊關係,mZIndex 爲 0 時表示 view 在 ECWS 的頂層。mZindex 默認爲 -1,表示 view 沒有被添加到 ECWS 中。mZIndex 是整數

  • 4.咱們定義 mRotate 爲正時 view 順時針轉動,mRotate 的區間爲[-360,360]。

    5.咱們定義 view 沒有縮放的時候 mScale 爲 1,mScale 爲 2 的時候表示 view 放大 2 倍,以此類推。

  • 6.mOriginWidth 和 mOriginHeight 爲 view 的初始大小,單位是px

  • 7.mAlpha 爲 view 的透明度,默認爲 1 且小於等於1。

  • 8.剩下的參數就不用解釋了,代碼裏面都有註釋。

(2).WE是如何刷新元素的

-----代碼塊2----- ws_element.dart
    
  add() {
    mElementShowingWidget = initWidget();
  }

  Widget initWidget();

  Widget buildTransform() {
    Matrix4 matrix4 = Matrix4.translationValues(mMoveX, mMoveY, 0);
    matrix4.rotateZ(mRotate);
    matrix4.scale(mScale, mScale, 1);
    return Transform(
      alignment: Alignment.center,
      transform: matrix4,
      child: Opacity(
        opacity: mAlpha,
        child: mElementShowingWidget,
      ),
    );
  }
複製代碼
  • 1.刷新元素的核心代碼就是代碼塊2:
    • 1.首先在 ECWS 添加一個 WE 的時候,WE 的子類中能夠經過實現 initWidget() 來初始化本身須要的元素內容
    • 2.而後每次數據更新時,咱們會經過 buildTransform() 構建一個 Widget 給外部使用。
    • 3.而 buildTransfrom 內部則是經過 Matrix4 和 Transform 來實現移動旋轉縮放,經過 Opacity 來進行 Alpha 變換。

(3).ECWS如何構建整個容器

-----代碼塊2----- element_container_widget.dart

@override
  Widget build(BuildContext context) {
    RawGestureDetector gestureDetectorTwo = GestureDetector(
      child: GestureDetector(
        child: Stack(
            alignment: AlignmentDirectional.center,
            key: globalKey,
            children: mElementList.map((e) {
              return e.buildTransform();
            })
                .toList()
                .reversed
                .toList()
        ),
        onPanUpdate: onMove,
        behavior: HitTestBehavior.opaque,
      ),
    ).build(context);
    gestureDetectorTwo.gestures[RotateScaleGestureRecognizer] =
        GestureRecognizerFactoryWithHandlers<RotateScaleGestureRecognizer>(
              () => RotateScaleGestureRecognizer(debugOwner: this),
              (RotateScaleGestureRecognizer instance) {
            instance
              ..onStart = onDoubleFingerScaleAndRotateStart
              ..onUpdate = onDoubleFingerScaleAndRotateProcess
              ..onEnd = onDoubleFingerScaleAndRotateEnd;
          },
        );
    return Listener(
      child: ConstrainedBox(
        constraints: BoxConstraints(
          minHeight: double.infinity,
          minWidth: double.infinity,
        ),
        child: gestureDetectorTwo,
      ),
      behavior: HitTestBehavior.opaque,
      onPointerDown: onDown,
      onPointerUp: onUp,
    );
  }

複製代碼
  • 1.咱們都知道 State 中須要在 build() 中返回一個 Widget 給 StatefulWidget。
  • 2.爲了裝下多個有層疊關係的元素,咱們使用 Stack 做爲元素的容器。
  • 3.Stack 外面包裝了 GestureDetector 來處理 move 事件。
  • 4.GestureDetector 外部包裝了我自定義的 RotateScaleGestureRecognizer 來處理雙指旋轉縮放事件。
  • 5.最外層則是用 Listener 來監聽手指 down 和 up 事件。
  • 6.上面這樣的設計的緣由我會在後面深刻 Flutter 的時候講解。

3.源碼流程解析

這一節我主要會對項目中的測試 demo 進行源碼流程分析,讓讀者對控件總體的運行方式有個簡單的瞭解。這一節主要是講解源碼,因此讀者必定要去 clone 源碼,跟隨文章的腳步前進。

(1).添加元素

  • 1.簡單的初始化動做我就不贅述了,咱們從 main.dart 的 add 按鈕開始。點擊後先會建立一個 StickerElement 這個是我測試用的元素,裏面代碼很簡單也不說了。
  • 2.addSelectAndUpdateElement 是一個組合方法,裏面調用了 addElementselectElementupdate,也就是添加元素,選中元素,更新元素。咱們一個個來分析::
    • 1.addElement:這個方法裏主要作了下面這些事情:
      • 1.進行數據檢查,若是被添加的 WE 爲空或者該 WE 已經在 ECWS 中,那麼添加失敗。
      • 2.在 ECWS 中我維持了一個 WE 的 List,全部的 WE 都存於其中,每次 add 的時候 WE 都會被添加到 list 的最前面 ,其餘 WE 的 mZIndex 也會順勢更新。
      • 3.調用 WE.add 方法,裏面使用 initWidget 初始化了 mElementShowingView,前面咱們說過了 initWidget 的邏輯由子類定義。
      • 4.調用監聽器的對應方法,且調用自動取消選中的方法(ECWS 能夠被外部決定是否自動取消選中)。
    • 2.selectElement:WE 被 add 了以後,咱們這裏直接將其選中,代碼裏面主要作了下面這些事情:
      • 1.進行數據檢查,若是須要選中的 WE 沒有被添加到 ECWS 中則選中失敗。
      • 2.將須要選中的 WE 從 list 中移除而後添加到 list 的頂部,而後順便更新其餘 WE 的 mZIndex。
      • 3.調用 WE 的 select 方法,裏面主要就是更新要選中的 WE 的數據。
      • 4.調用監聽器對應的方法。
    • 3.update:前面都作好了,就須要將 WE 調整到其應該的狀態,這裏我想你們都猜到了就是調用 setState 而後其會觸發咱們在第二節中說的 build 方法,而後調用每一個 WE 的 buildTransform 返回數據被更新後的 Widget。

(2).元素單指手勢

元素手勢不像添加元素那樣須要外部調用,元素手勢是經過事件分發觸發的,咱們這裏不講 Flutter 的事件分發機制,只講咱們基於其上的邏輯。

  • 1.對於元素單指手勢的處理,主要看三個觸摸事件:down、move、up。因此咱們直接看 ECWS.build 中設置的三個回調方法。
    • 1.onDown 裏面的邏輯以下:
      • 1.經過 findElementByPosition 根據 down 的位置找到當前位置下最頂層的 WE。
      • 2.若是當前有選中的 WE 且與當前觸摸 WE 是同一個的話,那麼先調用 downSelectTapOtherAction,這個函數能夠被子類覆寫,默認返回 false。也就是說子類能夠優先處理當前事件,若是子類處理了這個事件,那麼 return。若是子類不處理,那麼將 mMode 標記爲 SELECTED_CLICK_OR_MOVE,表示最終的手勢多是點擊元素,也多是移動元素。具體的行爲須要 move 或者 up 的時候才能斷定。
      • 3.若是當前有選中的 WE 但與當前觸摸的 WE 不是同一個的時候也分兩種狀況:一種狀況是觸摸的 WE 不存在,此時表示將 mMode 標記爲 SINGLE_TAP_BLANK_SCREEN 表示點擊了 ECWS 的空白區域。另外一種狀況是觸摸的 WE 存在,此時表示從新選中了一個 WE。
      • 4.若是當前沒有選中的 WE,也會有兩種狀況:一個是觸摸的 WE 也不存在,那麼和前面同樣表示點擊空白區域。不然的話就是選中一個 WE。
    • 2.onMove 中會優先將 move 事件交給 scrollSelectTapOtherAction,該方法也能夠被子類覆寫,一樣默認返回 false,若是子類處理了這個事件,那麼就直接 return 了。不然當 mModeSELECTED_CLICK_OR_MOVE(已經選中了 WE 開始移動)、SELECT(沒有選中 WE 開始移動)、MOVE(WE 移動過程當中) 三種狀況中的一種的時候,均可以觸發移動手勢。具體的邏輯在 singleFingerMove 中:
      • 1.先根據 mMode 的狀態,調用 singleFingerMoveStartsingleFingerMoveProcess。singleFingerMoveStart 中調用了監聽器和 WE 的對應方法,裏面基本沒什麼邏輯。 singleFingerMoveProcess 中也調用了監聽和 WE 的對應方法,可是 WE 的對應方法中更新了 mMoveX 和 mMoveY 的數據。
      • 2.調用 update 更新 WE 中的 view。將 mMode 設置爲 MOVE,表示處於移動中。
    • 3.onUp 方法:
      • 1.mModeSELECTED_CLICK_OR_MOVE,到這裏的時候才能確認,用戶的行爲是選中了元素以後的點擊,咱們在前面分析過了這裏面的事件分發的機制,這裏也不贅述了。
      • 2.mModeSINGLE_TAP_BLANK_SCREEN,表示點擊 ECWS 的空白處,這裏調用的 onClickBlank 也是能夠被子類覆寫的,能夠實現一些本身的邏輯。
      • 3.mModeMOVE,結束調用單指移動結束。

3、Flutter探究

這一章我會從一個 Android 工程師的角度來研究一下 Flutter,講一講我在移植控件時碰見的問題們。

1.Flutter與Android對比

先看看 Flutter 與 Android 寫的 App 實際的比較吧

圖2:對比

  • 1.我在將代碼從 Android 移植到 Flutter 上花費了大概 10 個小時。整個控件在 Android 上開始設計到開發完成則是花費了 100 多個小時。因此整個庫的移植成本並不算過高。
  • 2.看上面 gif 的比較,能夠發現流暢度上面並無區別。我找了幾個朋友實際體驗了一下,你們都一樣沒有發現使用起來有差別。
  • 3.圖三、圖4分別是 Flutter 和 Android 的性能圖。咱們發現的確像不少測評文章裏面說到的。Flutter 的內存消耗要比 Native 多。在實驗比較的時候我添加了幾十個元素。最後兩端都穩定在了一個內存數值上面。Flutter 是 256MB 左右,Android 是 128MB 左右。

圖3:flutter profile

圖4:android profile

  • 4.在移植代碼的過程當中,我總結了下面這些寫 Java 和 Dart 之間的區別:
    • 1.Dart 有很是多的語法糖,代碼比起 java 來講有比較多的精簡。
    • 2.Dart 的傳參方式使得寫 Flutter 控件的時候更像是在寫屬性配置表。

2.Flutter原理

以一個 Android 工程師的眼光來看 Flutter

(1).Flutter的事件簡單總結

  • 1.LIstener 是手勢的基礎:GestureDetector 是基於 Listener 開發的。

  • 2.事件自底向上,事件不可截斷

    • 1.先定義一下:自底向上表示從子 view 到父 view。自頂向下表示從父 view 到子 view。
    • 2.作過 Android 的同窗知道 Android 中的事件**是一個自頂向下再自底向上的過程。**在中間的任意一環咱們均可以進行攔截,從而讓事件再也不繼續傳遞。
    • 3.Flutter 的事件模型則是:自底向上,並且目前來看沒有任何操做能阻斷這個流程。
    • 4.也就是說,若是咱們使用 Listener 對任意一個 Widget 進行監聽,那麼咱們在事件傳遞的過程當中阻止 Listener 獲取事件。
    • 5.事件不可截斷的特性在開發中最有用的地方就是:若是咱們使用 tapUp,tapDown,這類手勢想要監聽手指的擡起和放下,那麼這些手勢可能會被其餘手勢給沖掉。此時咱們就能使用 Listener 來經過監聽具體的 down 和 up 事件,由於這個是不可截斷的。
  • 3.開發中咱們使用 GestureDetector 封裝 Widget,咱們定義的一個個手勢回調會讓 GestureDetector 生成多個 GestureRecognizer 附着在當前的 Widget 上以處理 Widget 接收到的事件。

  • 4.每根手指的 down、move、up 都是一個事件流,當 down 事件自底向上確立了一個 Widget 鏈的時候,附着在鏈中各個 Widget 上的 GestureRecognizer 們就會去競爭這個事件流的歸屬。

  • 5.一個事件流的勝出 GestureRecognizer 只有一個,勝出後整個事件流都屬於這個 GestureRecognizer 。

  • 6.GestureRecognizer 的勝出機制,就是 Flutter 在事件不可截斷這個 feature 上的補充的靈活性,可使得某個 Widget 上的手勢被截斷,推薦優先使用 Gesture

  • 7.Gesture 的勝出機制是怎麼樣的?

    • 1.若是一次競爭中只有一個 GestureRecognizer,那麼他就直接勝出。
    • 2.若是一次競爭中有多個相同的 GestureRecognizer,那麼越底層的越勝出。
    • 3.若是一次競爭中有不一樣的 GestureRecognizer:
      • 1.GestureRecognizer 中定義了一個超時機制,有些 GestureRecognizer 定義了某個事件進行了一個時間閾值後若是沒有其餘 GestureRecognizer 申請延長閾值那麼本 GestureRecognizer 就直接勝出。例如:TapGestureRecognizer 定義了 down 事件進行了 100 ms 以後,若是沒有其餘 GestureRecognizer 延長閾值,那麼本身就得到事件流。
      • 2.而 LongPressGestureRecognizer 定義的時間閾值是 500ms,若是 500ms 後沒有其餘 GestureRecognizer 申請延長閾值則本身得到事件流。
      • 3.那麼 TapGestureRecognizer 和 LongPressGestureRecognizer 都在的時候,經過 down 事件的長短來判斷誰勝出。

(2).Flutter的繪製邏輯

Flutter 核心原理

4、尾巴

啊!感受這篇文章有點有始無終的感受,文章從開始到結束跨了好幾周。中間又是加班又是搬家,把個人熱血都消磨了。原本多加一些 Flutter 的深刻探究的,可是感受會越寫越久,因此先就這樣。接下來我會寫一系列文章來分析 Flutter 的原理和 Flutter Sdk。因此更多內容敬請期待!ps:一氣呵成,再而竭,三而衰。真是完美的表現了我寫這篇文章的過程,但願讀者們不要學我。

連載文章

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、互聯網、程序員、計算機編程。下面是個人微信公衆號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事
相關文章
相關標籤/搜索