手勢
Flutter中的手勢系統(tǒng)有兩個獨立的層。第一層為原始指針(pointer)事件,它描述了屏幕上指針(例如,觸摸、鼠標(biāo)和觸控筆)的位置和移動。 第二層為手勢,描述由一個或多個指針移動組成的語義動作,如拖動、縮放、雙擊等。
原始指針事件
在移動端,各個平臺或UI系統(tǒng)的原始指針事件模型基本都是一致,即:一次完整的事件分為三個階段:手指按下、手指移動、和手指抬起,而高級的手勢(如點擊、雙擊、拖動等)都是基于這些原始事件的。
Flutter中可以使用Listener widget來監(jiā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: MainRoute(),
);
}
}
class MainRoute extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _MainState();
}
}
class _MainState extends State<MainRoute> {
//定義一個狀態(tài),保存當(dāng)前指針位置
PointerEvent _event;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主頁"),
),
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的一個子類,PointerEvent類中包括當(dāng)前指針的一些信息。
-
position:它是鼠標(biāo)相對于當(dāng)對于全局坐標(biāo)的偏移。(左上角原點) -
delta:兩次指針移動事件(PointerMoveEvent)的距離。 -
pressure:按壓力度,如果手機屏幕支持壓力傳感器(如iPhone的3D Touch),此屬性會更有意義,如果手機不支持,則始終為1。 -
orientation:指針移動方向,是一個角度值。
命中測試
當(dāng)指針按下時,F(xiàn)lutter會對應(yīng)用程序執(zhí)行命中測試(Hit Test),以確定指針與屏幕接觸的位置存在哪些widget, 指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測試發(fā)現(xiàn)的最內(nèi)部的widget,然后從那里開始,事件會在widget樹中向上冒泡,這些事件會從最內(nèi)部的widget被分發(fā)到到widget根的路徑上的所有Widget。
behavior屬性決定子Widget如何響應(yīng)命中測試,它的值類型為HitTestBehavior,這是一個枚舉類,有三個枚舉值:
-
deferToChild:子widget會一個接一個的進行命中測試,如果子Widget中有測試通過的,則當(dāng)前Widget通過。
指針事件作用于子Widget上時,父Widget也肯定可以收到該事件。
-
opaque:不透明的。在命中測試時,將當(dāng)前Widget當(dāng)成不透明處理(即使本身是不可見、透明的),最終的效果相當(dāng)于當(dāng)前Widget的整個區(qū)域都是點擊區(qū)域。 -
translucent:半透明的。當(dāng)點擊Widget時,widget可以接收到事件(無論是否可見),子widget則需要點擊到可見區(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("主頁"),
),
body: Listener(
behavior: HitTestBehavior.translucent,
child: Container(
alignment: Alignment.center,
///300x150不可見,只有Text可見
///默認情況下,點擊Text區(qū)域才響應(yīng)事件。點擊空白區(qū)域無輸出;點擊Text才能響應(yīng)
///設(shè)置為 opaque 后 則在300x300內(nèi)都能響應(yīng),哪怕不可見。點擊空白區(qū)域就能響應(yīng)
///設(shè)置為 translucent 后 則在300x300能響應(yīng)。也是點擊空白區(qū)域就能響應(yīng)
// color: Colors.blue,///不設(shè)置顏色就是不可見
width: 300.0,
height: 300.0,
child: Text(
"點擊",
),
),
onPointerDown: (PointerDownEvent event) =>
setState(() => debugPrint("響應(yīng)")),
),
);
}
}
opaque和translucent的區(qū)別在于,后者是將透明區(qū)域視為半透明,這意味著能夠完成"穿透"效果。
需要注意的是點擊 外部 文字,因為文字本身不是透明,不會進行穿透效果。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主頁"),
),
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, //穿透
)
],
));
}
手勢識別
Android中存在事件沖突,F(xiàn)lutter其實也存在,但是官方的GestureDetector來解決這個問題。通常我們?yōu)榱隧憫?yīng)用戶與設(shè)備屏幕交互就會使用這個手勢Widget:GestureDetector。
包括之前使用的InkWell 內(nèi)部實現(xiàn)也是GestureDetector
| 手勢 | 說明 |
|---|---|
onTapDown |
按下 |
onTapUp |
抬起 |
onTapCancel |
觸發(fā)了 onTapDown,但并沒有完成一個 onTap 動作 |
onTap |
點擊動作 |
onDoubleTap |
雙擊 |
onLongPress |
長按 |
onScaleStart, onScaleUpdate, onScaleEnd |
縮放 |
onVerticalDragDown, onVerticalDragStart, onVerticalDragUpdate, onVerticalDragEnd, onVerticalDragCancel |
在豎直方向上移動 |
onHorizontalDragDown, onHorizontalDragStart, onHorizontalDragUpdate, onHorizontalDragEnd, onHorizontalDragCancel |
在水平方向上移動 |
onPanDown, onPanStart, onPanUpdate, onPanEnd, onPanCancel |
拖曳 |
手勢的識別比較復(fù)雜。分解動作:先點擊再進行后續(xù)的手勢操作(滑動、抬起)這時候點擊下去會回調(diào)所有的
XXDown方法。因為此時系統(tǒng)并不知道你需要進行的后續(xù)手勢操作是什么。而如果是連貫的手勢動作就只會回調(diào)對應(yīng)的Down方法。同時如果同時設(shè)置了拖拽手勢參數(shù)與固定方向方法(水平、垂直)參數(shù)時候,那只會回調(diào)固定方向的方法。即固定方向優(yōu)先級最高。
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("主頁"),
),
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")),
//手指滑動時會觸發(fā)此回調(diào)
onPanUpdate: (DragUpdateDetails e) {
//用戶手指滑動時,更新偏移,重新構(gòu)建
setState(() {
_left += e.delta.dx;
_top += e.delta.dy;
});
},
),
)
],
);
}
}
手勢沖突
如果我們同時監(jiān)聽水平和垂直方向的拖動事件,那么我們斜著拖動時哪個方向會生效?實際上取決于第一次移動時兩個軸上的位移分量,哪個軸的大,哪個軸在本次滑動事件競爭中就勝出。例如,假設(shè)有一個ListView,它的第一個子Widget也是ListView,如果現(xiàn)在滑動這個子ListView,這時只有子Widget會動,因為這時子Widget會勝出而獲得滑動事件的處理權(quán)。
識別水平和垂直方向的拖動手勢,當(dāng)用戶按下手指時就會觸發(fā)競爭(水平方向和垂直方向),一旦某個方向“獲勝”,則直到當(dāng)次拖動手勢結(jié)束都會沿著該方向移動。
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("主頁"),
),
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")),
//垂直方向拖動事件
onVerticalDragUpdate: (DragUpdateDetails details) {
setState(() {
_top += details.delta.dy;
});
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_left += details.delta.dx;
});
},
),
)
],
);
}
}
自定義Widget
當(dāng)Flutter提供的現(xiàn)有Widget無法滿足我們的需求,或者我們?yōu)榱斯蚕泶a需要封裝一些通用Widget,這時我們就需要自定義Widget。自定義Widget主要有兩種方式:自繪與組合封裝。
自繪
對于一些復(fù)雜或不規(guī)則的UI,我們可能無法使用現(xiàn)有Widget組合的方式來實現(xiàn)。在Flutter中,提供了一個CustomPaint畫筆,它可以結(jié)合一個畫家CustomPainter來實現(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("主頁"),
),
body: GradientCircularProgressRoute(),
),
);
}
}
class GradientCircularProgressRoute extends StatefulWidget {
@override
GradientCircularProgressRouteState createState() {
return GradientCircularProgressRouteState();
}
}
class GradientCircularProgressRouteState
extends State<GradientCircularProgressRoute> {
@override
Widget build(BuildContext context) {
//返回畫筆
return CustomPaint(
painter: MyPainter(50.0),
);
}
}
class MyPainter extends CustomPainter {
MyPainter(this.radius);
double radius;
@override
void paint(Canvas canvas, Size size) {
///根據(jù)半徑計算大小
size = Size.fromRadius(radius);
var paint = Paint() //創(chuàng)建一個畫筆并配置其屬性
..isAntiAlias = true //是否抗鋸齒
..style = PaintingStyle.fill //畫筆樣式:填充
..color = Colors.blue //畫筆顏色
..strokeWidth = 3.0; //畫筆的寬度
///畫一個實心圓
Rect rect =
Rect.fromCircle(center: size.center(Offset.zero), radius: radius);
canvas.drawCircle(rect.center, radius, paint);
}
/// 返回true來重繪,反之則應(yīng)返回false不需要重繪。
@override
bool shouldRepaint(MyPainter oldDelegate) {
if(oldDelegate.radius != radius){
return true;
}
return false;
}
}
組合
這種方式是通過拼裝其它低級別的Widget來組合成一個高級別的Widget,例如Container就是一個組合Widget,它是由DecoratedBox、ConstrainedBox、Transform、Padding、Align等組成。
Notification機制
內(nèi)容引用自:
[Notification]: https://book.flutterchina.club/chapter8/notification.html
Notification是Flutter中一個重要的機制,在Widget樹中,每一個節(jié)點都可以分發(fā)通知,通知會沿著當(dāng)前節(jié)點(context)向上傳遞,所有父節(jié)點都可以通過NotificationListener來監(jiān)聽通知,F(xiàn)lutter中稱這種通知由子向父的傳遞為“通知冒泡”(Notification Bubbling),這個和用戶觸摸事件冒泡是相似的,但有一點不同:通知冒泡可以中止,但用戶觸摸事件不行。
Flutter中很多地方使用了通知,如可滾動(Scrollable) Widget中滑動時就會分發(fā)ScrollNotification,而Scrollbar正是通過監(jiān)聽ScrollNotification來確定滾動條位置的。除了ScrollNotification,F(xiàn)lutter中還有SizeChangedLayoutNotification、KeepAliveNotification 、LayoutChangedNotification等。下面是一個監(jiān)聽Scrollable Widget滾動通知的例子:
NotificationListener(
onNotification: (notification){
//print(notification);
switch (notification.runtimeType){
case ScrollStartNotification: print("開始滾動"); break;
case ScrollUpdateNotification: print("正在滾動"); break;
case ScrollEndNotification: print("滾動停止"); break;
case OverscrollNotification: print("滾動到邊界"); break;
}
},
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
);
上例中的滾動通知如ScrollStartNotification、ScrollUpdateNotification等都是繼承自ScrollNotification類,不同類型的通知子類會包含不同的信息,比如ScrollUpdateNotification有一個scrollDelta屬性,它記錄了移動的位移,其它通知屬性讀者可以自己查看SDK文檔。
自定義通知
除了Flutter內(nèi)部通知,我們也可以自定義通知,下面我們看看如何實現(xiàn)自定義通知:
-
定義一個通知類,要繼承自Notification類;
class MyNotification extends Notification { MyNotification(this.msg); final String msg; } -
分發(fā)通知。
Notification有一個
dispatch(context)方法,它是用于分發(fā)通知的,我們說過context實際上就是操作Element的一個接口,它與Element樹上的節(jié)點是對應(yīng)的,通知會從context對應(yīng)的Element節(jié)點向上冒泡。
下面我們看一個完整的例子:
class NotificationRoute extends StatefulWidget {
@override
NotificationRouteState createState() {
return new NotificationRouteState();
}
}
class NotificationRouteState extends State<NotificationRoute> {
String _msg="";
@override
Widget build(BuildContext context) {
//監(jiān)聽通知
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(
//按鈕點擊時分發(fā)通知
onPressed: () => MyNotification("Hi").dispatch(context),
child: Text("Send Notification"),
);
},
),
Text(_msg)
],
),
),
);
}
}
class MyNotification extends Notification {
MyNotification(this.msg);
final String msg;
}
上面代碼中,我們每點一次按鈕就會分發(fā)一個MyNotification類型的通知,我們在Widget根上監(jiān)聽通知,收到通知后我們將通知通過Text顯示在屏幕上。
注意:代碼中注釋的部分是不能正常工作的,因為這個
context是根Context,而NotificationListener是監(jiān)聽的子樹,所以我們通過Builder來構(gòu)建RaisedButton,來獲得按鈕位置的context。