Flutter手勢互動+自定義繪板元件v0.01

張風捷特烈發表於2019-07-15

終於把基本的元件扯完了,真的是多如牛毛。現在讓我們來看一下控制元件如何實現互動
最後會實現一個簡單的有點筆觸效果的畫布,來說明如何使用手勢互動。

Flutter手勢互動+自定義繪板元件v0.01


1.從RaisedButton看事件互動

Flutter的元件中有很多是有點選事件的,比如按鈕,這裡簡單翻一下原始碼。

1.1:RaisedButton的使用

下面是RaisedButton的簡單使用,點選按鈕會列印日誌

Flutter手勢互動+自定義繪板元件v0.01

var show = RaisedButton(
  child: Text("RaisedButton", style: TextStyle(fontSize: 12),),
  onPressed: () {
    print("onPressed");
  },
);
複製程式碼

1.2:溯源之旅

核心是追一下onPressed的根源在哪裡,並簡單畫個圖示意一下。

Flutter手勢互動+自定義繪板元件v0.01

---->[flutter/lib/src/material/raised_button.dart:101]-------
class RaisedButton extends MaterialButton{
    const RaisedButton({
    Key key,
    @required VoidCallback onPressed,
    //首先onPressed是一個VoidCallback物件,從名稱來看是一個空回撥
    //略...
    }): super(
        key: key,
        onPressed: onPressed,//呼叫父類的onPressed
}

---->[flutter/lib/src/material/material_button.dart:40]-------
class MaterialButton extends StatelessWidget {
  //在build方法中onPressed傳給了RawMaterialButton
  @override
  Widget build(BuildContext context) {
    return RawMaterialButton(
    
      onPressed: onPressed,
        //略...
    );
  }
}

---->[flutter/lib/src/material/material_button.dart:40]-------
class RawMaterialButton extends StatefulWidget {
    @override
  _RawMaterialButtonState createState() => _RawMaterialButtonState();
}

class _RawMaterialButtonState extends State<RawMaterialButton> {
//在RawMaterialButton建立的時候,onPressed使用在InkWell上
@override
Widget build(BuildContext context) {
  final Widget result = Focus(
        //略...
        child: InkWell(
          onTap: widget.onPressed,

}

---->[flutter/lib/src/material/ink_well.dart:813]-------
class InkWell extends InkResponse {
  const InkWell({
    GestureTapCallback onTap,
  }) : super(
    onTap: onTap,//onTap傳給了父類
}

---->[flutter/lib/src/material/ink_well.dart:184]-------
class InkResponse extends StatefulWidget {
     @override
  _InkResponseState<InkResponse> createState() => _InkResponseState<InkResponse>();
}

class _InkResponseState<T extends InkResponse> extends
        State<T> with AutomaticKeepAliveClientMixin<T> {
  @override
  Widget build(BuildContext context) {
    return Listener(
        //略...
      child: GestureDetector(//通過onTap回撥_handleTap方法
        onTap: enabled ? () => _handleTap(context) : null,

    }
    
  void _handleTap(BuildContext context) {
    //略...
    if (widget.onTap != null) {
      if (widget.enableFeedback)
        Feedback.forTap(context);
      widget.onTap();//最終OnTap呼叫的位置
    }
  }
}
複製程式碼

於是我們發現了一個掌控事件的幕後大佬:GestureDetector


2.GestureDetector事件處理

首先本質上要認清,GestureDetector是一個無狀態的Widget

2.1:響應事件的盒子

既然GestureDetector的onTap可以傳入一個函式作為回撥處理,那何妨一試

Flutter手勢互動+自定義繪板元件v0.01

var box = Container(
  color: Colors.cyanAccent,
  width: 100,
  height: 100,
);
var show = GestureDetector(
  child: box,
  onTap: () {
    print("onTap in my box");
  },
);
複製程式碼

2.2:事件一覽(第一波):葫蘆七兄弟

首先介紹的的是常用的這七個,根據名字來看應該都不難理解

事件名 簡介 回撥物件 簡介
onTap 單擊
onTapDown 按下 TapDownDetails 按下時觸點資訊
onTapUp 抬起 TapUpDetails 抬起時觸點資訊
onTapCancel 取消按下
onDoubleTap 雙擊
onLongPress 長按
onLongPressUp 長按抬起
 var box = Container(
   color: Colors.cyanAccent,
   width: 100,
   height: 100,
 );

 var show = GestureDetector(
     child: box,
     onTap: () {
       print("onTap in my box");
     },
     onTapDown: (pos) {
       print(
           "落點----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
     },
     onTapUp: (pos) {
       print(
           "抬起點----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
     },
     onTapCancel: () {
       print("onTapCancel in my box");
     },
     onDoubleTap: () {
       print("onDoubleTap in my box");
     },
     onLongPress: () {
       print("onLongPress in my box");
     },
     onLongPressUp: () {
       print("onLongPressUp in my box");      });
複製程式碼

這裡有兩點說一下:1.雙擊時不會觸發點選事件
2.關於onTapCancel,什麼是點選取消?

---->[情景1:普通上滑]----
I/flutter (13474): 落點----(x,y):(55.61517333984375,157.59931437174478)
I/flutter (13474): onTapCancel in my box

---->[情景2:長按]----
I/flutter (13474): 落點----(x,y):(52.28492228190104,140.27338663736978)
I/flutter (13474): onTapCancel in my box
I/flutter (13474): onLongPress in my box
I/flutter (13474): onLongPressUp in my box
複製程式碼

2.3:事件一覽(第二波):十兄弟
事件名 簡介 回撥物件 簡介
onVerticalDragDown 豎直拖動按下 DragDownDetails 觸點資訊
onVerticalDragStart 豎直拖動開始 DragStartDetails 觸點資訊
onVerticalDragUpdate 豎直拖動更新 DragUpdateDetails 觸點資訊
onVerticalDragEnd 豎直拖動結束 DragEndDetails 觸點資訊
onVerticalDragCancel 豎直拖動取消
onHorizontalDragDown 水平拖動按下 DragDownDetails 觸點資訊
onHorizontalDragStart 水平拖動開始 DragStartDetails 觸點資訊
onHorizontalDragUpdate 水平拖動更新 DragUpdateDetails 觸點資訊
onHorizontalDragEnd 水平拖動結束 DragEndDetails 觸點資訊
onHorizontalDragCancel 水平拖動取消

這裡對豎直的五個進行測試,水平的五個也類似

var show = GestureDetector(
    child: box,
    onVerticalDragDown: (pos) {
      print(
          "豎直拖拽按下----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
    },
    onVerticalDragStart: (pos) {
      print(
          "開始豎直拖拽----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
    },
    onVerticalDragUpdate: (pos) {
      print(
          "豎直拖拽更新----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
    },
    onVerticalDragEnd: (pos) {
      print(
          "豎直拖拽結束速度----(x,y):(${pos.velocity.pixelsPerSecond.dx},${pos.velocity.pixelsPerSecond.dy})");
    },
    onVerticalDragCancel: () {
      print("onVerticalDragCancel in my box");
    });
複製程式碼

這裡我想左上角快速滑動了一下,日誌為:

I/flutter (13474): 豎直拖拽按下----(x,y):(68.27012125651042,171.9265340169271)
I/flutter (13474): 開始豎直拖拽----(x,y):(68.27012125651042,171.9265340169271)
I/flutter (13474): 豎直拖拽更新----(x,y):(64.60684712727864,167.26185099283853)
I/flutter (13474): 豎直拖拽更新----(x,y):(57.94634501139323,159.26526896158853)
I/flutter (13474): 豎直拖拽更新----(x,y):(49.95374552408854,148.93635050455728)
I/flutter (13474): 豎直拖拽更新----(x,y):(39.62997182210287,137.60785929361978)
I/flutter (13474): 豎直拖拽更新----(x,y):(28.640146891276043,125.6129862467448)
I/flutter (13474): 豎直拖拽更新----(x,y):(16.31822458902995,113.6181131998698)
I/flutter (13474): 豎直拖拽結束速度----(x,y):(-1476.3951158711095,-1569.520405720337)
複製程式碼

注意一下,通過測試發現,如果只有豎直方向的處理,那麼即使水平滑動也會觸發回撥
但是豎直的水平同時出現時,會自動判斷你的滑動方向來進行相應的回撥。
另外原始碼說了:兩者最好不要一起用。如果想簡單的使用,可以用pan

  /// Horizontal and vertical drag callbacks cannot be used simultaneously(同時地)
  /// because a combination(組成) of a horizontal and vertical drag is a pan. Simply
  /// use the pan callbacks instead.
複製程式碼

2.4:事件一覽(第三波):五火教主

別怕,如上面所說,這也五個是拖動事件,只不過沒有方向區分而言

事件名 簡介 回撥物件 簡介
onPanDown 豎直拖動按下 DragDownDetails 觸點資訊
onPanStart 豎直拖動開始 DragStartDetails 觸點資訊
onPanUpdate 豎直拖動更新 DragUpdateDetails 觸點資訊
onPanEnd 豎直拖動結束 DragEndDetails 速度資訊
onPanCancel 豎直拖動取消
var box = Container(
  color: Colors.cyanAccent,
  width: 200,
  height: 200,
);
var show = GestureDetector(
  child: box,
  onPanDown: (pos) {
    print(
        "拖拽按下----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
  },
  onPanStart: (pos) {
    print(
        "開始拖拽----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
  },
  onPanUpdate: (pos) {
    print(
        "拖拽更新----(x,y):(${pos.globalPosition.dx},${pos.globalPosition.dy})");
  },
  onPanEnd: (pos) {
    print(
        "拖拽結束速度----(x,y):(${pos.velocity.pixelsPerSecond.dx},${pos.velocity.pixelsPerSecond.dy})");
  },
  onPanCancel: () {
    print("onPanCancel in my box");
  },
);
複製程式碼

2.5:事件一覽(第四波):三足鼎立

原始碼中說:Pan和scale回撥不能同時使用,因為scale是Pan的超集。簡單的話,使用scale回撥函式即可。
在使用上和前面的拖動時間基本一致,這裡就不再贅述。

var box = Container(
  color: Colors.cyanAccent,
  width: 200,
  height: 200,
);
var show = GestureDetector(
  child: box,
  onScaleStart: (pos) {
    print(
        "onScaleStart----(x,y):(${pos.focalPoint.dx},${pos.focalPoint.dy})");
  },
  onScaleUpdate: (pos) {
    print(
        "onScaleUpdate----(x,y):(${pos.focalPoint.dx},${pos.focalPoint.dy})");
  },
  onScaleEnd: (pos) {
    print(
        "onScaleEnd----(x,y):(${pos.velocity.pixelsPerSecond.dx},${pos.velocity.pixelsPerSecond.dy})");
  },
);
複製程式碼

2.6:關於InkWell

InkWell也是一個擁有事件處理能力的元件,只不過支援的事件比較少
常用包括點選,雙擊,長按,按下,特點是有水波紋效果(注:Container背景色會掩蓋水波紋)。

Flutter手勢互動+自定義繪板元件v0.01

var box = Container(
  width: 120,
  height: 120*0.681,
);
var show = InkWell
(
  child: box,
  focusColor: Colors.red,//聚焦時顏色
  hoverColor: Colors.yellow,//炫富色??
  splashColor: Colors.grey,//水波紋色
  highlightColor: Colors.blue,//長按時會顯示該色
  borderRadius: BorderRadius.all(Radius.elliptical(10, 10)),
  onTap: () {
    print("OnTap in InkWell");
  },
);
複製程式碼

3.手繪板 v0.01

3.0:前置準備
需要的知識點:Flutter中的手勢互動,主要是移動相關  
1.一條線是點的集合,繪板需要畫n條線,所以是點的集合的集合 _lines
2.元件為有狀態元件,_lines為狀態量,在移動時將點加入當前所畫的線  
3.當抬起時說明一條線完畢,應該拷貝入_lines,並清空當前線作為下一條
4.繪製單體類有顏色,大小,位置三個屬性,類名TolyCircle

class TolyDrawable {
  Color color;//顏色
  Offset pos;//位置
  TolyDrawable(this.color,this.pos);
}

class TolyCicle extends TolyDrawable{
  double radius;//大小
  TolyCicle(Color color, Offset pos,{this.radius=1}) : super(color, pos);
}
複製程式碼

3.1:準備畫板Paper

這裡傳入lines作為線集,遍歷線再遍歷點

class Paper extends CustomPainter{

  Paper({
    @required this.lines,
  }) {
    _paint = Paint()..style=PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
  }

  Paint _paint;
  final List<List<TolyCicle>> lines;
  
  @override
  void paint(Canvas canvas, Size size) {
    for (int i = 0; i < lines.length; i++) {
      drawLine(canvas,lines[i]);
    }
  
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
  ///根據點位繪製線
  void drawLine(Canvas canvas,List<TolyCicle> positions) {
    for (int i = 0; i < positions.length - 1; i++) {
      if (positions[i] != null && positions[i + 1] != null)
        canvas.drawLine(positions[i].pos, positions[i + 1].pos,
        _paint..strokeWidth=positions[i].radius);
    }
  }
}
複製程式碼

3.2:繪板元件

這樣就可以了,這裡還有很多待完善的地方,不過作為手勢的互動應用的例子還是不錯的

Flutter手勢互動+自定義繪板元件v0.01

class TolyCanvas extends StatefulWidget{
  @override
  State<StatefulWidget> createState() => _TolyCanvasState();

}

class _TolyCanvasState extends State<TolyCanvas> {
  var _positions=<TolyCicle>[];
  var _lines=<List<TolyCicle>>[];
  Offset _oldPos;//記錄上一點
  
  @override
  Widget build(BuildContext context) {

    var body=CustomPaint(
      painter: Paper(lines: _lines),
    );

    var scaffold = Scaffold(
      body: body,
    );

    var result =GestureDetector(
      child: scaffold,
      onPanDown: _panDown,
      onPanUpdate: _panUpdate,
      onPanEnd: _panEnd,
      onDoubleTap: (){
        _lines.clear();
        _render();
      },
    );
    return result;
  }

  /// 按下時表示新新增一條線,並記錄上一點位置
  void _panDown(DragDownDetails details) {
    print(details.toString());
    _lines.add(_positions);

    var x=details.globalPosition.dx;
    var y=details.globalPosition.dy;
    _oldPos= Offset(x, y);

  }

  ///渲染方法,將重新渲染元件
  void _render(){
    setState(() {

    });
  }
  ///移動中,將點新增到點集中
  void _panUpdate(DragUpdateDetails details) {
    var x=details.globalPosition.dx;
    var y=details.globalPosition.dy;
    var curPos = Offset(x, y);
    if ((curPos-_oldPos).distance>3) {//距離小於3不處理,避免渲染過多
      var len = (curPos-_oldPos).distance;
      var width =40* pow(len,-1.2);//TODO 處理不夠順滑,待處理
      var tolyCicle = TolyCicle(Colors.blue, curPos,radius:width);
      _positions.add(tolyCicle);
      _oldPos=curPos;
      _render();
    }

  }
  /// 抬起後,將舊線拷貝到線集中
  void _panEnd(DragEndDetails details) {
    var oldBall = <TolyCicle>[];
    for (int i = 0; i < _positions.length; i++) {
      oldBall.add(_positions[i]);
    }
    _lines.add(oldBall);
    _positions.clear();

  }
}

複製程式碼

結語

本文到此接近尾聲了,另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,
本人微訊號:zdl1994328,期待與你的交流與切磋。如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品。

相關文章