flutter用戶交互事件處理

在移動端所謂的用戶交互事件既是用戶的手勢操作處理。
手勢操作在flutter中可分為兩類:

  • 第一類是原始的指針事件(Pointer Event),即原生開發(fā)中常見的觸摸事件,表示屏幕上觸摸(或鼠標、手寫筆)行為觸發(fā)所的位移行為。
  • 第二類則是手勢識別(Gesture Detector),表示多個原生指針事件的組合操作,如點擊、雙擊、長按、等,是指針事件的語義化封裝。

指針事件

在移動端,各個平臺或UI系統(tǒng)的原始指針事件模型基本都是一致,即:一次完整的事件分為三個階段:手指按下(PointerDownEvent)、手指移動(PointerMoveEvent)、和手指抬起(PointerUpEvent),還有觸摸取消事件(PointerCancelEvent)。

從官方文檔中我們可以了解到:指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測試發(fā)現(xiàn)的最內(nèi)部的組件,然后從那里開始,事件會在組件樹中向上冒泡,這些事件會從最內(nèi)部的組件被分發(fā)到組件樹根的路徑上的所有組件。

也就是說flutter的指針事件機制也是基于冒泡事件進行處理的,但是這種冒泡卻無法自主停止。只能通過命中測試(Hit Test)去調(diào)整組件在生命中測試期內(nèi)該如何表現(xiàn),比如把觸摸事件交給子組件,或者交給其他視圖層級之下的組件去響應。

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

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(()=> print("down $event"),//手勢按下回調(diào)
  onPointerMove: (PointerMoveEvent event) => setState(()=>print("down $event"),//手勢移動回調(diào)
  onPointerUp: (PointerUpEvent event) => setState(()=>print("down $event"),//手勢抬起回調(diào)
),

通過拖拽綠色區(qū)塊可以看到控制臺打印如下:

I/flutter (13829): up PointerUpEvent(Offset(97.7, 287.7))
I/flutter (13829): down PointerDownEvent(Offset(110.7, 317.7))
I/flutter (13829): move PointerMoveEvent(Offset(112.9, 317.7))
I/flutter (13829): move PointerMoveEvent(Offset(114.2, 317.7))
I/flutter (13829): up PointerUpEvent(Offset(117.1, 315.7))

上面說到的命中測試是通過PointerEvent中的 behavior 屬性控制的。
改屬性的值類型為HitTestBehavior,這是一個枚舉類,有三個枚舉值:

  • deferToChild:子組件會一個接一個的進行命中測試,如果子組件中有測試通過的,則當前組件通過,這就意味著,如果指針事件作用于子組件上時,其父級組件也肯定可以收到該事件。
  • opaque:在命中測試時,將當前組件當成不透明處理(即使本身是透明的),最終的效果相當于當前Widget的整個區(qū)域都是點擊區(qū)域。舉個例子:
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")
),

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

  • translucent:當點擊組件透明區(qū)域時,可以對自身邊界內(nèi)及底部可視區(qū)域都進行命中測試,這意味著點擊頂部組件透明區(qū)域時,頂部組件和底部組件都可以接收到事件,例如:
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范圍內(nèi)非文本區(qū)域點擊")),
      ),
      onPointerDown: (event) => print("down1"),
      //behavior: HitTestBehavior.translucent, //放開此行注釋后可以"點透"
    )
  ],
)

上例中,當注釋掉最后一行代碼后,在左上角200*100范圍內(nèi)非文本區(qū)域點擊時(頂部組件透明區(qū)域),控制臺只會打印“down0”,也就是說頂部組件沒有接收到事件,而只有底部接收到了。當放開注釋后,再點擊時頂部和底部都會接收到事件,此時會打?。?br> I/flutter ( 3039): down1
I/flutter ( 3039): down0
如果behavior值改為HitTestBehavior.opaque,則只會打印"down1"。

手勢識別

flutter 是通過GestureDetector和GestureRecognizer處理手勢。

GestureDetector 常用的方法:
  • onTap:點擊
  • onDoubleTap:雙擊
  • onLongPress:長按
  • onPanUpdate:拖拽
  • onScaleUpdate:縮放
  • 等等
    flutter支持手勢監(jiān)聽多個事件,也就是說可以同時監(jiān)聽多個事件。但是由于有手勢競技場(下面解釋)出現(xiàn),因此每次只有一個事件能競技成功也就是能得到相應。

如下例子所示:假設有一個widget,它可以左右拖動,現(xiàn)在我們也想檢測在它上面手指按下和抬起的事件,代碼如下:

class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
  double _left = 0.0;
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          left: _left,
          child: GestureDetector(
              child: CircleAvatar(child: Text("B")), //要拖動和點擊的widget
              onHorizontalDragUpdate: (DragUpdateDetails details) {
                setState(() {
                  _left += details.delta.dx;
                });
              },
              onHorizontalDragEnd: (details){
                print("onHorizontalDragEnd");
              },
              onTapDown: (details){
                print("down");
              },
              onTapUp: (details){
                print("up");
              },
          ),
        )
      ],
    );
  }
}

現(xiàn)在我們按住圓形“B”拖動然后抬起手指,控制臺日志如下:

I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd

具體原因:剛開始按下手指時在沒有移動時,拖動手勢還沒有完整的語義,此時TapDown手勢勝出(win),此時打印"down",而拖動時,拖動手勢會勝出,當手指抬起時,onHorizontalDragEnd 和 onTapUp發(fā)生了沖突,但是因為是在拖動的語義中,所以onHorizontalDragEnd勝出,所以就會打印 “onHorizontalDragEnd”。
當然如果只是單純想要得到按下,抬起,以及拖拽觸發(fā),按下和抬起這種單一事件是可以通過結(jié)合Listener指針事件進行處理。

Positioned(
  top:80.0,
  left: _leftB,
  child: Listener(
    onPointerDown: (details) {
      print("down");
    },
    onPointerUp: (details) {
      //會觸發(fā)
      print("up");
    },
    child: GestureDetector(
      child: CircleAvatar(child: Text("C")),
      onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
          _leftB += details.delta.dx;
        });
      },
      onHorizontalDragEnd: (details) {
        print("onHorizontalDragEnd");
      },
    ),
  ),
)

如上寫法:可以監(jiān)聽到用戶點擊,拖拽,以及抬起事件。

手勢競技場

其實現(xiàn)方式是通過GestureDetector內(nèi)部對每一個手勢都建立了一個工廠類(Gesture Factory)。而工廠類內(nèi)部都會使用手勢識別類(GestureRecognizer),來確定當前處理的手勢。

而所有手勢的工廠類都會交給RawGestureDetector類,以完成檢測手勢大量工作:使用Listener監(jiān)聽原始指針事件,并在狀態(tài)改變時把信息同步給所有的手勢識別器。然后這些手勢會在競技場決定最后又誰來響應用戶事件。

但現(xiàn)實場景中也存在當點擊時需要父類事件和子類事件同時執(zhí)行響應的操作。如下代碼:

GestureDetector(
   onTap: ()=> print('parent tap');
   child:Container(
    color:Colors.pinkAccent,
    child: GestureDetector(
    onTap: ()=> print('child tap');
    child:Container(
      color:Colors.red,
      width:200.0,
      height:200.0,
      ),
    )               
  ),
);

運行后點擊藍色區(qū)域,可以發(fā)現(xiàn)只打印了 Child tap;
因此為了能讓父類也能接收到手勢,我們需要同時使用RawGestureGetector和GestureFactory,來改變競技場決定誰來響應事件的結(jié)果。

① 我們需要自定義一個手勢識別器,讓改手勢識別器在競技失敗時也能夠把自己重新添加回來,以便接下來能繼續(xù)去響應用戶事件。

如下定義了一個繼承點擊手勢識別器TapGestureRecognizer的類并重寫了器rejectGesture方法,手動的把自己復活:

class MultipleTapGesture extends TapGestureRecognizer{
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer);
  }
}

② 需要將手勢識別器和其工廠類傳遞給RawGestureDetector,以便用戶產(chǎn)生手勢交互事件時能夠立刻找到對應的識別方法。RawGestureDetector的初始化函數(shù)所做的配置工作,就是定義手勢識別器和其工廠類的映射關(guān)系。

如下所示:點擊藍色區(qū)域時,其父類容器也收到了Tap事件

RawGestureDetector(// 自己構(gòu)造父 Widget 的手勢識別映射關(guān)系
  gestures: {
    // 建立多手勢識別器與手勢識別工廠類的映射關(guān)系,從而返回可以響應該手勢的 recognizer
    MultipleTapGesture: GestureRecognizerFactoryWithHandlers<
        MultipleTapGesture>(
          () => MultipleTapGesture(),
          (MultipleTapGesture instance) {
        instance.onTap = () => print('parent tapped ');// 點擊回調(diào)
      },
    )
  },
  child: Container(
    color: Colors.pinkAccent,
    child: Center(
      child: GestureDetector(// 子視圖可以繼續(xù)使用 GestureDetector
        onTap: () => print('Child tapped'),
        child: Container(...),
      ),
    ),
  ),
);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 在 Flutter 中手勢操作分為兩類: 原始的指針事件(Pointer Event),即原生開發(fā)中常見的觸...
    Eren丶耶格爾閱讀 2,629評論 6 3
  • 在iOS開發(fā)中經(jīng)常會涉及到觸摸事件。本想自己總結(jié)一下,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位,特此轉(zhuǎn)載。作者:L...
    WQ_UESTC閱讀 6,251評論 4 26
  • 本文主要講解iOS觸摸事件的一系列機制,涉及的問題大致包括: 觸摸事件由觸屏生成后如何傳遞到當前應用? 應用接收觸...
    baihualinxin閱讀 1,285評論 0 9
  • 作者: Mike Bluestein | 譯:孫印鳳 原文地址: [https://www.smashingmag...
    格老子閱讀 3,560評論 0 6
  • 我絕不會停下書寫的手。歲月更迭,悲歡交織,命運跌打,令我早已深深懂得了什么是生命中最珍貴的東西。 有時候,只要一兩...
    矮馬閱讀 575評論 3 12

友情鏈接更多精彩內(nèi)容