手勢(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)")),
),
);
}
}
opaque和translucent的區(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)自定義通知:
-
定義一個(gè)通知類(lèi),要繼承自Notification類(lèi);
class MyNotification extends Notification { MyNotification(this.msg); final String msg; } -
分發(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。