# Pointer事件處理
本節先來介紹一下原始指針事件(Pointer Event,在移動設備上通常為觸摸事件),下一節再介紹手勢處理。
在移動端,各個平臺或UI系統的原始指針事件模型基本都是一致,即:一次完整的事件分為三個階段:手指按下、手指移動、和手指抬起,而更高級別的手勢(如點擊、雙擊、拖動等)都是基于這些原始事件的。
當指針按下時,Flutter會對應用程序執行**命中測試(Hit Test)**,以確定指針與屏幕接觸的位置存在哪些widget, 指針按下事件(以及該指針的后續事件)然后被分發到由命中測試發現的最內部的widget,然后從那里開始,事件會在widget樹中向上冒泡,這些事件會從最內部的widget被分發到到widget根的路徑上的所有Widget,這和Web開發中瀏覽器的**事件冒泡**機制相似, 但是Flutter中沒有機制取消或停止冒泡過程,而瀏覽器的冒泡是可以停止的。注意,只有通過命中測試的Widget才能觸發事件。
Flutter中可以使用Listener widget來監聽原始觸摸事件,它也是一個功能性widget。
```
Listener({
Key key,
this.onPointerDown, //手指按下回調
this.onPointerMove, //手指移動回調
this.onPointerUp,//手指抬起回調
this.onPointerCancel,//觸摸事件取消回調
this.behavior = HitTestBehavior.deferToChild, //在命中測試期間如何表現
Widget child
})
```
我們先看一個示例,后面再單獨討論一下`behavior`屬性。
```
...
//定義一個狀態,保存當前指針位置
PointerEvent _event;
...
Listener(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 300.0,
height: 150.0,
child: Text(_event?.toString()??"",style: TextStyle(color: Colors.white)),
),
onPointerDown: (PointerDownEvent event) => setState(()=>_event=event),
onPointerMove: (PointerMoveEvent event) => setState(()=>_event=event),
onPointerUp: (PointerUpEvent event) => setState(()=>_event=event),
),
```

手指在藍色矩形區域內移動即可看到當前指針偏移,當觸發指針事件時,參數PointerDownEvent、PointerMoveEvent、PointerUpEvent都是PointerEvent的一個子類,PointerEvent類中包括當前指針的一些信息,如:
- `position`:它是鼠標相對于當對于全局坐標的偏移。
- `delta`:兩次指針移動事件(PointerMoveEvent)的距離。
- `pressure`:按壓力度,如果手機屏幕支持壓力傳感器(如iPhone的3D Touch),此屬性會更有意義,如果手機不支持,則始終為1。
- `orientation`:指針移動方向,是一個角度值。
上面只是PointerEvent一些常用屬性,除了這些它還有很多屬性,讀者可以查看API文檔。
現在,我們重點來介紹一下behavior屬性,它決定子Widget如何響應命中測試,它的值類型為HitTestBehavior,這是一個枚舉類,有三個枚舉值:
- `deferToChild`:子widget會一個接一個的進行命中測試,如果子Widget中有測試通過的,則當前Widget通過,這就意味著,如果指針事件作用于子Widget上時,其父(祖先)Widget也肯定可以收到該事件。
- `opaque`:在命中測試時,將當前Widget當成不透明處理(即使本身是透明的),最終的效果相當于當前Widget的整個區域都是點擊區域。舉個例子:
```
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(300.0, 150.0)),
child: Center(child: Text("Box A")),
),
//behavior: HitTestBehavior.opaque,
onPointerDown: (event) => print("down A")
),
```
上例中,只有點擊文本內容區域才會觸發點擊事件,如果我們想讓整個300×150的矩形區域都能點擊我們可以將`behavior`設為`HitTestBehavior.opaque`。注意,該屬性并不能用于在Widget樹中攔截(忽略)事件,它只是決定命中測試時的Widget大小。
- `translucent`:當點擊Widget透明區域時,可以對自身邊界內及底部可視區域都進行命中測試,這意味著點擊頂部widget透明區域時,頂部widget和底部widget都可以接收到事件,例如:
```
Stack(
children: <Widget>[
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(300.0, 200.0)),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue)),
),
onPointerDown: (event) => print("down0"),
),
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(200.0, 100.0)),
child: Center(child: Text("左上角200*100范圍內非文本區域點擊")),
),
onPointerDown: (event) => print("down1"),
//behavior: HitTestBehavior.translucent, //放開此行注釋后可以"點透"
)
],
)
```
上例中,當注釋掉最后一行代碼后,在左上角200\*100范圍內非文本區域點擊時(頂部Widget透明區域),控制臺只會打印“down0”,也就是說頂部widget沒有接收到事件,而只有底部接收到了。當放開注釋后,再點擊時頂部和底部都會接收到事件,此時會打印:
```
I/flutter ( 3039): down1
I/flutter ( 3039): down0
```
如果`behavior`值改為`HitTestBehavior.opaque`,則只會打印"down1"。
### 忽略PointerEvent
假如我們不想讓某個子樹響應PointerEvent的話,我們可以使用IgnorePointer和AbsorbPointer,這兩個Widget都能阻止子樹接收指針事件,不同之處在于AbsorbPointer本身會參與命中測試,而IgnorePointer本身不會參與,這就意味著AbsorbPointer本身是可以接收指針事件的(但其子樹不行),而IgnorePointer不可以。一個簡單的例子如下:
```
Listener(
child: AbsorbPointer(
child: Listener(
child: Container(
color: Colors.red,
width: 200.0,
height: 100.0,
),
onPointerDown: (event)=>print("in"),
),
),
onPointerDown: (event)=>print("up"),
)
```
點擊Container時,由于它在`AbsorbPointer`的子樹上,所以不會響應指針事件,所以日志不會輸出"in",但`AbsorbPointer`本身是可以接收指針事件的,所以會輸出"up"。如果將`AbsorbPointer`換成`IgnorePointer`,那么兩個都不會輸出。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示