🦑

【Flutter】TextをPathに追従させて動かす

2024/04/14に公開

今回やること

タイトルの通り 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);

実行例

https://qiita.com/ling350181/items/745c0c6c04c4037298de

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;
}

https://api.flutter.dev/flutter/dart-ui/Path-class.html
https://api.flutter.dev/flutter/dart-ui/PathMetric-class.html
https://api.flutter.dev/flutter/dart-ui/Tangent-class.html

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