本節(jié)先介紹一些 Flutter 中用于處理手勢(shì)的GestureDetector
和GestureRecognizer
,然后再仔細(xì)討論一下手勢(shì)競(jìng)爭(zhēng)與沖突問(wèn)題。
GestureDetector
是一個(gè)用于手勢(shì)識(shí)別的功能性組件,我們通過(guò)它可以來(lái)識(shí)別各種手勢(shì)。GestureDetector
實(shí)際上是指針事件的語(yǔ)義化封裝,接下來(lái)我們?cè)敿?xì)介紹一下各種手勢(shì)識(shí)別。
我們通過(guò)GestureDetector
對(duì)Container
進(jìn)行手勢(shì)識(shí)別,觸發(fā)相應(yīng)事件后,在Container
上顯示事件名,為了增大點(diǎn)擊區(qū)域,將Container
設(shè)置為 200×100,代碼如下:
class GestureDetectorTestRoute extends StatefulWidget {
@override
_GestureDetectorTestRouteState createState() =>
new _GestureDetectorTestRouteState();
}
class _GestureDetectorTestRouteState extends State<GestureDetectorTestRoute> {
String _operation = "No Gesture detected!"; //保存事件名
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 200.0,
height: 100.0,
child: Text(_operation,
style: TextStyle(color: Colors.white),
),
),
onTap: () => updateText("Tap"),//點(diǎn)擊
onDoubleTap: () => updateText("DoubleTap"), //雙擊
onLongPress: () => updateText("LongPress"), //長(zhǎng)按
),
);
}
void updateText(String text) {
//更新顯示的事件名
setState(() {
_operation = text;
});
}
}
運(yùn)行效果如圖8-2所示:
注意: 當(dāng)同時(shí)監(jiān)聽
onTap
和onDoubleTap
事件時(shí),當(dāng)用戶觸 發(fā)tap 事件時(shí),會(huì)有200毫秒左右的延時(shí),這是因?yàn)楫?dāng)用戶點(diǎn)擊完之后很可能會(huì)再次點(diǎn)擊以觸發(fā)雙擊事件,所以GestureDetector
會(huì)等一段時(shí)間來(lái)確定是否為雙擊事件。如果用戶只監(jiān)聽了onTap
(沒(méi)有監(jiān)聽onDoubleTap
)事件時(shí),則沒(méi)有延時(shí)。
一次完整的手勢(shì)過(guò)程是指用戶手指按下到抬起的整個(gè)過(guò)程,期間,用戶按下手指后可能會(huì)移動(dòng),也可能不會(huì)移動(dòng)。GestureDetector
對(duì)于拖動(dòng)和滑動(dòng)事件是沒(méi)有區(qū)分的,他們本質(zhì)上是一樣的。GestureDetector
會(huì)將要監(jiān)聽的組件的原點(diǎn)(左上角)作為本次手勢(shì)的原點(diǎn),當(dāng)用戶在監(jiān)聽的組件上按下手指時(shí),手勢(shì)識(shí)別就會(huì)開始。下面我們看一個(gè)拖動(dòng)圓形字母A的示例:
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")),
//手指按下時(shí)會(huì)觸發(fā)此回調(diào)
onPanDown: (DragDownDetails e) {
//打印手指按下的位置(相對(duì)于屏幕)
print("用戶手指按下:${e.globalPosition}");
},
//手指滑動(dòng)時(shí)會(huì)觸發(fā)此回調(diào)
onPanUpdate: (DragUpdateDetails e) {
//用戶手指滑動(dòng)時(shí),更新偏移,重新構(gòu)建
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
});
},
onPanEnd: (DragEndDetails e){
//打印滑動(dòng)結(jié)束時(shí)在x、y軸上的速度
print(e.velocity);
},
),
)
],
);
}
}
運(yùn)行后,就可以在任意方向拖動(dòng)了,運(yùn)行效果如圖8-3所示:
日志:
I/flutter ( 8513): 用戶手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)
代碼解釋:
DragDownDetails.globalPosition
:當(dāng)用戶按下時(shí),此屬性為用戶按下的位置相對(duì)于屏幕(而非父組件)原點(diǎn)(左上角)的偏移。DragUpdateDetails.delta
:當(dāng)用戶在屏幕上滑動(dòng)時(shí),會(huì)觸發(fā)多次 Update 事件,delta
指一次 Update 事件的滑動(dòng)的偏移量。DragEndDetails.velocity
:該屬性代表用戶抬起手指時(shí)的滑動(dòng)速度(包含 x、y 兩個(gè)軸的),示例中并沒(méi)有處理手指抬起時(shí)的速度,常見的效果是根據(jù)用戶抬起手指時(shí)的速度做一個(gè)減速動(dòng)畫。
在本示例中,是可以朝任意方向拖動(dòng)的,但是在很多場(chǎng)景,我們只需要沿一個(gè)方向來(lái)拖動(dòng),如一個(gè)垂直方向的列表,GestureDetector
可以只識(shí)別特定方向的手勢(shì)事件,我們將上面的例子改為只能沿垂直方向拖動(dòng):
class _DragVertical extends StatefulWidget {
@override
_DragVerticalState createState() => new _DragVerticalState();
}
class _DragVerticalState extends State<_DragVertical> {
double _top = 0.0;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: _top,
child: GestureDetector(
child: CircleAvatar(child: Text("A")),
//垂直方向拖動(dòng)事件
onVerticalDragUpdate: (DragUpdateDetails details) {
setState(() {
_top += details.delta.dy;
});
}
),
)
],
);
}
}
這樣就只能在垂直方向拖動(dòng)了,如果只想在水平方向滑動(dòng)同理。
GestureDetector
可以監(jiān)聽縮放事件,下面示例演示了一個(gè)簡(jiǎn)單的圖片縮放效果:
class _ScaleTestRouteState extends State<_ScaleTestRoute> {
double _width = 200.0; //通過(guò)修改圖片寬度來(lái)達(dá)到縮放效果
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
//指定寬度,高度自適應(yīng)
child: Image.asset("./images/sea.png", width: _width),
onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
//縮放倍數(shù)在0.8到10倍之間
_width=200*details.scale.clamp(.8, 10.0);
});
},
),
);
}
}
運(yùn)行效果如圖8-4所示:
現(xiàn)在在圖片上雙指張開、收縮就可以放大、縮小圖片。本示例比較簡(jiǎn)單,實(shí)際中我們通常還需要一些其它功能,如雙擊放大或縮小一定倍數(shù)、雙指張開離開屏幕時(shí)執(zhí)行一個(gè)減速放大動(dòng)畫等,讀者可以在學(xué)習(xí)完后面“動(dòng)畫”一章中的內(nèi)容后自己來(lái)嘗試實(shí)現(xiàn)一下。
GestureDetector
內(nèi)部是使用一個(gè)或多個(gè)GestureRecognizer
來(lái)識(shí)別各種手勢(shì)的,而GestureRecognizer
的作用就是通過(guò)Listener
來(lái)將原始指針事件轉(zhuǎn)換為語(yǔ)義手勢(shì),GestureDetector
直接可以接收一個(gè)子 widget。GestureRecognizer
是一個(gè)抽象類,一種手勢(shì)的識(shí)別器對(duì)應(yīng)一個(gè)GestureRecognizer
的子類,F(xiàn)lutter 實(shí)現(xiàn)了豐富的手勢(shì)識(shí)別器,我們可以直接使用。
假設(shè)我們要給一段富文本(RichText
)的不同部分分別添加點(diǎn)擊事件處理器,但是TextSpan
并不是一個(gè) widget,這時(shí)我們不能用GestureDetector
,但TextSpan
有一個(gè)recognizer
屬性,它可以接收一個(gè)GestureRecognizer
。
假設(shè)我們需要在點(diǎn)擊時(shí)給文本變色:
import 'package:flutter/gestures.dart';
class _GestureRecognizerTestRouteState
extends State<_GestureRecognizerTestRoute> {
TapGestureRecognizer _tapGestureRecognizer = new TapGestureRecognizer();
bool _toggle = false; //變色開關(guān)
@override
void dispose() {
//用到GestureRecognizer的話一定要調(diào)用其dispose方法釋放資源
_tapGestureRecognizer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Text.rich(
TextSpan(
children: [
TextSpan(text: "你好世界"),
TextSpan(
text: "點(diǎn)我變色",
style: TextStyle(
fontSize: 30.0,
color: _toggle ? Colors.blue : Colors.red
),
recognizer: _tapGestureRecognizer
..onTap = () {
setState(() {
_toggle = !_toggle;
});
},
),
TextSpan(text: "你好世界"),
]
)
),
);
}
}
運(yùn)行效果:
注意:使用
GestureRecognizer
后一定要調(diào)用其dispose()
方法來(lái)釋放資源(主要是取消內(nèi)部的計(jì)時(shí)器)。
如果在上例中我們同時(shí)監(jiān)聽水平和垂直方向的拖動(dòng)事件,那么我們斜著拖動(dòng)時(shí)哪個(gè)方向會(huì)生效?實(shí)際上取決于第一次移動(dòng)時(shí)兩個(gè)軸上的位移分量,哪個(gè)軸的大,哪個(gè)軸在本次滑動(dòng)事件競(jìng)爭(zhēng)中就勝出。實(shí)際上 Flutter 中的手勢(shì)識(shí)別引入了一個(gè) Arena 的概念,Arena 直譯為“競(jìng)技場(chǎng)”的意思,每一個(gè)手勢(shì)識(shí)別器(GestureRecognizer
)都是一個(gè)“競(jìng)爭(zhēng)者”(GestureArenaMember
),當(dāng)發(fā)生滑動(dòng)事件時(shí),他們都要在“競(jìng)技場(chǎng)”去競(jìng)爭(zhēng)本次事件的處理權(quán),而最終只有一個(gè)“競(jìng)爭(zhēng)者”會(huì)勝出(win)。例如,假設(shè)有一個(gè)ListView
,它的第一個(gè)子組件也是ListView
,如果現(xiàn)在滑動(dòng)這個(gè)子ListView
,父ListView
會(huì)動(dòng)嗎?答案是否定的,這時(shí)只有子ListView
會(huì)動(dòng),因?yàn)檫@時(shí)子ListView
會(huì)勝出而獲得滑動(dòng)事件的處理權(quán)。
我們以拖動(dòng)手勢(shì)為例,同時(shí)識(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';
class BothDirectionTestRoute extends StatefulWidget {
@override
BothDirectionTestRouteState createState() =>
new BothDirectionTestRouteState();
}
class BothDirectionTestRouteState extends State<BothDirectionTestRoute> {
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;
});
},
),
)
],
);
}
}
此示例運(yùn)行后,每次拖動(dòng)只會(huì)沿一個(gè)方向移動(dòng)(水平或垂直),而競(jìng)爭(zhēng)發(fā)生在手指按下后首次移動(dòng)(move)時(shí),此例中具體的“獲勝”條件是:首次移動(dòng)時(shí)的位移在水平和垂直方向上的分量大的一個(gè)獲勝。
由于手勢(shì)競(jìng)爭(zhēng)最終只有一個(gè)勝出者,所以,當(dāng)有多個(gè)手勢(shì)識(shí)別器時(shí),可能會(huì)產(chǎn)生沖突。假設(shè)有一個(gè) widget,它可以左右拖動(dòng),現(xiàn)在我們也想檢測(cè)在它上面手指按下和抬起的事件,代碼如下:
class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
double _left = 0.0;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: _left,
child: GestureDetector(
child: CircleAvatar(child: Text("A")), //要拖動(dòng)和點(diǎn)擊的widget
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_left += details.delta.dx;
});
},
onHorizontalDragEnd: (details){
print("onHorizontalDragEnd");
},
onTapDown: (details){
print("down");
},
onTapUp: (details){
print("up");
},
),
)
],
);
}
}
現(xiàn)在我們按住圓形“A”拖動(dòng)然后抬起手指,控制臺(tái)日志如下:
I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd
我們發(fā)現(xiàn)沒(méi)有打印"up",這是因?yàn)樵谕蟿?dòng)時(shí),剛開始按下手指時(shí)在沒(méi)有移動(dòng)時(shí),拖動(dòng)手勢(shì)還沒(méi)有完整的語(yǔ)義,此時(shí) TapDown 手勢(shì)勝出(win),此時(shí)打印"down",而拖動(dòng)時(shí),拖動(dòng)手勢(shì)會(huì)勝出,當(dāng)手指抬起時(shí),onHorizontalDragEnd
和 onTapUp
發(fā)生了沖突,但是因?yàn)槭窃谕蟿?dòng)的語(yǔ)義中,所以onHorizontalDragEnd
勝出,所以就會(huì)打印 “onHorizontalDragEnd”。如果我們的代碼邏輯中,對(duì)于手指按下和抬起是強(qiáng)依賴的,比如在一個(gè)輪播圖組件中,我們希望手指按下時(shí),暫停輪播,而抬起時(shí)恢復(fù)輪播,但是由于輪播圖組件中本身可能已經(jīng)處理了拖動(dòng)手勢(shì)(支持手動(dòng)滑動(dòng)切換),甚至可能也支持了縮放手勢(shì),這時(shí)我們?nèi)绻谕獠吭儆?code>onTapDown、onTapUp
來(lái)監(jiān)聽的話是不行的。這時(shí)我們應(yīng)該怎么做?其實(shí)很簡(jiǎn)單,通過(guò) Listener 監(jiān)聽原始指針事件就行:
Positioned(
top:80.0,
left: _leftB,
child: Listener(
onPointerDown: (details) {
print("down");
},
onPointerUp: (details) {
//會(huì)觸發(fā)
print("up");
},
child: GestureDetector(
child: CircleAvatar(child: Text("B")),
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_leftB += details.delta.dx;
});
},
onHorizontalDragEnd: (details) {
print("onHorizontalDragEnd");
},
),
),
)
手勢(shì)沖突只是手勢(shì)級(jí)別的,而手勢(shì)是對(duì)原始指針的語(yǔ)義化的識(shí)別,所以在遇到復(fù)雜的沖突場(chǎng)景時(shí),都可以通過(guò)Listener
直接識(shí)別原始指針事件來(lái)解決沖突。
更多建議: