終於把基本的元件扯完了,真的是多如牛毛。現在讓我們來看一下控制元件如何實現互動
最後會實現一個簡單的有點筆觸效果的畫布,來說明如何使用手勢互動。
1.從RaisedButton看事件互動
Flutter的元件中有很多是有點選事件的,比如按鈕,這裡簡單翻一下原始碼。
1.1:RaisedButton的使用
下面是RaisedButton的簡單使用,點選按鈕會列印日誌
var show = RaisedButton(
child: Text("RaisedButton", style: TextStyle(fontSize: 12),),
onPressed: () {
print("onPressed");
},
);
複製程式碼
1.2:溯源之旅
核心是追一下onPressed的根源在哪裡,並簡單畫個圖示意一下。
---->[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可以傳入一個函式作為回撥處理,那何妨一試
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背景色會掩蓋水波紋)。
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七日》會是你的必備佳品。