本文首發於微信公眾號——世界上有意思的事,搬運轉載請註明出處,否則將追究版權責任。交流qq群:859640274
大家好久不見,又有一個多月沒有發文章了,所以今天發一篇來刷刷存在感。最近 Flutter 非常火,我這一個月也不斷的找資料來學習 Flutter。經過一段時間的摸索,我發現現在很多資料都非常”水“。各種 Dart 入門、Flutter 入門、Flutter 資料收集,完全沒有任何有趣的東西。我不想去寫重複而無聊的文章,所以本篇文章會拋轉引玉的探討一些在學習和開發 Flutter 的過程中遇見的問題和解決方案。
閱讀須知:
- 1.WE——>WsElement、ECWS——>ElementContainerWidgetState、EAL——>ElementActionListener
本文分為以下章節,讀者可按需閱讀:
- 1.Flutter之問——以 QA 的形式來闡述我對 Flutter 的看法和學習經驗。
- 2.移植一個Flutter控制元件——將仿寫抖音的貼紙控制元件移植到 Flutter 中。
- 3.Flutter探究——聊一聊 Flutter 的原理。
- 4.尾巴
一、Flutter之問
天下事有難易乎?為之,則難者亦易已!
Q:Flutter 怎麼學?
A:這是老生常談的問題了。隨便開啟一個 Flutter 系列文章,都會為你鋪平接下來幾周的路。但是幾周之後呢?似乎很少文章會接著寫下去,**畢竟大腦最喜歡簡單的東西(我也不例外),一件事情的難度與受歡迎程度成反比。**所以 Flutter 怎麼學?所謂:取乎其上,得乎其中。我只有一句話:以讓 Flutter 成為你最拿手技能為目標去學。
Q:能給一些 Flutter 的學習資料嗎?
A:我列舉一下我學習 Flutter 過程中用到的資料:
-
1.Dart官網,啃完官方文件,Dart 你就入門了。
-
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.入門很簡單。
二、移植一個FluTter控制元件
經常讀我的文章的讀者應該看過我上一篇文章:抖音、ins、微信功能大比拼——Story的貼紙文字,這篇文章中詳細比較了各家 Story 的貼紙文字的功能,然後在 Android 端實現了一個貼紙框架。而這一章我就打算將這個貼紙框架移植到 Flutter,相信最後的還原度會超過你的想象。接下來建議配合原始碼閱讀文章。注意這一章的大部分內容和上一篇文章中講解 Android 端實現控制元件的章節是差不多的。
使用方式:sticker_framework: ^0.0.1
1.架構方式
我們第一節先講講文字貼紙控制元件的架構實現,我會基於下面的 圖1 和 github 上的程式碼進行講解。建議大家把程式碼 clone 下來,當然別忘了給個 star。
我們先來根據圖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 就能繼續繼承出各種各樣的擴充套件控制元件。
- 1.我們需要選擇一個 StatefulWidget 作為基本的容器。所以圖中的 ElementContainerWidgetState 就是一個構造這樣的容器的 State,簡單概括一下它有這些功能:
- 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 集合所以監聽器可以新增多個。
- 1.先講橫著的箭頭:外部/內部呼叫,外部需要呼叫 ECWS 來進行對 WE 的增刪改查等操作時會進入這個路徑,這個路徑裡可以有下面這些操作:
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 是一個組合方法,裡面呼叫了 addElement、selectElement、update,也就是新增元素,選中元素,更新元素。我們一個個來分析::
- 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。
- 1.addElement:這個方法裡主要做了下面這些事情:
(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 了。否則當 mMode 為 SELECTED_CLICK_OR_MOVE(已經選中了 WE 開始移動)、SELECT(沒有選中 WE 開始移動)、MOVE(WE 移動過程中) 三種情況中的一種的時候,都可以觸發移動手勢。具體的邏輯在 singleFingerMove 中:
- 1.先根據 mMode 的狀態,呼叫 singleFingerMoveStart 或 singleFingerMoveProcess。singleFingerMoveStart 中呼叫了監聽器和 WE 的對應方法,裡面基本沒什麼邏輯。 singleFingerMoveProcess 中也呼叫了監聽和 WE 的對應方法,但是 WE 的對應方法中更新了 mMoveX 和 mMoveY 的資料。
- 2.呼叫 update 更新 WE 中的 view。將 mMode 設定為 MOVE,表示處於移動中。
- 3.onUp 方法:
- 1.mMode 為 SELECTED_CLICK_OR_MOVE,到這裡的時候才能確認,使用者的行為是選中了元素之後的點選,我們在前面分析過了這裡面的事件分發的機制,這裡也不贅述了。
- 2.mMode 為 SINGLE_TAP_BLANK_SCREEN,表示點選 ECWS 的空白處,這裡呼叫的 onClickBlank 也是可以被子類覆寫的,可以實現一些自己的邏輯。
- 3.mMode 為 MOVE,結束呼叫單指移動結束。
- 1.onDown 裡面的邏輯如下:
三、Flutter探究
這一章我會從一個 Android 工程師的角度來研究一下 Flutter,講一講我在移植控制元件時遇見的問題們。
1.Flutter與Android對比
先看看 Flutter 與 Android 寫的 App 實際的比較吧
- 1.我在將程式碼從 Android 移植到 Flutter 上花費了大概 10 個小時。整個控制元件在 Android 上開始設計到開發完成則是花費了 100 多個小時。所以整個庫的移植成本並不算太高。
- 2.看上面 gif 的比較,可以發現流暢度上面並沒有區別。我找了幾個朋友實際體驗了一下,大家都同樣沒有發現使用起來有差異。
- 3.圖3、圖4分別是 Flutter 和 Android 的效能圖。我們發現的確像很多測評文章裡面說到的。Flutter 的記憶體消耗要比 Native 多。在實驗比較的時候我新增了幾十個元素。最後兩端都穩定在了一個記憶體數值上面。Flutter 是 256MB 左右,Android 是 128MB 左右。
- 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 的深入探究的,但是感覺會越寫越久,所以先就這樣。接下來我會寫一系列文章來分析 Flutter 的原理和 Flutter Sdk。所以更多內容敬請期待!ps:一鼓作氣,再而竭,三而衰。真是完美的表現了我寫這篇文章的過程,希望讀者們不要學我。
連載文章
- 1.從零開始仿寫一個抖音app——開始
- 4.從零開始仿寫一個抖音App——日誌和埋點以及後端初步架構
- 5.從零開始仿寫一個抖音App——app架構更新與網路層定製
- 6.從零開始仿寫一個抖音App——音視訊開篇
- 7.從零開始仿寫一個抖音App——基於FFmpeg的極簡視訊播放器
- 8.從零開始仿寫一個抖音App——跨平臺視訊編輯SDK專案搭建
- 9.從零開始仿寫一個抖音App——Android繪製機制以及Surface家族原始碼全解析
不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、網際網路、程式設計師、計算機程式設計。下面是我的微信公眾號:世界上有意思的事,乾貨多多等你來看。