前言
之前幾篇我們介紹了貝塞爾曲線的原理、繪制曲線和動效實(shí)現(xiàn),這些都是代碼預(yù)設(shè)好的绸硕,如果我們要根據(jù)需要自行繪制曲線,就需要使用交互來實(shí)現(xiàn)了魂毁。本篇我們先來介紹簡單的交互式繪圖玻佩,通過獲取觸控位置來設(shè)定貝塞爾曲線的控制點(diǎn),從而實(shí)現(xiàn)交互式繪制曲線席楚。
獲取觸控位置
第一個要解決的問題是如何獲取手指在屏幕的觸控位置咬崔。在 Flutter 中,提供了一個 Listener
組件烦秩,可以監(jiān)聽各類觸控事件垮斯。Listener
的組件構(gòu)造方法定義如下:
const Listener({
Key? key,
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerHover,
this.onPointerCancel,
this.onPointerSignal,
this.behavior = HitTestBehavior.deferToChild,
Widget? child,
})
其中onPointerXX
都是觸控事件的回調(diào)方法,在回調(diào)里會攜帶觸控的位置信息只祠,具體的參數(shù)如下:
-
onPointerDown
:觸控點(diǎn)按下去(類似鼠標(biāo)按下未釋放)時的回調(diào)方法兜蠕,攜帶了一個PointerDownEvent
參數(shù),該參數(shù)會包含點(diǎn)按信息抛寝,其中有一個localPosition
參數(shù)熊杨,即當(dāng)時點(diǎn)按時的位置。 -
onPointerUp
:觸控點(diǎn)按 松開(類似鼠標(biāo)按下后釋放)時的回調(diào)方法盗舰,攜帶了一個PointerUpEvent
參數(shù)晶府,該參數(shù)會包含點(diǎn)按信息,其中有一個localPosition
參數(shù)钻趋,即當(dāng)時點(diǎn)按時的位置川陆。 -
onPointerMove
:按下后移動(拖動)時的回調(diào)方法,攜帶了PointerMoveEvent
參數(shù)爷绘,包含了起始位置和移動的距離等信息书劝。 -
onPointerHover
:這個很好理解进倍,hover
狀態(tài)時的事件,不過在觸控設(shè)備上沒有hover
事件购对。 -
onPointerCancel
:產(chǎn)生onPointerDown
事件的指針(Pointer)不再指向該接收器(有點(diǎn)費(fèi)解猾昆,目前沒想到具體的應(yīng)用場景,后續(xù)遇到了再研究一下)骡苞。 -
onPointerSignal
:當(dāng)指針發(fā)出其他信息時的回調(diào)垂蜗,目前也沒想到具體的場景,可能是桌面應(yīng)用會涉及到解幽。 -
behavior
:這個參數(shù)需要注意贴见,也就是觸控事件的檢測方式,是一個HitTestBehavior
枚舉躲株,默認(rèn)是deferToChild
片部,只允許子組件響應(yīng)觸控事件。opaque
自身可以響應(yīng)觸控事件(子組件和自己都可以)霜定,但下層的組件無法響應(yīng)觸控事件档悠。translucent
為穿透類型,下層組件也能夠響應(yīng)到觸控事件望浩。我們可以根據(jù)需要來確定觸控事件的響應(yīng)方式辖所。
下面是點(diǎn)按后獲取觸控位置為例代碼,使用的話還是比較簡單的磨德。當(dāng)然缘回,這里要說明一下,如果只是單純地獲取觸控事件典挑,而不需要觸控的細(xì)節(jié)(比如位置酥宴,觸控力度等),那么官方推薦是使用更高封裝層級的手勢識別組件 GestureDetector
您觉。
Listener(
onPointerUp: ((event) {
print('position: ${event.localPosition}');
}),
behavior: HitTestBehavior.opaque,
child: ...
),
交互繪制實(shí)現(xiàn)
有了上面的基礎(chǔ)幅虑,我們獲取到觸控點(diǎn)位置后,刷新界面觸發(fā) Canvas 重繪即可顾犹。不過這里有個問題,我們希望是可以連續(xù)繪制褒墨,而不是只能繪制一條曲線炫刷。比如說我們點(diǎn)按了2個點(diǎn),那就繪制直線郁妈;3個點(diǎn)就繪制2階貝塞爾曲線浑玛;4個點(diǎn)繪制3階貝塞爾曲線;4個點(diǎn)以上噩咪,把前一條曲線的結(jié)束點(diǎn)當(dāng)做新的曲線的起點(diǎn)顾彰,按上述的方式循環(huán)繪制极阅。這樣我們就能夠保證曲線是連續(xù)的了。這里可以通過遞歸方式實(shí)現(xiàn)涨享,代碼如下:
void drawCurves(Canvas canvas, Paint paint, List<Offset> points) {
if (points.length <= 1) {
return;
}
if (points.length == 2) {
canvas.drawLine(points[0], points[1], paint);
return;
}
if (points.length == 3) {
_draw2OrderBezierCurves(canvas, paint, points);
return;
}
if (points.length == 4) {
_draw3OrderBezierCurves(canvas, paint, points);
return;
}
var subPoints = points.sublist(0, 4);
drawCurves(canvas, paint, subPoints);
drawCurves(canvas, paint, points.sublist(3));
}
此外筋搏,考慮可能繪制的不是我們想要的效果,要支持撤銷功能厕隧,因此我們加了一個按鈕奔脐,點(diǎn)擊按鈕可以刪除最近添加的點(diǎn),從而實(shí)現(xiàn)撤銷功能吁讨。我們用 Scaffold
的 floatingActionButton
實(shí)現(xiàn)髓迎,邏輯很簡單,就是如果點(diǎn)數(shù)組不為空的話建丧,就刪除最后一個點(diǎn)排龄,然后刷新界面就好了。
floatingActionButton: IconButton(
onPressed: () {
if (points.isNotEmpty) {
points.removeLast();
setState(() {});
}
},
icon: Icon(
Icons.backspace,
color: Colors.blue,
),
),
繪制代碼
有了上面的基礎(chǔ)翎朱,繪制代碼就簡單很多了橄维,我們用一個數(shù)組存儲已經(jīng)點(diǎn)擊的點(diǎn),然后通過這些點(diǎn)遞歸調(diào)用繪制方法就可以 實(shí)現(xiàn)交互式繪制了闭翩,完整代碼如下所示挣郭,這里我們把控制點(diǎn)使用圓圈繪制出來了。
class GestureBezierDemo extends StatefulWidget {
GestureBezierDemo({Key? key}) : super(key: key);
@override
State<GestureBezierDemo> createState() => _GestureBezierDemoState();
}
class _GestureBezierDemoState extends State<GestureBezierDemo> {
var points = <Offset>[];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Listener(
onPointerUp: ((event) {
points.add(event.localPosition);
setState(() {});
}),
behavior: HitTestBehavior.opaque,
child: CustomPaint(
foregroundPainter: GestureBezierPainter(points: points),
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Color(0xFFF5F5F5),
),
),
),
floatingActionButton: IconButton(
onPressed: () {
if (points.isNotEmpty) {
points.removeLast();
setState(() {});
}
},
icon: Icon(
Icons.backspace,
color: Colors.blue,
),
),
);
}
}
class GestureBezierPainter extends CustomPainter {
GestureBezierPainter({required this.points});
final List<Offset> points;
@override
void paint(Canvas canvas, Size size) {
print(size);
canvas.drawColor(Color(0xFFF1F1F1), BlendMode.color);
var paint = Paint()..color = Color(0xFFE53020);
paint.strokeWidth = 2.0;
paint.style = PaintingStyle.stroke;
for (var point in points) {
canvas.drawCircle(point, 2.0, paint);
}
paint.color = Color(0xFF2480F0);
drawCurves(canvas, paint, points);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
void drawCurves(Canvas canvas, Paint paint, List<Offset> points) {
if (points.length <= 1) {
return;
}
if (points.length == 2) {
canvas.drawLine(points[0], points[1], paint);
return;
}
if (points.length == 3) {
_draw2OrderBezierCurves(canvas, paint, points);
return;
}
if (points.length == 4) {
_draw3OrderBezierCurves(canvas, paint, points);
return;
}
var subPoints = points.sublist(0, 4);
drawCurves(canvas, paint, subPoints);
drawCurves(canvas, paint, points.sublist(3));
}
_draw3OrderBezierCurves(Canvas canvas, Paint paint, List<Offset> points) {
assert(points.length == 4);
var yGap = 60.0;
var path = Path();
path.moveTo(points[0].dx, points[0].dy);
for (var t = 1; t <= 100; t += 1) {
var curvePoint = BezierUtil.get3OrderBezierPoint(
points[0], points[1], points[2], points[3], t / 100.0);
path.lineTo(curvePoint.dx, curvePoint.dy);
}
canvas.drawPath(path, paint);
}
_draw2OrderBezierCurves(Canvas canvas, Paint paint, List<Offset> points) {
assert(points.length == 3);
var path = Path();
path.moveTo(points[0].dx, points[0].dy);
for (var t = 1; t <= 100; t += 1) {
var curvePoint = BezierUtil.get2OrderBezierPoint(
points[0], points[1], points[2], t / 100.0);
path.lineTo(curvePoint.dx, curvePoint.dy);
}
canvas.drawPath(path, paint);
}
}
運(yùn)行效果
運(yùn)行效果如下圖所示疗韵。
總結(jié)
本篇其實(shí)更多地是介紹獲取屏幕觸控位置的內(nèi)容兑障,要實(shí)現(xiàn)交互式繪制就需要掌握用戶的觸控行為,從而繪制對應(yīng)的圖案蕉汪。接下來我們會繼續(xù)探索更多交互行為流译,做更多的交互式繪制探索。