flutter实现沿圆角矩形边框的高光运动

2023-11-30  本文已影响0人  非新生代菜鸟

基本思路

1、创建圆角矩形,高光模型(小球),使用Stack来定义高光与矩形的相对位置关系;
2、利用AnimatedBuilder创建动画,通过_controller.addListener来更新高光位置坐标,从而实现动画效果。
3、拆分运动行程,四条直线和四个弧度,dart中坐标系原点为左上角(0,0),顺时针方向从除圆角外直线向右开始行程。在案例中,行程分别对应line1、arc1(右上角)P、line2、arc2(右下角)、line3、arc3(左下角)、line4、arc4(左上角)。

实现方法

1、定义变量:

  late AnimationController _controller;
  late Animation<double> _animation;
  double w = 400; #矩形宽
  double h = 300; #矩形高
  double r = 30; #圆角半径
  late double totalLength; #圆角矩形周长
  double ballR = 2;  #高光小球半径
  double x = 0; #高光小球x坐标
  double y = 0; #高光小球y坐标

2、初始化:

@override
  void initState() {
    super.initState();
    totalLength = (w + h) * 2 - 8 * r + 4 * r * pi / 2;
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 10000),
    )..repeat();
    _animation = Tween<double>(begin: 0, end: totalLength).animate(_controller);
    // 初始化 x 和 y 的值 运动起点(r,0)
    x = r;
    y = 0;
    _controller.addListener(() {
      // 更新 x 和 y 的值
      updatePosition();
    });
  }

3、路径坐标算法:(圆角弧度的theta角计算:使用运动距离的增量除以半径r):

delta _animation = _animation.value - 已走过的行程;
theta t = delta _animation / r;
x = r * cos(t);
y = r * sin(t);

void updatePosition() {

      // 定义行程
      double line1 = w - 2 * r;
      double line2 = h - 2 * r;
      double line3 = w - 2 * r;
      double line4 = h - 2 * r;
      double arc1 = r * pi / 2;
      double arc2 = r * pi / 2;
      double arc3 = r * pi / 2;
      double arc4 = r * pi / 2;

    setState(() {
      // 在这里更新 x 和 y 的值
      if (_animation.value <= line1) {
        // 第1段,向右直线 line1 从坐标(r,0)开始,向右走直线,距离为 w-2*r
        x = r + _animation.value;
        y = 0;
      } else if (_animation.value > line1 && _animation.value <= (line1 + arc1)) {
        // 第2段,右上圆角 arc1
        double t = (_animation.value - line1) / r - pi / 2;
        x = (w - r) + r * cos(t);
        y = r + r * sin(t);
      } else if (_animation.value > (line1 + arc1) && (_animation.value <= (line1 + arc1 + line2))) {
        // 第3段,下行直线 line2
        x = w;
        y = _animation.value - (line1 + arc1) + r;
      } else if (_animation.value > (line1 + arc1 + line2) && _animation.value <= (line1 + arc1 + line2 + arc2)) {
        // 第4段,右下圆角 arc2
        double t = (_animation.value - (line1 + arc1 + line2)) / r;
        x = (w - r) + r * cos(t);
        y = (h - r) + r * sin(t);
      } else if (_animation.value > (line1 + arc1 + line2 + arc2) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3)) {
        // 第5段,向左直线 line3
        y = h;
        x = (w - r) - (_animation.value - (line1 + arc1 + line2 + arc2));
      } else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3)) {
        // 第6段,左下圆角 arc3
        double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3)) / r - 3 * pi / 2;
        x = r + r * cos(t);
        y = (h - r) + r * sin(t);
      } else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) {
        // 第7段,上行直线 line4
        x = 0;
        y = (h - r) - (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3));
      } else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4) && _animation.value <= totalLength) {
        // 第8段,左上圆角 arc4
        double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) / r - pi;
        x = r + r * cos(t);
        y = r + r * sin(t);
      }
    });
  }

4、相关数学公式:

# 在圆的极坐标系中,弧度R、半径 radius 和角度 theta 之间的关系可以通过以下公式表示:
R = theta ×  radius
# 这个公式表明,在圆上,给定半径和角度,弧度可以通过将角度除以半径来计算。
# 从弧度转换为角度,可以使用以下公式:
theta = R / radius
# 使用 theta 计算 x 和 y 坐标:
x = r * cos(theta);
y = r * sin(theta);
# 最后注意要相对dart直角坐标原点做坐标平移转换:
x = (平移量)+ r * cos(theta);
y = (平移量)+ r * sin(theta);

完整代码

import 'dart:math';
import 'package:flutter/material.dart';

class TestPage extends StatefulWidget {
  const TestPage({Key? key}) : super(key: key);

  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Animation Example',
          style: TextStyle(fontSize: 16, color: Colors.red),
        ),
      ),
      body: const Center(
        child: MyCustomRectangle(),
      ),
    );
  }
}

class MyCustomRectangle extends StatefulWidget {
  const MyCustomRectangle({super.key});

  @override
  _MyCustomRectangleState createState() => _MyCustomRectangleState();
}

class _MyCustomRectangleState extends State<MyCustomRectangle> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  double w = 400;
  double h = 300;
  double r = 30;
  late double totalLength;
  double ballR = 2;
  double x = 0;
  double y = 0;

  @override
  void initState() {
    super.initState();
    totalLength = (w + h) * 2 - 8 * r + 4 * r * pi / 2;
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 10000),
    )..repeat();
    _animation = Tween<double>(begin: 0, end: totalLength).animate(_controller);

    // 初始化 x 和 y 的值 运动起点(r,0)
    x = r;
    y = 0;

    _controller.addListener(() {
      // 更新 x 和 y 的值
      updatePosition();
    });
  }

  void updatePosition() {
    setState(() {
      // 在这里更新 x 和 y 的值
      // 定义行程
      double line1 = w - 2 * r;
      double line2 = h - 2 * r;
      double line3 = w - 2 * r;
      double line4 = h - 2 * r;
      double arc1 = r * pi / 2;
      double arc2 = r * pi / 2;
      double arc3 = r * pi / 2;
      double arc4 = r * pi / 2;
      if (_animation.value <= line1) {
        // 第1段,向右直线 line1 从坐标(r,0)开始,向右走直线,距离为 w-2*r
        x = r + _animation.value % line1;
        y = 0;
      } else if (_animation.value > line1 && _animation.value <= (line1 + arc1)) {
        // 第2段,右上圆角 arc1
        double t = (_animation.value - line1) / r - pi / 2;
        x = (w - r) + r * cos(t);
        y = r + r * sin(t);
      } else if (_animation.value > (line1 + arc1) && (_animation.value <= (line1 + arc1 + line2))) {
        // 第3段,下行直线 line2
        x = w;
        y = _animation.value - (line1 + arc1) + r;
      } else if (_animation.value > (line1 + arc1 + line2) && _animation.value <= (line1 + arc1 + line2 + arc2)) {
        // 第4段,右下圆角 arc2
        double t = (_animation.value - (line1 + arc1 + line2)) / r;
        x = (w - r) + r * cos(t);
        y = (h - r) + r * sin(t);
      } else if (_animation.value > (line1 + arc1 + line2 + arc2) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3)) {
        // 第5段,向左直线 line3
        y = h;
        x = (w - r) - (_animation.value - (line1 + arc1 + line2 + arc2));
      } else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3)) {
        // 第6段,左下圆角 arc3
        double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3)) / r - 3 * pi / 2;
        x = r + r * cos(t);
        y = (h - r) + r * sin(t);
      } else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) {
        // 第7段,上行直线 line4
        x = 0;
        y = (h - r) - (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3));
      } else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4) && _animation.value <= totalLength) {
        // 第8段,左上圆角 arc4
        double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) / r - pi;
        x = r + r * cos(t);
        y = r + r * sin(t);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: w + 2 * ballR,
      height: h + 2 * ballR,
      child: Stack(
        children: [
          Positioned(
            left: ballR,
            top: ballR,
            child: Container(
              width: w,
              height: h,
              decoration: BoxDecoration(
                  color: Colors.yellow,
                  border: Border.all(
                    color: Colors.blue,
                    width: 2,
                  ),
                  borderRadius: BorderRadius.circular(r)),
            ),
          ),
          // Moving dot along the border
          AnimatedBuilder(
            animation: _animation,
            builder: (context, child) {
              return Positioned(
                left: x,
                top: y,
                child: Container(
                  width: ballR * 2,
                  height: ballR * 2,
                  decoration: const BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.purple,
                  ),
                ),
              );
            },
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
上一篇 下一篇

猜你喜欢

热点阅读