7 手勢(shì)、自定義Widget

手勢(shì)

Flutter中的手勢(shì)系統(tǒng)有兩個(gè)獨(dú)立的層。第一層為原始指針(pointer)事件,它描述了屏幕上指針(例如,觸摸、鼠標(biāo)和觸控筆)的位置和移動(dòng)。 第二層為手勢(shì),描述由一個(gè)或多個(gè)指針移動(dòng)組成的語(yǔ)義動(dòng)作,如拖動(dòng)、縮放、雙擊等。

原始指針事件

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

Flutter中可以使用Listener widget來(lái)監(jiān)聽(tīng)原始觸摸事件:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _MainState();
  }
}

class _MainState extends State<MainRoute> {
  //定義一個(gè)狀態(tài),保存當(dāng)前指針位置
  PointerEvent _event;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主頁(yè)"),
      ),
      body: Listener(
        child: Container(
          alignment: Alignment.center,
          width: 300.0,
          height: 150.0,
          child: Text(_event?.toString() ?? ""),
        ),
        onPointerDown: (PointerDownEvent event) =>
            setState(() => _event = event),
        onPointerMove: (PointerMoveEvent event) =>
            setState(() => _event = event),
        onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
      ),
    );
  }
}

PointerDownEvent、PointerMoveEvent、PointerUpEvent都是PointerEvent的一個(gè)子類(lèi),PointerEvent類(lèi)中包括當(dāng)前指針的一些信息。

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

命中測(cè)試

當(dāng)指針按下時(shí),F(xiàn)lutter會(huì)對(duì)應(yīng)用程序執(zhí)行命中測(cè)試(Hit Test),以確定指針與屏幕接觸的位置存在哪些widget, 指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測(cè)試發(fā)現(xiàn)的最內(nèi)部的widget,然后從那里開(kāi)始,事件會(huì)在widget樹(shù)中向上冒泡,這些事件會(huì)從最內(nèi)部的widget被分發(fā)到到widget根的路徑上的所有Widget。

behavior屬性決定子Widget如何響應(yīng)命中測(cè)試,它的值類(lèi)型為HitTestBehavior,這是一個(gè)枚舉類(lèi),有三個(gè)枚舉值:

  • deferToChild:子widget會(huì)一個(gè)接一個(gè)的進(jìn)行命中測(cè)試,如果子Widget中有測(cè)試通過(guò)的,則當(dāng)前Widget通過(guò)。

指針事件作用于子Widget上時(shí),父Widget也肯定可以收到該事件。

  • opaque:不透明的。在命中測(cè)試時(shí),將當(dāng)前Widget當(dāng)成不透明處理(即使本身是不可見(jiàn)、透明的),最終的效果相當(dāng)于當(dāng)前Widget的整個(gè)區(qū)域都是點(diǎn)擊區(qū)域。
  • translucent:半透明的。當(dāng)點(diǎn)擊Widget時(shí),widget可以接收到事件(無(wú)論是否可見(jiàn)),子widget則需要點(diǎn)擊到可見(jiàn)區(qū)域才能接收。
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: MainRoute(),
    );
  }
}

class MainRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MainState();
  }
}

class _MainState extends State<MainRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("主頁(yè)"),
      ),
      body: Listener(
        behavior: HitTestBehavior.translucent,
        child: Container(
          alignment: Alignment.center,
          ///300x150不可見(jiàn),只有Text可見(jiàn)
          ///默認(rèn)情況下,點(diǎn)擊Text區(qū)域才響應(yīng)事件。點(diǎn)擊空白區(qū)域無(wú)輸出;點(diǎn)擊Text才能響應(yīng)
          ///設(shè)置為 opaque 后 則在300x300內(nèi)都能響應(yīng),哪怕不可見(jiàn)。點(diǎn)擊空白區(qū)域就能響應(yīng)
          ///設(shè)置為 translucent 后 則在300x300能響應(yīng)。也是點(diǎn)擊空白區(qū)域就能響應(yīng)
//            color: Colors.blue,///不設(shè)置顏色就是不可見(jiàn)
          width: 300.0,
          height: 300.0,
          child: Text(
            "點(diǎn)擊",
          ),
        ),
        onPointerDown: (PointerDownEvent event) =>
            setState(() => debugPrint("響應(yīng)")),
      ),
    );
  }
}

opaquetranslucent的區(qū)別在于,后者是將透明區(qū)域視為半透明,這意味著能夠完成"穿透"效果。

需要注意的是點(diǎn)擊 外部 文字,因?yàn)槲淖直旧聿皇峭该?,不?huì)進(jìn)行穿透效果。

 @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("主頁(yè)"),
        ),
        body: Stack(
          children: <Widget>[
            Listener(
              child: Container(
                width: 300.0,
                height: 300.0,
                color: Colors.blue,
                child: Center(child: Text("底部")),
              ),
              onPointerDown: (event) => print("down0"),
            ),
            Listener(
              child: Container(
                width: 100.0,
                height: 100.0,
                child: Center(child: Text("外部")),
              ),
              onPointerDown: (event) => print("down1"),
              behavior: HitTestBehavior.translucent, //穿透
            )
          ],
        ));
  }

手勢(shì)識(shí)別

Android中存在事件沖突,F(xiàn)lutter其實(shí)也存在,但是官方的GestureDetector來(lái)解決這個(gè)問(wèn)題。通常我們?yōu)榱隧憫?yīng)用戶與設(shè)備屏幕交互就會(huì)使用這個(gè)手勢(shì)Widget:GestureDetector

包括之前使用的InkWell 內(nèi)部實(shí)現(xiàn)也是GestureDetector

手勢(shì) 說(shuō)明
onTapDown 按下
onTapUp 抬起
onTapCancel 觸發(fā)了 onTapDown,但并沒(méi)有完成一個(gè) onTap 動(dòng)作
onTap 點(diǎn)擊動(dòng)作
onDoubleTap 雙擊
onLongPress 長(zhǎng)按
onScaleStart, onScaleUpdate, onScaleEnd 縮放
onVerticalDragDown, onVerticalDragStart, onVerticalDragUpdate, onVerticalDragEnd, onVerticalDragCancel 在豎直方向上移動(dòng)
onHorizontalDragDown, onHorizontalDragStart, onHorizontalDragUpdate, onHorizontalDragEnd, onHorizontalDragCancel 在水平方向上移動(dòng)
onPanDown, onPanStart, onPanUpdate, onPanEnd, onPanCancel 拖曳

手勢(shì)的識(shí)別比較復(fù)雜。分解動(dòng)作:先點(diǎn)擊再進(jìn)行后續(xù)的手勢(shì)操作(滑動(dòng)、抬起)這時(shí)候點(diǎn)擊下去會(huì)回調(diào)所有的XXDown方法。因?yàn)榇藭r(shí)系統(tǒng)并不知道你需要進(jìn)行的后續(xù)手勢(shì)操作是什么。而如果是連貫的手勢(shì)動(dòng)作就只會(huì)回調(diào)對(duì)應(yīng)的Down方法。

同時(shí)如果同時(shí)設(shè)置了拖拽手勢(shì)參數(shù)與固定方向方法(水平、垂直)參數(shù)時(shí)候,那只會(huì)回調(diào)固定方向的方法。即固定方向優(yōu)先級(jí)最高。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主頁(yè)"),
        ),
        body: _Drag(),
      ),
    );
  }
}

class _Drag extends StatefulWidget {
  @override
  _DragState createState() => new _DragState();
}

class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
  double _top = 0.0; //距頂部的偏移
  double _left = 0.0; //距左邊的偏移

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //手指滑動(dòng)時(shí)會(huì)觸發(fā)此回調(diào)
            onPanUpdate: (DragUpdateDetails e) {
              //用戶手指滑動(dòng)時(shí),更新偏移,重新構(gòu)建
              setState(() {
                _left += e.delta.dx;
                _top += e.delta.dy;
              });
            },
          ),
        )
      ],
    );
  }
}

手勢(shì)沖突

如果我們同時(shí)監(jiān)聽(tīng)水平和垂直方向的拖動(dòng)事件,那么我們斜著拖動(dòng)時(shí)哪個(gè)方向會(huì)生效?實(shí)際上取決于第一次移動(dòng)時(shí)兩個(gè)軸上的位移分量,哪個(gè)軸的大,哪個(gè)軸在本次滑動(dòng)事件競(jìng)爭(zhēng)中就勝出。例如,假設(shè)有一個(gè)ListView,它的第一個(gè)子Widget也是ListView,如果現(xiàn)在滑動(dòng)這個(gè)子ListView,這時(shí)只有子Widget會(huì)動(dòng),因?yàn)檫@時(shí)子Widget會(huì)勝出而獲得滑動(dòng)事件的處理權(quán)。

識(shí)別水平和垂直方向的拖動(dòng)手勢(shì),當(dāng)用戶按下手指時(shí)就會(huì)觸發(fā)競(jìng)爭(zhēng)(水平方向和垂直方向),一旦某個(gè)方向“獲勝”,則直到當(dāng)次拖動(dòng)手勢(shì)結(jié)束都會(huì)沿著該方向移動(dòng)。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主頁(yè)"),
        ),
        body: Test(),
      ),
    );
  }
}

class Test extends StatefulWidget {
  @override
  TestState createState() => TestState();
}

class TestState extends State<Test> {
  double _top = 0.0;
  double _left = 0.0;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          top: _top,
          left: _left,
          child: GestureDetector(
            child: CircleAvatar(child: Text("A")),
            //垂直方向拖動(dòng)事件
            onVerticalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _top += details.delta.dy;
              });
            },
            onHorizontalDragUpdate: (DragUpdateDetails details) {
              setState(() {
                _left += details.delta.dx;
              });
            },
          ),
        )
      ],
    );
  }
}

自定義Widget

當(dāng)Flutter提供的現(xiàn)有Widget無(wú)法滿足我們的需求,或者我們?yōu)榱斯蚕泶a需要封裝一些通用Widget,這時(shí)我們就需要自定義Widget。自定義Widget主要有兩種方式:自繪與組合封裝。

自繪

對(duì)于一些復(fù)雜或不規(guī)則的UI,我們可能無(wú)法使用現(xiàn)有Widget組合的方式來(lái)實(shí)現(xiàn)。在Flutter中,提供了一個(gè)CustomPaint畫(huà)筆,它可以結(jié)合一個(gè)畫(huà)家CustomPainter來(lái)實(shí)現(xiàn)繪制自定義圖形。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("主頁(yè)"),
        ),
        body: GradientCircularProgressRoute(),
      ),
    );
  }
}

class GradientCircularProgressRoute extends StatefulWidget {
  @override
  GradientCircularProgressRouteState createState() {
    return  GradientCircularProgressRouteState();
  }
}

class GradientCircularProgressRouteState
    extends State<GradientCircularProgressRoute>  {
  @override
  Widget build(BuildContext context) {
    //返回畫(huà)筆
    return CustomPaint(
      painter: MyPainter(50.0),
    );
  }
}

class MyPainter extends CustomPainter {
  MyPainter(this.radius);

  double radius;

  @override
  void paint(Canvas canvas, Size size) {
    ///根據(jù)半徑計(jì)算大小
    size = Size.fromRadius(radius);
    var paint = Paint() //創(chuàng)建一個(gè)畫(huà)筆并配置其屬性
      ..isAntiAlias = true //是否抗鋸齒
      ..style = PaintingStyle.fill //畫(huà)筆樣式:填充
      ..color = Colors.blue //畫(huà)筆顏色
      ..strokeWidth = 3.0; //畫(huà)筆的寬度

    ///畫(huà)一個(gè)實(shí)心圓
    Rect rect =
    Rect.fromCircle(center: size.center(Offset.zero), radius: radius);
    canvas.drawCircle(rect.center, radius, paint);
  }


  /// 返回true來(lái)重繪,反之則應(yīng)返回false不需要重繪。
  @override
  bool shouldRepaint(MyPainter oldDelegate) {
    if(oldDelegate.radius != radius){
      return true;
    }
    return false;
  }
}

組合

這種方式是通過(guò)拼裝其它低級(jí)別的Widget來(lái)組合成一個(gè)高級(jí)別的Widget,例如Container就是一個(gè)組合Widget,它是由DecoratedBox、ConstrainedBox、Transform、Padding、Align等組成。

Notification機(jī)制

內(nèi)容引用自:
[Notification]: https://book.flutterchina.club/chapter8/notification.html

Notification是Flutter中一個(gè)重要的機(jī)制,在Widget樹(shù)中,每一個(gè)節(jié)點(diǎn)都可以分發(fā)通知,通知會(huì)沿著當(dāng)前節(jié)點(diǎn)(context)向上傳遞,所有父節(jié)點(diǎn)都可以通過(guò)NotificationListener來(lái)監(jiān)聽(tīng)通知,F(xiàn)lutter中稱這種通知由子向父的傳遞為“通知冒泡”(Notification Bubbling),這個(gè)和用戶觸摸事件冒泡是相似的,但有一點(diǎn)不同:通知冒泡可以中止,但用戶觸摸事件不行。

Flutter中很多地方使用了通知,如可滾動(dòng)(Scrollable) Widget中滑動(dòng)時(shí)就會(huì)分發(fā)ScrollNotification,而Scrollbar正是通過(guò)監(jiān)聽(tīng)ScrollNotification來(lái)確定滾動(dòng)條位置的。除了ScrollNotification,F(xiàn)lutter中還有SizeChangedLayoutNotification、KeepAliveNotification 、LayoutChangedNotification等。下面是一個(gè)監(jiān)聽(tīng)Scrollable Widget滾動(dòng)通知的例子:

NotificationListener(
  onNotification: (notification){
    //print(notification);
    switch (notification.runtimeType){
      case ScrollStartNotification: print("開(kāi)始滾動(dòng)"); break;
      case ScrollUpdateNotification: print("正在滾動(dòng)"); break;
      case ScrollEndNotification: print("滾動(dòng)停止"); break;
      case OverscrollNotification: print("滾動(dòng)到邊界"); break;
    }
  },
  child: ListView.builder(
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text("$index"),);
      }
  ),
);

上例中的滾動(dòng)通知如ScrollStartNotification、ScrollUpdateNotification等都是繼承自ScrollNotification類(lèi),不同類(lèi)型的通知子類(lèi)會(huì)包含不同的信息,比如ScrollUpdateNotification有一個(gè)scrollDelta屬性,它記錄了移動(dòng)的位移,其它通知屬性讀者可以自己查看SDK文檔。

自定義通知

除了Flutter內(nèi)部通知,我們也可以自定義通知,下面我們看看如何實(shí)現(xiàn)自定義通知:

  1. 定義一個(gè)通知類(lèi),要繼承自Notification類(lèi);

    class MyNotification extends Notification {
      MyNotification(this.msg);
      final String msg;
    }
    
  2. 分發(fā)通知。

    Notification有一個(gè)dispatch(context)方法,它是用于分發(fā)通知的,我們說(shuō)過(guò)context實(shí)際上就是操作Element的一個(gè)接口,它與Element樹(shù)上的節(jié)點(diǎn)是對(duì)應(yīng)的,通知會(huì)從context對(duì)應(yīng)的Element節(jié)點(diǎn)向上冒泡。

下面我們看一個(gè)完整的例子:

class NotificationRoute extends StatefulWidget {
  @override
  NotificationRouteState createState() {
    return new NotificationRouteState();
  }
}

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //監(jiān)聽(tīng)通知  
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
        setState(() {
          _msg+=notification.msg+"  ";
        });
      },
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
//          RaisedButton(
//           onPressed: () => MyNotification("Hi").dispatch(context),
//           child: Text("Send Notification"),
//          ),  
            Builder(
              builder: (context) {
                return RaisedButton(
                  //按鈕點(diǎn)擊時(shí)分發(fā)通知  
                  onPressed: () => MyNotification("Hi").dispatch(context),
                  child: Text("Send Notification"),
                );
              },
            ),
            Text(_msg)
          ],
        ),
      ),
    );
  }
}

class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}

上面代碼中,我們每點(diǎn)一次按鈕就會(huì)分發(fā)一個(gè)MyNotification類(lèi)型的通知,我們?cè)赪idget根上監(jiān)聽(tīng)通知,收到通知后我們將通知通過(guò)Text顯示在屏幕上。

注意:代碼中注釋的部分是不能正常工作的,因?yàn)檫@個(gè)context是根Context,而NotificationListener是監(jiān)聽(tīng)的子樹(shù),所以我們通過(guò)Builder來(lái)構(gòu)建RaisedButton,來(lái)獲得按鈕位置的context。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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