🦑
【Flutter】TextをPathに追従させて動かす
今回やること
タイトルの通り Text を Path に追従させて動かします。
Path を定義する
今回は適当に S 字の Path を定義します。
final followPath = Path();
followPath.moveTo(size.width / 2, 0);
followPath.cubicTo(size.width, 0, size.width, size.height / 2, size.width / 2, size.height / 2);
followPath.cubicTo(0, size.height / 2, 0, size.height, size.width / 2, size.height);
Path から Offset(Position)を取得する
computeMetrics()を使用し、Path 情報(PathMetric)を取得し Path の長さや距離に対する position を取得します。
progress はアニメーションで 0.0~1.0 で動かします。
Offset? getOffset() {
List<PathMetric> pathMetrics = followPath.computeMetrics().toList();
double pathLength = pathMetrics.first.length;
final distance = pathLength * progress;
final Tangent? tangent = pathMetrics.first.getTangentForOffset(distance);
return tangent?.position;
}
CustomPainter で Text を描画する
Offset を親へ返して親側で Text を描画することも出来ますが、
今回は CustomPainter を使って Text を描画します。
void paint(Canvas canvas, Size size) {
final followPath = Path();
followPath.moveTo(size.width / 2, 0);
followPath.cubicTo(size.width, 0, size.width, size.height / 2, size.width / 2, size.height / 2);
followPath.cubicTo(0, size.height / 2, 0, size.height, size.width / 2, size.height);
final textPainter = TextPainter(
text: TextSpan(
text: 'move',
style: const TextStyle(fontSize: 24, color: Colors.black),
),
textDirection: TextDirection.ltr,
);
Offset? getOffset() {
List<PathMetric> pathMetrics = followPath.computeMetrics().toList();
double pathLength = pathMetrics.first.length;
final distance = pathLength * progress;
final Tangent? tangent = pathMetrics.first.getTangentForOffset(distance);
return tangent?.position;
}
final offset = getOffset() ?? Offset.zero;
textPainter.layout();
textPainter.paint(canvas, offset);
}
AnimationController を使って Text を動かす
あとは AnimationController を定義し、その値を使い progress の値を変化させて
Text を動かします。
コードは下記の全文を参照してください。
終わりに
今回は Text を動かしてみましたが、Path から Offset を取得するところが
ポイントなので Widget なども Path に追従させることも可能です。
是非遊んでみてください。
コード全文
import 'dart:ui';
import 'package:flutter/material.dart';
class FollowPathPage extends StatefulWidget {
const FollowPathPage({super.key});
State<FollowPathPage> createState() => _FollowPathPageState();
}
class _FollowPathPageState extends State<FollowPathPage> with SingleTickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) => CustomPaint(
painter: TextOnPathPainter(progress: _controller.value),
size: const Size(200, 200),
)),
ElevatedButton(
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else {
_controller.repeat(reverse: true);
}
},
child: const Text('Start/Stop'),
),
],
),
),
);
}
}
class TextOnPathPainter extends CustomPainter {
final double progress;
TextOnPathPainter({
required this.progress,
required this.text,
});
void paint(Canvas canvas, Size size) {
final followPath = Path();
followPath.moveTo(size.width / 2, 0);
followPath.cubicTo(size.width, 0, size.width, size.height / 2, size.width / 2, size.height / 2);
followPath.cubicTo(0, size.height / 2, 0, size.height, size.width / 2, size.height);
final pathPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1;
final textPainter = TextPainter(
text: TextSpan(
text: 'move',
style: const TextStyle(fontSize: 24, color: Colors.black),
),
textDirection: TextDirection.ltr,
);
canvas.drawPath(followPath, pathPaint);
Offset? getOffset() {
List<PathMetric> pathMetrics = followPath.computeMetrics().toList();
double pathLength = pathMetrics.first.length;
final distance = pathLength * progress;
final Tangent? tangent = pathMetrics.first.getTangentForOffset(distance);
return tangent?.position;
}
final offset = getOffset() ?? Offset.zero;
textPainter.layout();
textPainter.paint(canvas, offset);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Discussion