移植一個(gè)抖音貼紙組件到Flutter

本文發(fā)于簡(jiǎn)書——何時(shí)夕,搬運(yùn)轉(zhuǎn)載請(qǐng)注明出處,否則將追究版權(quán)責(zé)任。交流qq群:859640274

大家好久不見,又有一個(gè)多月沒有發(fā)文章了,所以今天發(fā)一篇來(lái)刷刷存在感。最近 Flutter 非常火,我這一個(gè)月也不斷的找資料來(lái)學(xué)習(xí) Flutter。經(jīng)過(guò)一段時(shí)間的摸索,我發(fā)現(xiàn)現(xiàn)在很多資料都非常”水“。各種 Dart 入門、Flutter 入門、Flutter 資料收集,完全沒有任何有趣的東西。我不想去寫重復(fù)而無(wú)聊的文章,所以本篇文章會(huì)拋轉(zhuǎn)引玉的探討一些在學(xué)習(xí)和開發(fā) Flutter 的過(guò)程中遇見的問(wèn)題和解決方案。

閱讀須知:

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

本文分為以下章節(jié),讀者可按需閱讀:

  • 1.Flutter之問(wèn)——以 QA 的形式來(lái)闡述我對(duì) Flutter 的看法和學(xué)習(xí)經(jīng)驗(yàn)。
  • 2.移植一個(gè)Flutter控件——將仿寫抖音的貼紙控件移植到 Flutter 中。
  • 3.Flutter探究——聊一聊 Flutter 的原理。
  • 4.尾巴

一、Flutter之問(wèn)

天下事有難易乎?為之,則難者亦易已!

Q:Flutter 怎么學(xué)?

A:這是老生常談的問(wèn)題了。隨便打開一個(gè) Flutter 系列文章,都會(huì)為你鋪平接下來(lái)幾周的路。但是幾周之后呢?似乎很少文章會(huì)接著寫下去,畢竟大腦最喜歡簡(jiǎn)單的東西(我也不例外),一件事情的難度與受歡迎程度成反比。所以 Flutter 怎么學(xué)?所謂:取乎其上,得乎其中。我只有一句話:以讓 Flutter 成為你最拿手技能為目標(biāo)去學(xué)。

Q:能給一些 Flutter 的學(xué)習(xí)資料嗎?

A:我列舉一下我學(xué)習(xí) Flutter 過(guò)程中用到的資料:

  • 1.Dart官網(wǎng),啃完官方文檔,Dart 你就入門了。

  • 2.Flutter實(shí)戰(zhàn),這本開源書的例子很多,全部敲一遍 flutter 你就入門了。特別是最后的 Flutter 原理分析可以仔細(xì)看看。

  • 3.Flutter github 倉(cāng)庫(kù),現(xiàn)在網(wǎng)絡(luò)上 Flutter 原理分析的文章真的非常少,所以真想要成為 Flutter 專家,你必須作為開拓者去閱讀 Flutter 在各種層級(jí)下的源碼。

Q:Flutter 會(huì)干掉 Native?

A:Flutter 是 Native 的子集。在手機(jī)被”革命“之前,但凡業(yè)務(wù)比較復(fù)雜的公司,只會(huì)要求 Native 工程師掌握 Flutter。而不會(huì)出現(xiàn)拋棄 Native 只做 Flutter 的工程師,因?yàn)?Flutter 說(shuō)一千道一萬(wàn)只是一個(gè) ui 框架。畢竟它自身的復(fù)雜度很難支撐起比它還復(fù)雜的業(yè)務(wù)。以上只是個(gè)人觀點(diǎn),有分歧可以在評(píng)論區(qū)探討

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

A:下面是我總結(jié)出來(lái)的 Flutter 比 Native 好的地方:

  • 1.ios、android 一把抓,還可能帶上 web、mac、pc。
  • 2.Dart 語(yǔ)言非?,F(xiàn)代,比 java、oc 好上太多。
  • 3.新興框架沒有歷史包袱。
  • 4.熱更技術(shù)非常誘人。
  • 5.入門很簡(jiǎn)單。

二、移植一個(gè)FluTter控件

經(jīng)常讀我的文章的讀者應(yīng)該看過(guò)我上一篇文章:抖音、ins、微信功能大比拼——Story的貼紙文字,這篇文章中詳細(xì)比較了各家 Story 的貼紙文字的功能,然后在 Android 端實(shí)現(xiàn)了一個(gè)貼紙框架。而這一章我就打算將這個(gè)貼紙框架移植到 Flutter,相信最后的還原度會(huì)超過(guò)你的想象。接下來(lái)建議配合源碼閱讀文章。注意這一章的大部分內(nèi)容和上一篇文章中講解 Android 端實(shí)現(xiàn)控件的章節(jié)是差不多的。

github 地址

使用方式:sticker_framework: ^0.0.1

1.架構(gòu)方式

我們第一節(jié)先講講文字貼紙控件的架構(gòu)實(shí)現(xiàn),我會(huì)基于下面的 圖1 和 github 上的代碼進(jìn)行講解。建議大家把代碼 clone 下來(lái),當(dāng)然別忘了給個(gè) star。

flutter文字貼紙架構(gòu).jpg

我們先來(lái)根據(jù)圖1來(lái)講講整個(gè)控件的架構(gòu)

  • 1.我們先從整體來(lái)看:
    • 1.我們需要選擇一個(gè) StatefulWidget 作為基本的容器。所以圖中的 ElementContainerWidgetState 就是一個(gè)構(gòu)造這樣的容器的 State,簡(jiǎn)單概括一下它有這些功能:
      • 1.處理各種手勢(shì)事件,這里的手勢(shì)包括單指和雙指。
      • 2.添加和刪除一些子 Widget。這里的子 Widget 用于繪制各種元素。
      • 3.提供一些 api 讓外部能操控元素。
      • 4.提供一個(gè) listener,讓外部能夠監(jiān)聽內(nèi)部的各種流程。
    • 2.有了繪制容器,我們需要向繪制容器里面添加 Widget。而 Widget 在用戶操作的過(guò)程中需要有各種數(shù)據(jù),所以這里我用了 WE 來(lái)封裝需要展示的 Widget,其內(nèi)部有下面這些東西:
      • 1.各種用戶操作過(guò)程中需要的數(shù)據(jù)例如:scale、rotate、x、y等等。
      • 2.有一些方法能夠通過(guò)數(shù)據(jù)來(lái)更新 Widget。
      • 3.提供一些 api 讓 ECWS 能更新 WE 里面的數(shù)據(jù) 。
    • 3.由 ECWS 和 WE 就能繼續(xù)繼承出各種各樣的擴(kuò)展控件。
  • 2.整體講完了,我們就可以來(lái)仔細(xì)的講講圖中的流程
    • 1.先講橫著的箭頭:外部/內(nèi)部調(diào)用,外部需要調(diào)用 ECWS 來(lái)進(jìn)行對(duì) WE 的增刪改查等操作時(shí)會(huì)進(jìn)入這個(gè)路徑,這個(gè)路徑里可以有下面這些操作:
      • 1.addElement:向 ECWS 中添加一個(gè)元素。
      • 2.deleteElement:從 ECWS 中刪除一個(gè)元素。
      • 3.update:讓 WE 根據(jù)當(dāng)前數(shù)構(gòu)建出一個(gè) Widget。
      • 4.findElementByPosition:找到傳入的坐標(biāo)下的最頂層的 WE。
      • 5.selectElement:選中一個(gè) WE 且將其調(diào)到最頂層。
      • 6.unSelectElement:取消選中一個(gè) WE。
    • 2.再來(lái)講豎著的箭頭:手勢(shì)事件流,這里中間會(huì)經(jīng)歷一些內(nèi)部邏輯我們后面來(lái)講,最終事件流會(huì)觸發(fā)下面的一系列行為:
      • 1.單指移動(dòng)的整個(gè)流程:當(dāng)我們選中了一個(gè) WE 的時(shí)候就可以對(duì)它進(jìn)行移動(dòng)。這里移動(dòng)可以分為開始、進(jìn)行中、結(jié)束。每個(gè)事件都會(huì)調(diào)用 WE 的對(duì)應(yīng)方法以更新其內(nèi)部數(shù)據(jù)。
      • 2.雙指旋轉(zhuǎn)縮放的整個(gè)流程:當(dāng)我們選中了一個(gè) WE 的時(shí)候可以用雙指對(duì)它進(jìn)行縮放和旋轉(zhuǎn)。這里可以分為開始、進(jìn)行中、結(jié)束。這里也會(huì)調(diào)用 WE 的對(duì)應(yīng)方法更新數(shù)據(jù)。
      • 3.選中元素再次點(diǎn)擊:當(dāng)我們選中了一個(gè) WE 的時(shí)候,可以對(duì)其再次點(diǎn)擊。
      • 4.點(diǎn)擊空白區(qū)域:當(dāng)我們沒有點(diǎn)擊任意 WE 的時(shí)候可以進(jìn)行一些操作,例如清除當(dāng)前 WE 的選中狀態(tài)。這個(gè)行為是可以繼承的,可以交由子類來(lái)覆寫。
      • 5.子類事件:我們看上面其實(shí)感覺觸發(fā)的事件比較少。所以在 down、move、up 的時(shí)候會(huì)優(yōu)先調(diào)用三個(gè)方法 downSelectTapOtherAction、scrollSelectTapOtherAction、upSelectTapOtherAction。這三個(gè)方法可以被子類覆寫,如果返回 true 的話表示事件已經(jīng)消耗了,ECWS 就不會(huì)再觸發(fā)其他事件。這樣一來(lái)子類也可以對(duì)手勢(shì)進(jìn)行擴(kuò)展,例如按住某個(gè)地方單指縮放等等。
      • 7.我圖中 ECWS 也實(shí)現(xiàn)了一個(gè)子類 DECWS,這個(gè)類簡(jiǎn)單的加兩個(gè)手勢(shì):
        • 1.單指移動(dòng)縮放:類似抖音的隨拍,按住元素的右下角的時(shí)候可以用拖動(dòng)來(lái)對(duì)元素進(jìn)行縮放和旋轉(zhuǎn)。
        • 2.刪除:類似抖音的隨拍,點(diǎn)擊元素左上角的時(shí)候可以直接刪除元素。
    • 3.圖1中有一個(gè)特性其實(shí)沒有畫出來(lái)因?yàn)楫嫴幌铝耍?strong>那就是:ECWS 在1和2中的幾乎所有行為都能被外部監(jiān)聽,ElementActionListener 就是負(fù)責(zé)監(jiān)聽的接口。ECWS 中存有一個(gè) EAL 的 set 集合所以監(jiān)聽器可以添加多個(gè)。

2.技術(shù)點(diǎn)實(shí)現(xiàn)

我在開發(fā)整個(gè)控件的時(shí)候遇到過(guò)比較多的技術(shù)實(shí)現(xiàn)上的難點(diǎn),所以這一節(jié)就選一些來(lái)講講,讓讀者在看源碼的時(shí)候不會(huì)特別困惑。

(1).定義數(shù)據(jù)結(jié)構(gòu)與繪制坐標(biāo)系

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

int mZIndex = -1; // 圖像的層級(jí)

  double mMoveX = 0.0; // 初始化后相對(duì) ElementContainerWidget 中心的移動(dòng)距離

  double mMoveY = 0.0; // 初始化后相對(duì) ElementContainerWidget 中心的移動(dòng)距離

  double mOriginWidth; // 初始化時(shí)內(nèi)容的寬度

  double mOriginHeight; // 初始化時(shí)內(nèi)容的高度

  Rect mEditRect; // 可繪制的區(qū)域

  double mRotate = 0.0; // 圖像順時(shí)針旋轉(zhuǎn)的角度,以 π 為基準(zhǔn)

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

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

  bool mIsSelected = false; // 是否處于選中狀態(tài)

  bool mIsSingeFingerMove = false; // 是否處于單指移動(dòng)的狀態(tài)

  bool mIsDoubleFingerScaleAndRotate = false; // 是否處于雙指旋轉(zhuǎn)縮放的狀態(tài)

  Widget mElementShowingWidget; // 展示內(nèi)容的 widget

  Offset mOffset; // ElementContainerWidget 相對(duì)屏幕的位移

函數(shù)未動(dòng)數(shù)據(jù)先行,數(shù)據(jù)結(jié)構(gòu)是一個(gè)框架非常核心的東西,定義了一個(gè)好的數(shù)據(jù)結(jié)構(gòu)可以省去很多不必要的代碼。所以這一小節(jié)我們來(lái)根據(jù)代碼塊1定義一下數(shù)據(jù)結(jié)構(gòu)和 Widget 繪制坐標(biāo)系

  • 1.我們將 WE 所在的 ECWS 作為 WE 中 view 的可繪制區(qū)域,代碼塊1中的 mEditRect 就是這個(gè)區(qū)域代表的矩形。所以 mEditRect 一般為[0, 0, ECWS.getWidth, ECWS.getHeight],mEditRect 的單位為px

  • 2.我們定義的坐標(biāo)系原點(diǎn)在 mEditRect 的中心點(diǎn),也就是 ECWS 的中心點(diǎn)。mMoveX、mMoveY 分別表示 view 距離坐標(biāo)系原點(diǎn)的距離。因?yàn)樗鼈儌z默認(rèn)為 0,所以一般 view 被添加到 ECWS 中的時(shí)候默認(rèn)位置就在 ECWS 的中心。這兩個(gè)參數(shù)的單位為px

  • 3.我們的坐標(biāo)系具有 z 軸,mZIndex 就是 z 軸的坐標(biāo),z 軸表示 view 的層疊關(guān)系,mZIndex 為 0 時(shí)表示 view 在 ECWS 的頂層。mZindex 默認(rèn)為 -1,表示 view 沒有被添加到 ECWS 中。mZIndex 是整數(shù)

  • 4.我們定義 mRotate 為正時(shí) view 順時(shí)針轉(zhuǎn)動(dòng),mRotate 的區(qū)間為[-360,360]。

    5.我們定義 view 沒有縮放的時(shí)候 mScale 為 1,mScale 為 2 的時(shí)候表示 view 放大 2 倍,以此類推。

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

  • 7.mAlpha 為 view 的透明度,默認(rèn)為 1 且小于等于1。

  • 8.剩下的參數(shù)就不用解釋了,代碼里面都有注釋。

(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 添加一個(gè) WE 的時(shí)候,WE 的子類中可以通過(guò)實(shí)現(xiàn) initWidget() 來(lái)初始化自己需要的元素內(nèi)容
    • 2.然后每次數(shù)據(jù)更新時(shí),我們會(huì)通過(guò) buildTransform() 構(gòu)建一個(gè) Widget 給外部使用。
    • 3.而 buildTransfrom 內(nèi)部則是通過(guò) Matrix4 和 Transform 來(lái)實(shí)現(xiàn)移動(dòng)旋轉(zhuǎn)縮放,通過(guò) Opacity 來(lái)進(jìn)行 Alpha 變換。

(3).ECWS如何構(gòu)建整個(gè)容器

-----代碼塊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() 中返回一個(gè) Widget 給 StatefulWidget。
  • 2.為了裝下多個(gè)有層疊關(guān)系的元素,我們使用 Stack 作為元素的容器。
  • 3.Stack 外面包裝了 GestureDetector 來(lái)處理 move 事件。
  • 4.GestureDetector 外部包裝了我自定義的 RotateScaleGestureRecognizer 來(lái)處理雙指旋轉(zhuǎn)縮放事件。
  • 5.最外層則是用 Listener 來(lái)監(jiān)聽手指 down 和 up 事件。
  • 6.上面這樣的設(shè)計(jì)的原因我會(huì)在后面深入 Flutter 的時(shí)候講解。

3.源碼流程解析

這一節(jié)我主要會(huì)對(duì)項(xiàng)目中的測(cè)試 demo 進(jìn)行源碼流程分析,讓讀者對(duì)控件整體的運(yùn)行方式有個(gè)簡(jiǎn)單的了解。這一節(jié)主要是講解源碼,所以讀者一定要去 clone 源碼,跟隨文章的腳步前進(jìn)。

(1).添加元素

  • 1.簡(jiǎn)單的初始化動(dòng)作我就不贅述了,我們從 main.dart 的 add 按鈕開始。點(diǎn)擊后先會(huì)創(chuàng)建一個(gè) StickerElement 這個(gè)是我測(cè)試用的元素,里面代碼很簡(jiǎn)單也不說(shuō)了。
  • 2.addSelectAndUpdateElement 是一個(gè)組合方法,里面調(diào)用了 addElement、selectElement、update,也就是添加元素,選中元素,更新元素。我們一個(gè)個(gè)來(lái)分析::
    • 1.addElement:這個(gè)方法里主要做了下面這些事情:
      • 1.進(jìn)行數(shù)據(jù)檢查,如果被添加的 WE 為空或者該 WE 已經(jīng)在 ECWS 中,那么添加失敗。
      • 2.在 ECWS 中我維持了一個(gè) WE 的 List,所有的 WE 都存于其中,每次 add 的時(shí)候 WE 都會(huì)被添加到 list 的最前面 ,其他 WE 的 mZIndex 也會(huì)順勢(shì)更新。
      • 3.調(diào)用 WE.add 方法,里面使用 initWidget 初始化了 mElementShowingView,前面我們說(shuō)過(guò)了 initWidget 的邏輯由子類定義。
      • 4.調(diào)用監(jiān)聽器的對(duì)應(yīng)方法,且調(diào)用自動(dòng)取消選中的方法(ECWS 可以被外部決定是否自動(dòng)取消選中)。
    • 2.selectElement:WE 被 add 了之后,我們這里直接將其選中,代碼里面主要做了下面這些事情:
      • 1.進(jìn)行數(shù)據(jù)檢查,如果需要選中的 WE 沒有被添加到 ECWS 中則選中失敗。
      • 2.將需要選中的 WE 從 list 中移除然后添加到 list 的頂部,然后順便更新其他 WE 的 mZIndex。
      • 3.調(diào)用 WE 的 select 方法,里面主要就是更新要選中的 WE 的數(shù)據(jù)。
      • 4.調(diào)用監(jiān)聽器對(duì)應(yīng)的方法。
    • 3.update:前面都做好了,就需要將 WE 調(diào)整到其應(yīng)該的狀態(tài),這里我想大家都猜到了就是調(diào)用 setState 然后其會(huì)觸發(fā)我們?cè)诘诙?jié)中說(shuō)的 build 方法,然后調(diào)用每個(gè) WE 的 buildTransform 返回?cái)?shù)據(jù)被更新后的 Widget。

(2).元素單指手勢(shì)

元素手勢(shì)不像添加元素那樣需要外部調(diào)用,元素手勢(shì)是通過(guò)事件分發(fā)觸發(fā)的,我們這里不講 Flutter 的事件分發(fā)機(jī)制,只講我們基于其上的邏輯。

  • 1.對(duì)于元素單指手勢(shì)的處理,主要看三個(gè)觸摸事件:down、move、up。所以我們直接看 ECWS.build 中設(shè)置的三個(gè)回調(diào)方法。
    • 1.onDown 里面的邏輯如下:
      • 1.通過(guò) findElementByPosition 根據(jù) down 的位置找到當(dāng)前位置下最頂層的 WE。
      • 2.如果當(dāng)前有選中的 WE 且與當(dāng)前觸摸 WE 是同一個(gè)的話,那么先調(diào)用 downSelectTapOtherAction,這個(gè)函數(shù)可以被子類覆寫,默認(rèn)返回 false。也就是說(shuō)子類可以優(yōu)先處理當(dāng)前事件,如果子類處理了這個(gè)事件,那么 return。如果子類不處理,那么將 mMode 標(biāo)記為 SELECTED_CLICK_OR_MOVE,表示最終的手勢(shì)可能是點(diǎn)擊元素,也可能是移動(dòng)元素。具體的行為需要 move 或者 up 的時(shí)候才能判定。
      • 3.如果當(dāng)前有選中的 WE 但與當(dāng)前觸摸的 WE 不是同一個(gè)的時(shí)候也分兩種情況:一種情況是觸摸的 WE 不存在,此時(shí)表示將 mMode 標(biāo)記為 SINGLE_TAP_BLANK_SCREEN 表示點(diǎn)擊了 ECWS 的空白區(qū)域。另一種情況是觸摸的 WE 存在,此時(shí)表示重新選中了一個(gè) WE。
      • 4.如果當(dāng)前沒有選中的 WE,也會(huì)有兩種情況:一個(gè)是觸摸的 WE 也不存在,那么和前面一樣表示點(diǎn)擊空白區(qū)域。否則的話就是選中一個(gè) WE。
    • 2.onMove 中會(huì)優(yōu)先將 move 事件交給 scrollSelectTapOtherAction,該方法也可以被子類覆寫,同樣默認(rèn)返回 false,如果子類處理了這個(gè)事件,那么就直接 return 了。否則當(dāng) mModeSELECTED_CLICK_OR_MOVE(已經(jīng)選中了 WE 開始移動(dòng))、SELECT(沒有選中 WE 開始移動(dòng))、MOVE(WE 移動(dòng)過(guò)程中) 三種情況中的一種的時(shí)候,都可以觸發(fā)移動(dòng)手勢(shì)。具體的邏輯在 singleFingerMove 中:
      • 1.先根據(jù) mMode 的狀態(tài),調(diào)用 singleFingerMoveStartsingleFingerMoveProcess。singleFingerMoveStart 中調(diào)用了監(jiān)聽器和 WE 的對(duì)應(yīng)方法,里面基本沒什么邏輯。 singleFingerMoveProcess 中也調(diào)用了監(jiān)聽和 WE 的對(duì)應(yīng)方法,但是 WE 的對(duì)應(yīng)方法中更新了 mMoveX 和 mMoveY 的數(shù)據(jù)。
      • 2.調(diào)用 update 更新 WE 中的 view。將 mMode 設(shè)置為 MOVE,表示處于移動(dòng)中。
    • 3.onUp 方法:
      • 1.mModeSELECTED_CLICK_OR_MOVE,到這里的時(shí)候才能確認(rèn),用戶的行為是選中了元素之后的點(diǎn)擊,我們?cè)谇懊娣治鲞^(guò)了這里面的事件分發(fā)的機(jī)制,這里也不贅述了。
      • 2.mModeSINGLE_TAP_BLANK_SCREEN,表示點(diǎn)擊 ECWS 的空白處,這里調(diào)用的 onClickBlank 也是可以被子類覆寫的,可以實(shí)現(xiàn)一些自己的邏輯。
      • 3.mModeMOVE,結(jié)束調(diào)用單指移動(dòng)結(jié)束。

三、Flutter探究

這一章我會(huì)從一個(gè) Android 工程師的角度來(lái)研究一下 Flutter,講一講我在移植控件時(shí)遇見的問(wèn)題們。

1.Flutter與Android對(duì)比

先看看 Flutter 與 Android 寫的 App 實(shí)際的比較吧

圖2:對(duì)比
  • 1.我在將代碼從 Android 移植到 Flutter 上花費(fèi)了大概 10 個(gè)小時(shí)。整個(gè)控件在 Android 上開始設(shè)計(jì)到開發(fā)完成則是花費(fèi)了 100 多個(gè)小時(shí)。所以整個(gè)庫(kù)的移植成本并不算太高。
  • 2.看上面 gif 的比較,可以發(fā)現(xiàn)流暢度上面并沒有區(qū)別。我找了幾個(gè)朋友實(shí)際體驗(yàn)了一下,大家都同樣沒有發(fā)現(xiàn)使用起來(lái)有差異。
  • 3.圖3、圖4分別是 Flutter 和 Android 的性能圖。我們發(fā)現(xiàn)的確像很多測(cè)評(píng)文章里面說(shuō)到的。Flutter 的內(nèi)存消耗要比 Native 多。在實(shí)驗(yàn)比較的時(shí)候我添加了幾十個(gè)元素。最后兩端都穩(wěn)定在了一個(gè)內(nèi)存數(shù)值上面。Flutter 是 256MB 左右,Android 是 128MB 左右。
圖3:flutter profile
圖4:android profile
  • 4.在移植代碼的過(guò)程中,我總結(jié)了下面這些寫 Java 和 Dart 之間的區(qū)別:
    • 1.Dart 有非常多的語(yǔ)法糖,代碼比起 java 來(lái)說(shuō)有比較多的精簡(jiǎn)。
    • 2.Dart 的傳參方式使得寫 Flutter 控件的時(shí)候更像是在寫屬性配置表。

2.Flutter原理

以一個(gè) Android 工程師的眼光來(lái)看 Flutter

(1).Flutter的事件簡(jiǎn)單總結(jié)

  • 1.LIstener 是手勢(shì)的基礎(chǔ):GestureDetector 是基于 Listener 開發(fā)的。

  • 2.事件自底向上,事件不可截?cái)?/p>

    • 1.先定義一下:自底向上表示從子 view 到父 view。自頂向下表示從父 view 到子 view。
    • 2.做過(guò) Android 的同學(xué)知道 Android 中的事件是一個(gè)自頂向下再自底向上的過(guò)程。在中間的任意一環(huán)我們都可以進(jìn)行攔截,從而讓事件不再繼續(xù)傳遞。
    • 3.Flutter 的事件模型則是:自底向上,而且目前來(lái)看沒有任何操作能阻斷這個(gè)流程。
    • 4.也就是說(shuō),如果我們使用 Listener 對(duì)任意一個(gè) Widget 進(jìn)行監(jiān)聽,那么我們?cè)谑录鬟f的過(guò)程中阻止 Listener 獲取事件。
    • 5.事件不可截?cái)嗟奶匦栽陂_發(fā)中最有用的地方就是:如果我們使用 tapUp,tapDown,這類手勢(shì)想要監(jiān)聽手指的抬起和放下,那么這些手勢(shì)可能會(huì)被其他手勢(shì)給沖掉。此時(shí)我們就能使用 Listener 來(lái)通過(guò)監(jiān)聽具體的 down 和 up 事件,因?yàn)檫@個(gè)是不可截?cái)嗟摹?/li>
  • 3.開發(fā)中我們使用 GestureDetector 封裝 Widget,我們定義的一個(gè)個(gè)手勢(shì)回調(diào)會(huì)讓 GestureDetector 生成多個(gè) GestureRecognizer 附著在當(dāng)前的 Widget 上以處理 Widget 接收到的事件。

  • 4.每根手指的 down、move、up 都是一個(gè)事件流,當(dāng) down 事件自底向上確立了一個(gè) Widget 鏈的時(shí)候,附著在鏈中各個(gè) Widget 上的 GestureRecognizer 們就會(huì)去競(jìng)爭(zhēng)這個(gè)事件流的歸屬。

  • 5.一個(gè)事件流的勝出 GestureRecognizer 只有一個(gè),勝出后整個(gè)事件流都屬于這個(gè) GestureRecognizer 。

  • 6.GestureRecognizer 的勝出機(jī)制,就是 Flutter 在事件不可截?cái)噙@個(gè) feature 上的補(bǔ)充的靈活性,可以使得某個(gè) Widget 上的手勢(shì)被截?cái)啵?strong>推薦優(yōu)先使用 Gesture。

  • 7.Gesture 的勝出機(jī)制是怎么樣的?

    • 1.如果一次競(jìng)爭(zhēng)中只有一個(gè) GestureRecognizer,那么他就直接勝出。
    • 2.如果一次競(jìng)爭(zhēng)中有多個(gè)相同的 GestureRecognizer,那么越底層的越勝出。
    • 3.如果一次競(jìng)爭(zhēng)中有不同的 GestureRecognizer:
      • 1.GestureRecognizer 中定義了一個(gè)超時(shí)機(jī)制,有些 GestureRecognizer 定義了某個(gè)事件進(jìn)行了一個(gè)時(shí)間閾值后如果沒有其他 GestureRecognizer 申請(qǐng)延長(zhǎng)閾值那么本 GestureRecognizer 就直接勝出。例如:TapGestureRecognizer 定義了 down 事件進(jìn)行了 100 ms 之后,如果沒有其他 GestureRecognizer 延長(zhǎng)閾值,那么自己就獲得事件流。
      • 2.而 LongPressGestureRecognizer 定義的時(shí)間閾值是 500ms,如果 500ms 后沒有其他 GestureRecognizer 申請(qǐng)延長(zhǎng)閾值則自己獲得事件流。
      • 3.那么 TapGestureRecognizer 和 LongPressGestureRecognizer 都在的時(shí)候,通過(guò) down 事件的長(zhǎng)短來(lái)判斷誰(shuí)勝出。

(2).Flutter的繪制邏輯

Flutter 核心原理

四、尾巴

啊!感覺這篇文章有點(diǎn)虎頭蛇尾的感覺,文章從開始到結(jié)束跨了好幾周。中間又是加班又是搬家,把我的熱血都消磨了。本來(lái)多加一些 Flutter 的深入探究的,但是感覺會(huì)越寫越久,所以先就這樣。接下來(lái)我會(huì)寫一系列文章來(lái)分析 Flutter 的原理和 Flutter Sdk。所以更多內(nèi)容敬請(qǐng)期待!ps:一鼓作氣,再而竭,三而衰。真是完美的表現(xiàn)了我寫這篇文章的過(guò)程,希望讀者們不要學(xué)我。

連載文章

不販賣焦慮,也不標(biāo)題黨。分享一些這個(gè)世界上有意思的事情。題材包括且不限于:科幻、科學(xué)、科技、互聯(lián)網(wǎng)、程序員、計(jì)算機(jī)編程。下面是我的微信公眾號(hào):世界上有意思的事,干貨多多等你來(lái)看。

世界上有意思的事

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容