Flutter自定义控件第二弹,一个动画对话框

上次写了一个折线图的教程,这次写了一个简单的带动画的控件,效果如下

知识点有:

  1. AnimatedBuilder
  2. AnimationController
  3. Tween
  4. Interval
  5. CustomPaint

让我们开始吧:

1. 首先,先编写里面那个圆圈+对勾的控件

我们最开始的代码是这样的:

import 'package:flutter/material.dart';

class _AnimationCircle extends CustomPainter {
  double value;

  @override
  void paint(Canvas canvas, Size size) {}

  @override
  bool shouldRepaint(_AnimationCircle oldDelegate) {
    return value != oldDelegate.value;
  }
}
复制代码

里面的value,是我们动画改变的时候的值,当值不一样的时候我们需要重绘,然后我们还需要定义3个坐标,分别是✔️️的3个点,坐标范围是0-1,这样我们可以通过乘以size来获得实际的坐标位置,然后_AnimationCircle类中添加下面3个属性:

Offset point1;
Offset point2;
Offset point3;
复制代码

然后我们的对勾,在控件的中心位置,我们可以根据下图大概确定这3个点的坐标。第一个点我们从边上开始,然后再移动到距离的一半,这样看起来比较漂亮

然后我们将这3个点的值定义为如下(凭感觉定义的值,就是这么自信。。。):

  static const Offset point1 = Offset(0.0, 0.5);
  static const Offset point2 = Offset(0.3, 0.8);
  static const Offset point3 = Offset(0.7, 0.3);
复制代码

然后还需要3个值,用来在显示动画的时候定义1点到2点之间的开始和结束坐标,2点到3点之间的结束坐标。

  Offset point1_2Start;
  Offset point1_2End;
  Offset point3End;
复制代码

然后还有圆形的弧度开始和结束值:

  double arcStart;
  double arcEnd;
复制代码

然后现在我们的代码是这个样子:

import 'package:flutter/material.dart';

class _AnimationCircle extends CustomPainter {
  static const Offset point1 = Offset(0.0, 0.5);
  static const Offset point2 = Offset(0.3, 0.8);
  static const Offset point3 = Offset(0.7, 0.3);
  double value;
  Offset point1_2Start;
  Offset point1_2End;
  Offset point3End;

  double point1_2StartValue;
  double point1_2EndValue;
  double point3EndValue;
  
  double arcStart;
  double arcEnd;

  @override
  void paint(Canvas canvas, Size size) {}

  @override
  bool shouldRepaint(_AnimationCircle oldDelegate) {
    return value != oldDelegate.value;
  }
}
复制代码

然后我们这个控件,有4个值需要传入,看动画能看出来,我们的对勾不是动画开始就开始画,而是等过了一段时间才开始画,所以我们添加如下构造函数:

  _AnimationCircle({
    @required this.value,
    @required this.point1_2StartValue,
    @required this.point1_2EndValue,
    @required this.point3EndValue,
  });
复制代码

第一个value,我们用来计算圆形的弧度,第二个值,用来计算1点到2点的起始坐标,第三个值,用来计算1点到2点的结束位置坐标,第四个值,用来计算第三个点的结束坐标,2点到3点之间的线段起始坐标一直是2点,所以这里只有结束坐标。

2. 然后我们开始下一个控件的定义,这个控件用来计算上述坐标给_AnimationCircle

class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
    with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
  AnimationController controller;

  @override
  void initState() {
    controller = AnimationController(vsync: this, lowerBound: 0, upperBound: 1);
    controller
      ..forward()
      ..addListener(() {
        setState(() { });
      });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _AnimationCircle(),
    );
  }
}
复制代码

上面的代码比较简单,就不多说了,然后关键部分在build方法里这些值的计算,从动画效果可以看出,圆形绘制完毕后对勾也绘制完了,并且弧形是先绘制出来的,然后才是1点到2点的动画,再然后1到2点和2点到3点的动画。

我们把动画时间分为4段,分别是0-0.25、0.25-0.5、0.5-0.75、0.75-1。

1点到2点的动画绘制时,需要让point1_2End先变大,point1_2End大于point1_2Start才可以绘制线段出来,所以我们在第三段动画时间里传递point1_2EndValue值,在point1_2StartValue没有传递之前point1_2StartValue都是0,所以绘制出来了1点到2点的线段。

在第四段动画时间里再传递point1_2StartValuepoint1_2EndValuepoint3EndValue,就完成了计算。

这部分逻辑稍微复杂一些,不明白的话可以多看几遍就懂了,然后代码如下:

class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
    with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
  AnimationController controller;

  @override
  void initState() {
    controller = AnimationController(vsync: this, lowerBound: 0, upperBound: 1);
    controller
      ..forward()
      ..addListener(() {
        setState(() { });
      });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _AnimationCircle(
        value: controller.value,
        // 在第四段时间结束后,point1_2Start的值就是1了
        point1_2StartValue: Interval(0.75, 1).transform(controller.value),
        // 在第三段时间结束后,point1_2End的值就是1了,
        // 在第三段时间刚结束的时候point1_2Start还是0,
        // 因为point1_2Start这个值在第四段时间才是非0
        point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
        // 在第四段时间结束后,point3End的值就是1了
        point3EndValue: Interval(0.75, 1).transform(controller.value),
      ),
    );
  }
}
复制代码

3. 然后我们修改_AnimationCircle类把动画控件显示出来,到目前为止的完整代码如下,我都注释了所以就不再解释了

import 'dart:math' as math;

import 'package:flutter/material.dart';

//author:liuhc
class _AnimationCircle extends CustomPainter {
  static const Offset point1 = Offset(0.0, 0.5);
  static const Offset point2 = Offset(0.3, 0.8);
  static const Offset point3 = Offset(0.7, 0.3);
  double value;
  Offset point1_2Start;
  Offset point1_2End;
  Offset point3End;

  double point1_2StartValue;
  double point1_2EndValue;
  double point3EndValue;

  double arcStart;
  double arcEnd;

  _AnimationCircle({
    @required this.value,
    @required this.point1_2StartValue,
    @required this.point1_2EndValue,
    @required this.point3EndValue,
  }) {
    //arcStart这样计算,看起来就是在转圈,否则只是单纯的绘制圆形,可以把下面的2去掉查看效果就明白了
    arcStart = math.pi + math.pi * value * 2;
    //math.pi * 2 的弧度是一个完整的圆形,这样动画结束的时候value是1,也就正好画出了一个圆形
    arcEnd = math.pi + math.pi * value;
    point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
    point1_2End = point1 + (point2 - point1) * point1_2EndValue;
    point3End = point2 + (point3 - point2) * point3EndValue;
  }

  @override
  void paint(Canvas canvas, Size size) {
    double minValue = math.min(size.width, size.height);
    Paint paint = Paint()
      //画笔颜色
      ..color = Colors.blue
      //边缘平滑
      ..strokeCap = StrokeCap.round
      //画笔宽度
      ..strokeWidth = 3
      //不填充
      ..style = PaintingStyle.stroke;
    //这个判断是因为在动画的第三段对勾才开始绘制,在之前point1_2Start和point1_2End都是point1
    //只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
    //之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
    if (point1_2Start != point1_2End) {
      canvas.drawLine(point1_2Start * minValue, point1_2End * minValue, paint);
    }
    //只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
    //之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
    if (point2 != point3End) {
      canvas.drawLine(point2 * minValue, point3End * minValue, paint);
    }
    canvas.drawArc(Rect.fromLTWH(0, 0, minValue, minValue), arcStart, arcEnd, false, paint);
  }

  @override
  bool shouldRepaint(_AnimationCircle oldDelegate) {
    return value != oldDelegate.value;
  }
}

class _AnimationCircleWidget extends StatefulWidget {
  @override
  _AnimationCircleWidgetState createState() => _AnimationCircleWidgetState();
}

class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
    with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
  AnimationController controller;

  @override
  void initState() {
    controller = AnimationController(vsync: this, duration: Duration(seconds: 2), lowerBound: 0, upperBound: 1);
    controller
      ..forward()
      ..addListener(() {
        setState(() {});
      });
    super.initState();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _AnimationCircle(
        value: controller.value,
        // 在第四段时间结束后,point1_2Start的值就是1了
        point1_2StartValue: Interval(0.75, 1).transform(controller.value),
        // 在第三段时间结束后,point1_2End的值就是1了,
        // 在第三段时间刚结束的时候point1_2Start还是0,
        // 因为point1_2Start这个值在第四段时间才是非0
        point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
        // 在第四段时间结束后,point3End的值就是1了
        point3EndValue: Interval(0.75, 1).transform(controller.value),
      ),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: Test(),
    ),
  );
}

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: RaisedButton(
          child: Text("测试"),
          onPressed: () {
            showDialog(
              barrierDismissible: false,
              context: context,
              builder: (context) {
                return Center(
                  child: Container(
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    ),
                    height: 120.0,
                    width: 120.0,
                    padding: EdgeInsets.all(30.0),
                    child: _AnimationCircleWidget(),
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }
}
复制代码

然后我们运行一下,发现是这样的

左边的对勾没了,这是因为我们在完整动画时间的第四段(0.75-1)里,下面2个point1_2Startpoint1_2End最后是相等的

    point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
    point1_2End = point1 + (point2 - point1) * point1_2EndValue;
复制代码

看第一张图可以看出来,对勾左边的线段是右边线段的一半,所以我们在传递值的地方改一下,我们将值的范围从0-1改为从0-0.5,我们改_AnimationCircleWidgetState的build方法

@override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _AnimationCircle(
        value: controller.value,
        // 注意,改动在这里,加了一个Tween,可以和上面的方法对比一下就知道区别了
        // 在第四段时间结束后,point1_2Start的值就是0.5了
        point1_2StartValue: Tween(begin: 0, end: 0.5).transform(Interval(0.75, 1).transform(controller.value)),
        // 在第三段时间结束后,point1_2End的值就是1了,
        // 在第三段时间刚结束的时候point1_2Start还是0,
        // 因为point1_2Start这个值在第四段时间才是非0
        point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
        // 在第四段时间结束后,point3End的值就是1了
        point3EndValue: Interval(0.75, 1).transform(controller.value),
      ),
    );
  }
复制代码

这样改动后,

    point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
    point1_2End = point1 + (point2 - point1) * point1_2EndValue;
复制代码

point1_2Start的值在动画结束后就是point1 + (point2 - point1) * 0.5,这样我们左边的对勾就显示出来了,并且是右边那条线段的一半,我们再运行一下,然后效果就是第一张图的效果了

4. 完整代码如下:

import 'dart:math' as math;

import 'package:flutter/material.dart';

/// 
/// 描述:动画演示
/// 作者:liuhc
class _AnimationCircle extends CustomPainter {
  static const Offset point1 = Offset(0.0, 0.5);
  static const Offset point2 = Offset(0.3, 0.8);
  static const Offset point3 = Offset(0.7, 0.3);
  double value;
  Offset point1_2Start;
  Offset point1_2End;
  Offset point3End;

  double point1_2StartValue;
  double point1_2EndValue;
  double point3EndValue;

  double arcStart;
  double arcEnd;

  _AnimationCircle({
    @required this.value,
    @required this.point1_2StartValue,
    @required this.point1_2EndValue,
    @required this.point3EndValue,
  }) {
    //arcStart这样计算,看起来就是在转圈,否则只是单纯的绘制圆形,可以把下面的2去掉查看效果就明白了
    arcStart = math.pi + math.pi * value * 2;
    //math.pi * 2 的弧度是一个完整的圆形,这样动画结束的时候value是1,也就正好画出了一个圆形
    arcEnd = math.pi + math.pi * value;
    point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
    point1_2End = point1 + (point2 - point1) * point1_2EndValue;
    point3End = point2 + (point3 - point2) * point3EndValue;
  }

  @override
  void paint(Canvas canvas, Size size) {
    double minValue = math.min(size.width, size.height);
    Paint paint = Paint()
      //画笔颜色
      ..color = Colors.blue
      //边缘平滑
      ..strokeCap = StrokeCap.round
      //画笔宽度
      ..strokeWidth = 3
      //不填充
      ..style = PaintingStyle.stroke;
    //这个判断是因为在动画的第三段对勾才开始绘制,在之前point1_2Start和point1_2End都是point1
    //只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
    //之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
    if (point1_2Start != point1_2End) {
      canvas.drawLine(point1_2Start * minValue, point1_2End * minValue, paint);
    }
    //只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
    //之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
    if (point2 != point3End) {
      canvas.drawLine(point2 * minValue, point3End * minValue, paint);
    }
    canvas.drawArc(Rect.fromLTWH(0, 0, minValue, minValue), arcStart, arcEnd, false, paint);
  }

  @override
  bool shouldRepaint(_AnimationCircle oldDelegate) {
    return value != oldDelegate.value;
  }
}

class _AnimationCircleWidget extends StatefulWidget {
  @override
  _AnimationCircleWidgetState createState() => _AnimationCircleWidgetState();
}

class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
    with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
  AnimationController controller;

  @override
  void initState() {
    controller = AnimationController(vsync: this, duration: Duration(seconds: 2), lowerBound: 0, upperBound: 1);
    controller
      ..forward()
      ..addListener(() {
        setState(() {});
      });
    super.initState();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _AnimationCircle(
        value: controller.value,
        // 在第四段时间结束后,point1_2Start的值就是0.5了
        point1_2StartValue: Interval(0.75, 1.0).transform(controller.value),
        // 在第三段时间结束后,point1_2End的值就是1了,
        // 在第三段时间刚结束的时候point1_2Start还是0,
        // 因为point1_2Start这个值在第四段时间才是非0
        point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
        // 在第四段时间结束后,point3End的值就是1了
        point3EndValue: Interval(0.75, 1.0).transform(controller.value),
      ),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: Test(),
    ),
  );
}

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("动画演示"),
      ),
      body: Center(
        child: RaisedButton(
          child: Text("测试"),
          onPressed: () {
            showDialog(
              barrierDismissible: false,
              context: context,
              builder: (context) {
                return Center(
                  child: Container(
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    ),
                    height: 120.0,
                    width: 120.0,
                    padding: EdgeInsets.all(30.0),
                    child: _AnimationCircleWidget(),
                  ),
                );
              },
            );
          },
        ),
      ),
    );
  }
}
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值