本文發(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é)是差不多的。
使用方式: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。
我們先來(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ò)展控件。
- 1.我們需要選擇一個(gè) StatefulWidget 作為基本的容器。所以圖中的 ElementContainerWidgetState 就是一個(gè)構(gòu)造這樣的容器的 State,簡(jiǎn)單概括一下它有這些功能:
- 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è)。
- 1.先講橫著的箭頭:外部/內(nèi)部調(diào)用,外部需要調(diào)用 ECWS 來(lái)進(jìn)行對(duì) WE 的增刪改查等操作時(shí)會(huì)進(jìn)入這個(gè)路徑,這個(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。
- 1.addElement:這個(gè)方法里主要做了下面這些事情:
(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) mMode 為 SELECTED_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)用 singleFingerMoveStart 或 singleFingerMoveProcess。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.mMode 為 SELECTED_CLICK_OR_MOVE,到這里的時(shí)候才能確認(rèn),用戶的行為是選中了元素之后的點(diǎn)擊,我們?cè)谇懊娣治鲞^(guò)了這里面的事件分發(fā)的機(jī)制,這里也不贅述了。
- 2.mMode 為 SINGLE_TAP_BLANK_SCREEN,表示點(diǎn)擊 ECWS 的空白處,這里調(diào)用的 onClickBlank 也是可以被子類覆寫的,可以實(shí)現(xiàn)一些自己的邏輯。
- 3.mMode 為 MOVE,結(jié)束調(diào)用單指移動(dòng)結(jié)束。
- 1.onDown 里面的邏輯如下:
三、Flutter探究
這一章我會(huì)從一個(gè) Android 工程師的角度來(lái)研究一下 Flutter,講一講我在移植控件時(shí)遇見的問(wèn)題們。
1.Flutter與Android對(duì)比
先看看 Flutter 與 Android 寫的 App 實(shí)際的比較吧
- 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 左右。
- 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的繪制邏輯
四、尾巴
啊!感覺這篇文章有點(diǎn)虎頭蛇尾的感覺,文章從開始到結(jié)束跨了好幾周。中間又是加班又是搬家,把我的熱血都消磨了。本來(lái)多加一些 Flutter 的深入探究的,但是感覺會(huì)越寫越久,所以先就這樣。接下來(lái)我會(huì)寫一系列文章來(lái)分析 Flutter 的原理和 Flutter Sdk。所以更多內(nèi)容敬請(qǐng)期待!ps:一鼓作氣,再而竭,三而衰。真是完美的表現(xiàn)了我寫這篇文章的過(guò)程,希望讀者們不要學(xué)我。
連載文章
- 1.從零開始仿寫一個(gè)抖音app——開始
- 4.從零開始仿寫一個(gè)抖音App——日志和埋點(diǎn)以及后端初步架構(gòu)
- 5.從零開始仿寫一個(gè)抖音App——app架構(gòu)更新與網(wǎng)絡(luò)層定制
- 6.從零開始仿寫一個(gè)抖音App——音視頻開篇
- 7.從零開始仿寫一個(gè)抖音App——基于FFmpeg的極簡(jiǎn)視頻播放器
- 8.從零開始仿寫一個(gè)抖音App——跨平臺(tái)視頻編輯SDK項(xiàng)目搭建
- 9.從零開始仿寫一個(gè)抖音App——Android繪制機(jī)制以及Surface家族源碼全解析
不販賣焦慮,也不標(biāo)題黨。分享一些這個(gè)世界上有意思的事情。題材包括且不限于:科幻、科學(xué)、科技、互聯(lián)網(wǎng)、程序員、計(jì)算機(jī)編程。下面是我的微信公眾號(hào):世界上有意思的事,干貨多多等你來(lái)看。