【Flutter學習】事件處理與通知之事件處理


一,概述

  移動應用中一個必不可少的環節就是與用戶的交互,在Flutter中提供的手勢檢測為GestureDetector。 Flutter中的手勢系統分為二層:

  • 第一層是觸摸原事件(指針)
    • PointerDownEvent:用戶與屏幕接觸產生了聯系
    • PointerMoveEvent:手指已從屏幕上的一個位置移動到另一個位置
    • PointMoveEvent:指針停止接觸屏幕
    • PointerUpEvent:用戶已停止接觸屏幕
    • PointerCanceEvent:此指針的輸入不再指向此應用程序
  • 第二層是手勢事件(輕擊,拖動,縮放)
    • 自帶交互的控件監聽
      • RaisedButton、
      • IconButton、
      • OutlineButton、
      • Checkbox、
      • SnackBar、
      • Switch等    
    • 不自帶交互的控件監聽
      • 用GestureDelector進行手勢檢測
      • 用Dismissible實現滑動刪除    

二,手勢事件   

  • 1.自帶交互的控件
    在Flutter中,自帶如點擊事件的控件有RaisedButton、IconButton、OutlineButton、Checkbox、SnackBar、Switch等,如下面給OutlineButton添加點擊事件:

    body:Center(
      child: OutlineButton(
        child: Text('點擊我'),
         onPressed: (){
              Fluttertoast.showToast(
                        msg: '你點擊了FlatButton',
                toastLength: Toast.LENGTH_SHORT,
                    gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
         );
      }),
    ),

    上面代碼就可以捕捉OutlineButton的點擊事件。

  • 2.不自帶交互的控件
    • GestureDetector  
      很多控件不像RaisedButton、OutlineButton等已經對presses(taps)或手勢做出了響應。那么如果要監聽這些控件的手勢就需要用另一個控件GestureDetector,那看看源碼GestureDetector支持哪些手勢:
      GestureDetector({
      Key key,
      this.child,
      this.onTapDown,//按下,每次和屏幕交互都會調用
      this.onTapUp,//抬起,停止觸摸時調用
      this.onTap,//點擊,短暫觸摸屏幕時調用
      this.onTapCancel,//取消 觸發了onTapDown,但沒有完成onTap
      this.onDoubleTap,//雙擊,短時間內觸摸屏幕兩次
      this.onLongPress,//長按,觸摸時間超過500ms觸發
      this.onLongPressUp,//長按松開
      this.onVerticalDragDown,//觸摸點開始和屏幕交互,同時豎直拖動按下
      this.onVerticalDragStart,//觸摸點開始在豎直方向拖動開始
      this.onVerticalDragUpdate,//觸摸點每次位置改變時,豎直拖動更新
      this.onVerticalDragEnd,//豎直拖動結束
      this.onVerticalDragCancel,//豎直拖動取消
      this.onHorizontalDragDown,//觸摸點開始跟屏幕交互,並水平拖動
      this.onHorizontalDragStart,//水平拖動開始,觸摸點開始在水平方向移動
      this.onHorizontalDragUpdate,//水平拖動更新,觸摸點更新
      this.onHorizontalDragEnd,//水平拖動結束觸發
      this.onHorizontalDragCancel,//水平拖動取消 onHorizontalDragDown沒有成功觸發
      //onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存
      this.onPanDown,//觸摸點開始跟屏幕交互時觸發
      this.onPanStart,//觸摸點開始移動時觸發
      this.onPanUpdate,//屏幕上的觸摸點位置每次改變時,都會觸發這個回調
      this.onPanEnd,//pan操作完成時觸發
      this.onPanCancel,//pan操作取消
      //onScale可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存,不能與onPan並存
      this.onScaleStart,//觸摸點開始跟屏幕交互時觸發,同時會建立一個焦點為1.0
      this.onScaleUpdate,//跟屏幕交互時觸發,同時會標示一個新的焦點
      this.onScaleEnd,//觸摸點不再跟屏幕交互,標示這個scale手勢完成
      this.behavior,
      this.excludeFromSemantics = false
      })
      這里注意:onVerticalXXX/onHorizontalXXX和onPanXXX不能同時設置,如果同時需要水平、豎直方向的移動,設置onPanXXX。
      直接上例子:
      • 2.1.onTapXXX

        child: GestureDetector(
           child: Container(
                    width: 300.0,
                   height: 300.0,
                    color:Colors.red,
            ),
          onTapDown: (d){
             print("onTapDown");
           },
          onTapUp: (d){
             print("onTapUp");
           },
          onTap:(){
             print("onTap");
           },
          onTapCancel: (){
              print("onTaoCancel");
           },
        )

        點了一下,並且抬起,結果是:

        I/flutter (16304): onTapDown
        I/flutter (16304): onTapUp
        I/flutter (16304): onTap

        先觸發onTapDown 然后onTapUp 繼續onTap

      • 2.2.onLongXXX

        //手勢測試
        Widget gestureTest = GestureDetector(
         child: Container(
          width: 300.0,
          height: 300.0,
          color:Colors.red,
         ),
         onDoubleTap: (){
           print("雙擊onDoubleTap");
         },
         onLongPress: (){
           print("長按onLongPress");
         },
         onLongPressUp: (){
           print("長按抬起onLongPressUP");
         },
        );

        實際結果:

        I/flutter (16304): 長按onLongPress
        I/flutter (16304): 長按抬起onLongPressUP
        I/flutter (16304): 雙擊onDoubleTap
      • 2.3.onVerticalXXX

        //手勢測試
        Widget gestureTest = GestureDetector(
        child: Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        ),
        onVerticalDragDown: (_){
           print("豎直方向拖動按下onVerticalDragDown:"+_.globalPosition.toString());
        },
        onVerticalDragStart: (_){
           print("豎直方向拖動開始onVerticalDragStart"+_.globalPosition.toString());
        },
        onVerticalDragUpdate: (_){
           print("豎直方向拖動更新onVerticalDragUpdate"+_.globalPosition.toString());
        },
        onVerticalDragCancel: (){
           print("豎直方向拖動取消onVerticalDragCancel");
        },
        onVerticalDragEnd: (_){
           print("豎直方向拖動結束onVerticalDragEnd");
        },
        );

        輸出結果:

        I/flutter (16304): 豎直方向拖動按下onVerticalDragDown:Offset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動開始onVerticalDragStartOffset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 290.0)
        I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 291.3)
        I/flutter (16304): 豎直方向拖動結束onVerticalDragEnd

         

      • 2.4.onPanXXX

        //手勢測試
        Widget gestureTest = GestureDetector(
         child: Container(
          width: 300.0,
          height: 300.0,
          color:Colors.red,
        ),
        onPanDown: (_){
          print("onPanDown");
        },
        onPanStart: (_){
          print("onPanStart");
        },
        onPanUpdate: (_){
          print("onPanUpdate");
        },
        onPanCancel: (){
          print("onPanCancel");
        },
        onPanEnd: (_){
          print("onPanEnd");
        },
        );

        無論豎直拖動還是橫向拖動還是一起來,結果如下:

        I/flutter (16304): onPanDown
        I/flutter (16304): onPanStart
        I/flutter (16304): onPanUpdate
        I/flutter (16304): onPanUpdate
        I/flutter (16304): onPanEnd

         

      • 2.5.onScaleXXX

        //手勢測試
        Widget gestureTest = GestureDetector(
          child: Container(
          width: 300.0,
          height: 300.0,
          color:Colors.red,
          ),
          onScaleStart: (_){
            print("onScaleStart");
          },
          onScaleUpdate: (_){
            print("onScaleUpdate");
          },
          onScaleEnd: (_){
           print("onScaleEnd");
        } );

        無論點擊、豎直拖動、水平拖動,結果如下:

        I/flutter (16304): onScaleStart
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleUpdate
        I/flutter (16304): onScaleEnd
    • 用dismissible實現滑動刪除
      滑動刪除模式在很多移動應用中很常見。例如,我們在整理手機通訊錄時,希望能快速刪除一些聯系人,一般手指輕輕一滑即可以實現刪除功能。Flutter提供了Dismissible組件使這項任務變得簡單。
      • 構造函數
        /**
         *  滑動刪除
         *
         * const Dismissible({
            @required Key key,//
            @required this.child,//
            this.background,//滑動時組件下一層顯示的內容,沒有設置secondaryBackground時,從右往左或者從左往右滑動都顯示該內容,設置了secondaryBackground后,從左往右滑動顯示該內容,從右往左滑動顯示secondaryBackground的內容
            //secondaryBackground不能單獨設置,只能在已經設置了background后才能設置,從右往左滑動時顯示
            this.secondaryBackground,//
            this.onResize,//組件大小改變時回調
            this.onDismissed,//組件消失后回調
            this.direction = DismissDirection.horizontal,//
            this.resizeDuration = const Duration(milliseconds: 300),//組件大小改變的時長
            this.dismissThresholds = const <DismissDirection, double>{},//
            this.movementDuration = const Duration(milliseconds: 200),//組件消失的時長
            this.crossAxisEndOffset = 0.0,//
            })
         */
      • 示例demo

        /***
         * 滑動刪除
         */
        
        class MyListView extends StatelessWidget {
          var list = ['第一個','第二個','第三個','第四個','第五個','第六個'];
            @override
          Widget build(BuildContext context) {
            // TODO: implement build
            return new ListView.builder(
              itemCount: list.length,
              itemBuilder: (context,index){
                var item = list[index];
                return new Dismissible(
                  key: Key(item),
                  child: new ListTile(
                    title: new Text(item),
                  ),
        
                  onDismissed: (direction){
                    list.remove(index);
                    print(direction);
                  },
        
                  background: Container(
                    color: Colors.red,
                    child: new Center(
                      child: new Text('刪除',
                      style: new TextStyle(
                        color: Colors.white
                      )
                      ),
                    ),
                  ),
                  secondaryBackground: new Container(
                    color: Colors.green,
                  ),
                );
              },
            );
          }
        }

三,原始指針事件

  在移動端,各個平台或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
    })

     

    • 示例demo:
      我們先看一個示例,后面再單獨討論一下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")
        ),

        上例中,只有點擊文本內容區域才會觸發點擊事件,因為 deferToChild 會去子widget判斷是否命中測試,而該例中子widget就是 Text("Box 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的話,我們可以使用IgnorePointerAbsorbPointer,這兩個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,那么兩個都不會輸出。

     

 





 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM