Flutter最近比較熱門,可是Flutter成體系的文章並很少,前期避免不了踩坑;我這篇文章主要介紹如何使用Flutter實現一個比較複雜的手勢交互,順便分享一下我在使用Flutter過程當中遇到的一些小坑,減小你們入坑;android
做者:HitenDev 連接:www.jianshu.com/p/4d1e81ab3…ios
本項目支持ios&android運行,效果以下git
對了,順便分享一下生成gif的小竅門,建議用手機自帶錄屏功能導出mp4文件到電腦,而後電腦端用ffmpeg命令行處理,控制gif的質量和文件大小,個人建議是分辨率控制在270p,幀率在10左右;github
看文章的小夥伴最好能手持即刻App,親自體驗一下探索頁的交互,是黃色Logo黃色主題色的即刻;有人稱‘黃即’;面試
即刻App原版功能有卡片旋轉,卡片撤回和卡片自動移除,時間關係暫時沒有實現,但核心的功能都在;算法
從一個Android開發者的習慣來看待,這個交互可拆份內外兩層控件,外層咱們須要一個總體下拉的控件,我稱爲下拉控件;內層咱們須要實現一個上、下、左、右四方向拖拽移動的控件,咱們稱爲卡片控件;下拉控件和卡片控件不只要處理手勢,還須要處理子Widget的佈局;下面我再分析細節功能:微信
下拉控件:閉包
卡片控件app
套用App開發伎倆,實現上面的交互無非就是控件佈局和手勢識別。固然Flutter開發也是這些套路,只不過萬物皆是Widget,在Flutter中經常使用的基本佈局有Column、Row、Stack等,手勢識別有Listener、GestureDetector、RawGestureDetector等,這是本文重點講解的控件,不限於上面這幾個Widget,由於Flutter提供的Widget太多了,重點的控件須要牢記外,其餘時候真是現用現查;佈局
因此下面咱們從佈局和手勢這兩個大的技術點,來一一擊破功能點;
這裏所謂的佈局,包括Widget的尺寸大小和位置的控制,通常都是父Widget掌管子Widget的命運,Flutter就是一層一層Widget嵌套,不要擔憂,下面從外到內具體案例講解;
首先咱們要實現最外層佈局,效果是:子Widget豎直襬放,且最上面的Widget默認須要擺放在屏幕外;
如上圖所示,紅色區域是屏幕範圍,header是頭部隱藏的菜單佈局,content是卡片佈局的主體;
先說入的坑
豎直佈局我最早想到的是Column,我想要的效果是content高度和父Widget的高度一致,我首先想到是讓Expanded包裹content,結果是content的高度永遠等於Column高度減header高度,形成現象就是content高度不填充,或者是擠壓現象,若是繼續使用Colunm可能就得放棄Expanded,手動給content賦值高度,沒準是個辦法,但我不肯意手動賦值content的高度,太不優雅了,最後果斷棄用Column;
另外一個問題是如何隱藏header,我想到兩種方案:
可是上面這兩種都有坑,第一種方式會影響控件的點擊事件,onTap方法不會被回調;第二種因爲高度在不斷改變,會影響header內部子Widget的佈局,很難作視覺差的控制;
最終方案
最後採用Stack來佈局,經過Stack配合Positioned,實現header佈局在屏幕外,並且能夠作到讓content佈局填充父Widget;
PullDragWidget
首先解釋一下Positioned的基本用法,top、bottom、height控制高度和位置,並且兩兩配合使用,top和bottom能夠理解成marginTop和marginBottom,height顧名思義是直接Widget的高度,若是top配置bottom,意味着高度等於parentHeight-top-bottom,若是top/bottom配合height使用,高度通常是固定的,固然top和bottom是接受負數的;
再分析代碼,首先**_offsetY是下拉距離,是一個改變的量初始值爲0,content須要設置top = _offsetY和bottom = -_offsetY**,改變的是上下位置,高度不會改變;同理,header是採用top和height控制,高度固定,只須要動態改變top便可;
用Flutter寫佈局真的很簡單,我極力推崇使用Stack佈局,由於它比較靈活,沒有太多的限制,用好Stack主要還得用好Positioned,學好它沒錯;
卡片實現的效果就是依次層疊,錯落有致,這個很容易想到Stack來實現,固然有了上面踩坑,用Stack算是很輕鬆了;
重疊的效果使用Stack很簡單,錯落有致的效果實在起來可能性就比較多了,好比可使用Positioned,也能夠包裹Container改變margin或者padding,可是考慮到角度的旋轉,我選擇使用Transform,由於Transform不只能夠玩轉位移,還有角度和縮放等,其內部其實是操做一個矩陣變換;Transform挺好用,可是在Transform多層嵌套的某些特殊狀況下,會存在不響應onTap事件的狀況,我想這應該是Transform的bug,拖拽事件暫時沒有發現問題,這個是否是bug有待確認,暫時不影響使用;
CardStackWidget
_CardWidget
簡單總結一下卡片佈局代碼,CardStackWidget是管理卡片Stack的父控件,負責對每一個卡片進行佈局,_CardWidget是對單獨卡片內部進行佈局,整體來講沒有什麼難點,細節控制邏輯是在對上層**_CardWidget和底層_CardWidget**偏移量的計算;
佈局的內容就講這麼多,總體來講仍是比較簡單,所謂的有些坑也不必定算是坑,只是不適應某些應用場景罷了;
Flutter手勢識別最經常使用的是Listener和GestureDetector這兩個Widget,其中Listener主要針對原始觸摸點進行處理,GestureDetector已經對原始觸摸點加工成了不一樣的手勢;這兩個類的方法介紹以下;
Listener
GestureDetector手勢回調:
Listener和GestureDetector如何抉擇,首先GestureDetector是基於Listener封裝,它解決了大部分手勢衝突,咱們使用GestureDetector就夠用了,可是GestureDetector不是萬能的,必要時候須要自定義RawGestureDetector;
另一個很重要的概念,Flutter手勢事件是一個從內Widget向外Widget的冒泡機制,假設內外Widget同時監聽豎直方向的拖拽事件onVerticalDragUpdate,每每都是內層控件得到事件,外層事件被動取消;這樣的概念和Android父佈局攔截機制就徹底不一樣了;
雖然Flutter沒有外層攔截機制,可是彷佛還有一線但願,那就是IgnorePointer和AbsorbPointerWidget,這倆哥們能夠忽略或者阻止子Widget樹不響應Event;
基本原理介紹完了,接下來分析案例交互,上面說了我把總體佈局拆分紅了下拉控件和卡片控件,分析即刻App的拖拽的行爲:當下拉控件沒有展開下拉菜單時,卡片控件是能夠相應上、左、右三個方向的手勢,下拉控件只相應一個向下方向的手勢;當下拉菜單展開時,卡片不能相應任何手勢,下拉控件能夠相應豎直方向的全部事件;
上圖更加形象解釋兩種狀態下的手勢響應,下拉控件是父Widget,卡片控件是子Widget,因爲子Widget能優先響手勢,因此在初始階段,咱們不能讓子Widget響應向下的手勢;
因爲GestureDetector只封裝水平和豎直方向的手勢,且兩種手勢不能同時使用,咱們從GestureDetector源碼來看,能不能封裝一個監聽不一樣四個方向的手勢,;
GestureDetector
GestureDetector最終返回的是RawGestureDetector,其中gestures是一個map,豎直方向的手勢在VerticalDragGestureRecognizer這個類;
VerticalDragGestureRecognizer
VerticalDragGestureRecognizer繼承DragGestureRecognizer,大部分邏輯都在DragGestureRecognizer中,咱們只關注重寫的方法:
想實現接受三個方向的手勢,自定義DragGestureRecognizer是一個好的思路;我但願接受上、下、左、右四個方向的參數,根據參數不一樣監聽不一樣的手勢行爲,照葫蘆畫瓢自定義一個接受方向的GestureRecognizer:
DirectionGestureRecognizer
可參考原Demo
因爲DragGestureRecognizer的不少方法是私有的,想從新只能copy一份代碼出來,而後重寫主要的方法,根據不一樣入參處理不一樣的手勢邏輯;
注意事項
敲黑板了,在自定義DragGestureRecognizer時:_getDeltaForDetails返回值表示dx和dy的偏移量,在只存在水平或者只存在豎直方向的狀況下,須要將另外一個方向的dx或dy置0;
當前Widget樹有且只存在一個手勢時,手勢判斷的邏輯**_hasSufficientPendingDragDeltaToAccept可能不會被調用,這時候必定要重寫_getDeltaForDetails控制返回dx和dy**;
如何使用
自定義的DirectionGestureRecognizer能夠配置left、right、up、down四個方向的手勢,並且支持不一樣方向的組合;
好比咱們只想監聽豎直向下方向,就建立**DirectionGestureRecognizer(DirectionGestureRecognizer.down)**的手勢識別;
想監聽上、左、右的手勢,建立**DirectionGestureRecognizer(DirectionGestureRecognizer.left | DirectionGestureRecognizer.right | DirectionGestureRecognizer.up)**的手勢識別;
DirectionGestureRecognizer就像一把磨刀石,刀已經磨鋒利,砍材就很輕鬆了,下面進行控件的手勢實現;
PullDragWidget
PullDragWidget是下拉拖拽控件,根Widget是一個RawGestureDetector用來監聽手勢,其中gestures支持向下拖拽和點擊兩個手勢;當下拉控件處於**_opened狀態說header已經拉下來,此時配合IgnorePointer**,禁用子Widget全部的事件監聽,天然內部的卡片就相應不了任何事件;
同下拉控件同樣,卡片控件只須要監聽其他三個方向的手勢,便可完成任務:
CardStackWidget
這是掘金評論提的問題,我解答一下:在GestureDetector中有Pan手勢和Drag手勢,這兩個手勢都能用處拖拽的場景,但不一樣的是Drag手勢僅限於水平和豎直方向的監聽,Pan手勢不約束方向任意方向都能監聽,除此以外觸發條件也不一致,Pan手勢的觸發條件是滑動動屏幕的距離distance大於kTouchSlop*2,Drag手勢的觸發條件是dx或者dy大於kTouchSlop,dx、dy和distance造成勾股定理的三個邊長;假設一樣在監聽豎直滑動這種場景,VerticalDrag老是比Pan先觸發;若是下拉控件用VerticalDrag卡片控件用Pan,下拉控件會優先獲取向上的拖拽,卡片控件就會失去向上拖拽的機會,這就實現不了交互了,退一步即便Pan的觸發條件跟VerticalDrag同樣,因爲Flutter的事件傳遞是從內到外的,這會致使外層下拉控件徹底失去響應機會。以上個人我的理解,若有誤導還請大佬評論指正。
分析Flutter手勢冒泡的特性,父Widget既沒有響應事件的優先權,也沒有監聽單獨方向(left、right 、up 、down)的手勢,只能本身想辦法自定義GestureRecognizer,把本來Vertical和Horizontal兩個方向的手勢識別擴展成left、right 、up 、down四個方向,區分開會產生衝突的手勢;
固然也可能有其餘的方案來實現該交互的手勢識別,條條大路通羅馬,我只是拋磚引玉,你們有好的方案能夠積極留言提出寶貴意見;
知識點
因爲篇幅有限並無介紹完該交互的全部內容,深表遺憾,總結概括一下代碼中用到的知識點:
上面章節主要介紹在當前場景下用Flutter佈局和手勢的實戰技巧,其中更深層次手勢競技和分發的源碼級分析,有機會再作深刻學習和分享;
另外本篇並非按部就班的零基礎入門,對剛接觸的同窗可能感受有點懵,可是沒有關係,建議你clone一份代碼跑起來效果,沒準就能提起本身學習的興趣;
最最後,本篇全部代碼都是開源的,你的點贊是對我最大的鼓勵。
項目地址: github.com/HitenDev/Fl…
在這裏得到的不只僅是技術!
最後若是對技術比較感興趣,歡迎關注個人微信公衆號:終端研發部,id:codeGooger,一塊兒進階技術