Flutter實(shí)戰(zhàn) 原始指針事件處理

2021-03-08 14:01 更新

本節(jié)先來介紹一下原始指針事件(Pointer Event,在移動(dòng)設(shè)備上通常為觸摸事件),下一節(jié)再介紹手勢(shì)處理。

在移動(dòng)端,各個(gè)平臺(tái)或 UI 系統(tǒng)的原始指針事件模型基本都是一致,即:一次完整的事件分為三個(gè)階段:手指按下、手指移動(dòng)、和手指抬起,而更高級(jí)別的手勢(shì)(如點(diǎn)擊、雙擊、拖動(dòng)等)都是基于這些原始事件的。

當(dāng)指針按下時(shí),F(xiàn)lutter 會(huì)對(duì)應(yīng)用程序執(zhí)行命中測(cè)試(Hit Test),以確定指針與屏幕接觸的位置存在哪些組件(widget), 指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測(cè)試發(fā)現(xiàn)的最內(nèi)部的組件,然后從那里開始,事件會(huì)在組件樹中向上冒泡,這些事件會(huì)從最內(nèi)部的組件被分發(fā)到組件樹根的路徑上的所有組件,這和 Web 開發(fā)中瀏覽器的事件冒泡機(jī)制相似, 但是 Flutter 中沒有機(jī)制取消或停止“冒泡”過程,而瀏覽器的冒泡是可以停止的。注意,只有通過命中測(cè)試的組件才能觸發(fā)事件。

Flutter 中可以使用Listener來監(jiān)聽原始觸摸事件,按照本書對(duì)組件的分類,則Listener也是一個(gè)功能性組件。下面是Listener的構(gòu)造函數(shù)定義:

  1. Listener({
  2. Key key,
  3. this.onPointerDown, //手指按下回調(diào)
  4. this.onPointerMove, //手指移動(dòng)回調(diào)
  5. this.onPointerUp,//手指抬起回調(diào)
  6. this.onPointerCancel,//觸摸事件取消回調(diào)
  7. this.behavior = HitTestBehavior.deferToChild, //在命中測(cè)試期間如何表現(xiàn)
  8. Widget child
  9. })

我們先看一個(gè)示例,后面再單獨(dú)討論一下behavior屬性。

  1. ...
  2. //定義一個(gè)狀態(tài),保存當(dāng)前指針位置
  3. PointerEvent _event;
  4. ...
  5. Listener(
  6. child: Container(
  7. alignment: Alignment.center,
  8. color: Colors.blue,
  9. width: 300.0,
  10. height: 150.0,
  11. child: Text(_event?.toString()??"",style: TextStyle(color: Colors.white)),
  12. ),
  13. onPointerDown: (PointerDownEvent event) => setState(()=>_event=event),
  14. onPointerMove: (PointerMoveEvent event) => setState(()=>_event=event),
  15. onPointerUp: (PointerUpEvent event) => setState(()=>_event=event),
  16. ),

運(yùn)行后效果如圖8-1所示:

圖8-1

手指在藍(lán)色矩形區(qū)域內(nèi)移動(dòng)即可看到當(dāng)前指針偏移,當(dāng)觸發(fā)指針事件時(shí),參數(shù)PointerDownEvent、PointerMoveEventPointerUpEvent都是PointerEvent的一個(gè)子類,PointerEvent類中包括當(dāng)前指針的一些信息,如:

  • position:它是鼠標(biāo)相對(duì)于當(dāng)對(duì)于全局坐標(biāo)的偏移。
  • delta:兩次指針移動(dòng)事件(PointerMoveEvent)的距離。
  • pressure:按壓力度,如果手機(jī)屏幕支持壓力傳感器(如iPhone的3D Touch),此屬性會(huì)更有意義,如果手機(jī)不支持,則始終為1。
  • orientation:指針移動(dòng)方向,是一個(gè)角度值。

上面只是PointerEvent一些常用屬性,除了這些它還有很多屬性,讀者可以查看API文檔。

現(xiàn)在,我們重點(diǎn)來介紹一下behavior屬性,它決定子組件如何響應(yīng)命中測(cè)試,它的值類型為HitTestBehavior,這是一個(gè)枚舉類,有三個(gè)枚舉值:

  • deferToChild:子組件會(huì)一個(gè)接一個(gè)的進(jìn)行命中測(cè)試,如果子組件中有測(cè)試通過的,則當(dāng)前組件通過,這就意味著,如果指針事件作用于子組件上時(shí),其父級(jí)組件也肯定可以收到該事件。

  • opaque:在命中測(cè)試時(shí),將當(dāng)前組件當(dāng)成不透明處理(即使本身是透明的),最終的效果相當(dāng)于當(dāng)前Widget的整個(gè)區(qū)域都是點(diǎn)擊區(qū)域。舉個(gè)例子:

  1. Listener(
  2. child: ConstrainedBox(
  3. constraints: BoxConstraints.tight(Size(300.0, 150.0)),
  4. child: Center(child: Text("Box A")),
  5. ),
  6. //behavior: HitTestBehavior.opaque,
  7. onPointerDown: (event) => print("down A")
  8. ),

上例中,只有點(diǎn)擊文本內(nèi)容區(qū)域才會(huì)觸發(fā)點(diǎn)擊事件,因?yàn)?deferToChild 會(huì)去子組件判斷是否命中測(cè)試,而該例中子組件就是 Text("Box A") 。 如果我們想讓整個(gè)300×150的矩形區(qū)域都能點(diǎn)擊我們可以將behavior設(shè)為HitTestBehavior.opaque。注意,該屬性并不能用于在組件樹中攔截(忽略)事件,它只是決定命中測(cè)試時(shí)的組件大小。

  • translucent:當(dāng)點(diǎn)擊組件透明區(qū)域時(shí),可以對(duì)自身邊界內(nèi)及底部可視區(qū)域都進(jìn)行命中測(cè)試,這意味著點(diǎn)擊頂部組件透明區(qū)域時(shí),頂部組件和底部組件都可以接收到事件,例如:

  1. Stack(
  2. children: <Widget>[
  3. Listener(
  4. child: ConstrainedBox(
  5. constraints: BoxConstraints.tight(Size(300.0, 200.0)),
  6. child: DecoratedBox(
  7. decoration: BoxDecoration(color: Colors.blue)),
  8. ),
  9. onPointerDown: (event) => print("down0"),
  10. ),
  11. Listener(
  12. child: ConstrainedBox(
  13. constraints: BoxConstraints.tight(Size(200.0, 100.0)),
  14. child: Center(child: Text("左上角200*100范圍內(nèi)非文本區(qū)域點(diǎn)擊")),
  15. ),
  16. onPointerDown: (event) => print("down1"),
  17. //behavior: HitTestBehavior.translucent, //放開此行注釋后可以"點(diǎn)透"
  18. )
  19. ],
  20. )

上例中,當(dāng)注釋掉最后一行代碼后,在左上角200*100范圍內(nèi)非文本區(qū)域點(diǎn)擊時(shí)(頂部組件透明區(qū)域),控制臺(tái)只會(huì)打印“down0”,也就是說頂部組件沒有接收到事件,而只有底部接收到了。當(dāng)放開注釋后,再點(diǎn)擊時(shí)頂部和底部都會(huì)接收到事件,此時(shí)會(huì)打印:

  1. I/flutter ( 3039): down1
  2. I/flutter ( 3039): down0

如果behavior值改為HitTestBehavior.opaque,則只會(huì)打印"down1"。

#忽略PointerEvent

假如我們不想讓某個(gè)子樹響應(yīng)PointerEvent的話,我們可以使用IgnorePointerAbsorbPointer,這兩個(gè)組件都能阻止子樹接收指針事件,不同之處在于AbsorbPointer本身會(huì)參與命中測(cè)試,而IgnorePointer本身不會(huì)參與,這就意味著AbsorbPointer本身是可以接收指針事件的(但其子樹不行),而IgnorePointer不可以。一個(gè)簡(jiǎn)單的例子如下:

  1. Listener(
  2. child: AbsorbPointer(
  3. child: Listener(
  4. child: Container(
  5. color: Colors.red,
  6. width: 200.0,
  7. height: 100.0,
  8. ),
  9. onPointerDown: (event)=>print("in"),
  10. ),
  11. ),
  12. onPointerDown: (event)=>print("up"),
  13. )

點(diǎn)擊Container時(shí),由于它在AbsorbPointer的子樹上,所以不會(huì)響應(yīng)指針事件,所以日志不會(huì)輸出"in",但AbsorbPointer本身是可以接收指針事件的,所以會(huì)輸出"up"。如果將AbsorbPointer換成IgnorePointer,那么兩個(gè)都不會(huì)輸出。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)