FlutterでフルスクラッチでWidgetを作る時に知っておきたいこと
Flutterで開発をしていると、ときおり標準Widgetやpuv.devに無い、独自な形状と機能を持つWidget(以下、カスタムウィジェット)を作りたくなります。
カスタムウィジェットは、次の2つを組み合わせて作ることが出来ます。
-
CustomPaint
(独自の形状を描画する) -
GestureDetector
(CustomPaint
に対する、タップやスクロール等のジェスチャを推定する)
CustomPaint
については、簡単にこちらに解説してます。
FlutterのCustomPaintで図形を描いて遊ぶ
この記事では、Flutterにおけるジェスチャ推定について解説し、また GestureDetector
+CustomPaint
を使って簡単なスライダーを作ります。
Flutterにおけるジェスチャ推定システム
Flutterのジェスチャ推定システムは、次2つのレイヤーからなります。
-
pointer events
(以下、Pointer
)- 画面上のポインタ(タッチ、マウス、スタイラス)の位置と動きを生でハンドリングする
-
gesture events
(以下、Gesture
)- 1つ以上の
Pointer
からなる意味的なジェスチャ(タップ、ドラッグ、スケール等)をハンドリングする
- 1つ以上の
公式にはPointer
よりも Gesture
でジェスチャ推定することが推奨されています。
Taps, drags, and other gestures - Flutter
Gesture
は、ドラッグ開始、ドラッグの最中、ドラッグ停止、といった各ジェスチャのライフサイクルに対応するイベントを、複数発行することが出来ます。
例えばタップイベントなら、次のような感じです。
onTapDown: タップイベントの可能性があるポインタが、画面に触れた
onTapUp: タップイベントのトリガーとなったポインタが、画面から離れた
タップイベント以外にも、水平方向のドラッグ、垂直方向のドラッグ、パン(任意の方向へタップしたまま移動)などをハンドリングできます。
GestureDetectorについて
Widgetにジェスチャ推定を追加する場合、GestureDetector
を使います。
GestureDetector
は、どのコールバック関数(onTapDown:
,onTapUp
等)が設定されている(非null
)かを元に、どのジェスチャを認識しようとするかを決定します。
複数コールバック関数が設定されている場合、gesture arenaに各ジェスチャ推定器を入れて、勝者を決定します。
例えば、画面に指が触れた時点では 水平ドラッグ推定器、垂直ドラッグ推定器がgesture arenaが入ります。(これらがコールバック関数に設定されていれば)
その後、ポインタが横に動いたら、水平ドラッグ推定器が勝利し、水平ドラッグイベントを検出、といった具合です。
作例(カスタムスライダー)
ここでは、CustomPaint
とGestureDetector
を使って、簡単なスライダーを作ってみます。
CustomPaint
については、こちらに解説してます。
FlutterのCustomPaintで図形を描いて遊ぶ
作成するのは、冒頭でもお見せしたこちらのスライダーです。
次のコードで、このスライダーを作ることが出来ます。
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
class MySliderPage extends StatelessWidget {
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: MySlider(),
),
);
}
}
class MySlider extends StatefulWidget {
const MySlider();
MySliderState createState() => MySliderState();
}
class MySliderState extends State<MySlider> {
Offset center = const Offset(0, 0); // ハンドラの位置
final size = const Size(200, 200); // 描画領域のサイズ
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
var handlerX = details.localPosition.dx;
handlerX = min(handlerX, size.width); // ハンドラが描画領域を超えないように調整
handlerX = max(0, handlerX); // ハンドラが描画領域を超えないように調整
setState(() {
center = Offset(handlerX, 0); // ハンドラの位置を更新
});
},
child: CustomPaint(
size: size,
painter: BarPaint(),
foregroundPainter: HandlerPaint(
center: center,
),
),
);
}
}
/// スライダーのハンドラ(青部分)を描画
class HandlerPaint extends CustomPainter {
HandlerPaint({
this.center,
});
final Offset center;
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.blue;
canvas.drawCircle(center, 10, paint);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; // ここがfalseだと、onPanUpdate:が発火してもハンドラの描画が更新されない
}
}
/// スライダーのバー(赤部分)を描画
class BarPaint extends CustomPainter {
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 5;
canvas.drawLine(
const Offset(0, 0),
Offset(size.width, 0),
paint,
);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
GestureDetector
にonPanUpdate
を設定しているので、描画領域を指がスライドするたびにonPanUpdate
が発火します。
onPanUpdate
中でsetState()
を呼び出すことで、ハンドラの位置を更新・再描画しています。
このように、GestureDetector
とCustomPaint
を組み合わせることで、カスタムウィジェットを作成することが出来ます。
参考文献
Building a Custom Slider in Flutter with GestureDetector | by RJS Tech | Medium
Taps, drags, and other gestures - Flutter
Discussion