Flutter手勢交互+自定義繪板組件v0.01

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


1.從RaisedButton看事件交互

Flutter的組件中有不少是有點擊事件的,好比按鈕,這裏簡單翻一下源碼。bash

1.1:RaisedButton的使用

下面是RaisedButton的簡單使用,點擊按鈕會打印日誌微信

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

1.2:溯源之旅

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

---->[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調用的位置
    }
  }
}
複製代碼

因而咱們發現了一個掌控事件的幕後大佬:GestureDetectoride


2.GestureDetector事件處理

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

2.1:響應事件的盒子

既然GestureDetector的onTap能夠傳入一個函數做爲回調處理,那何妨一試post

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,什麼是點擊取消?ui

---->[情景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 水平拖動取消

這裏對豎直的五個進行測試,水平的五個也相似this

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背景色會掩蓋水波紋)。

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:繪板組件

這樣就能夠了,這裏還有不少待完善的地方,不過做爲手勢的交互應用的例子仍是不錯的

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七日》會是你的必備佳品。

相關文章
相關標籤/搜索