FlutterFlutter圈子All in Flutter

CustomPainter——微信拍视频按钮效果实现

2020-04-18  本文已影响0人  向日花开

CustomPainter 这个类前文讲过,就在贝塞尔曲线那块,不了解的可以爬楼,但这个类不难,其主要功能就是提供用户绘制各种各样的控件。
本文主要记录讲解微信拍照按钮的效果实现,其按钮效果大体如下:

效果图

实现思路观察

由效果图可知,这个按钮效果分两个阶段。

变量定义与初始化

我们需要三只画笔,分别画背景圆,按钮圆,圆形进度条,以及控制半径变化的动画值以及控制进度条的动画值。


  final double firstProgress; //第一段动画控制值,值范围[0,1]

  final double secondProgress; //第二段动画控制值,值范围[0,1]

  //主按钮的颜色
  final Color buttonColor = Colors.white;
  //进度条相关参数
  final double progressWidth = 5; //进度条 宽度
  final Color progressColor = Colors.green; //进度条颜色


  //主按钮背后一层的颜色,也是progress绘制时的背景色
  Color progressBackgroundColor;

  //背景圆的画笔
  Paint backGroundPaint;

  //主按钮画笔
  Paint btnPaint;

  //进度条画笔
  Paint progressPaint;

  WeChatShotVideoBtn(this.firstProgress, this.secondProgress) {
    progressBackgroundColor = buttonColor.withOpacity(0.7);

    //初始化画笔
    backGroundPaint = Paint()
      ..style = PaintingStyle.fill
      ..color = progressBackgroundColor;

    btnPaint = Paint()
      ..style = PaintingStyle.fill
      ..color = buttonColor;

    progressPaint = Paint()
      ..style = PaintingStyle.stroke
      ..color = progressColor
      ..strokeWidth = progressWidth;
  }

绘制

画圆需要知道圆心和半径,这三个圆的圆心都是同一个,我们根据之前的分析思路画圆即可,根据第一阶段传入的动画值控制半径不断的画圆,当第一阶段动画结束后,根据第二阶段的动画值画进度条即可。具体绘制如下,注释很全。

 @override
  void paint(Canvas canvas, Size size) {
    //初始化的圆半径,就是在动画开始前的圆半径
    final double initRadius = size.width * 0.5;

    // 底部最大的圆
    final double center = size.width * 0.5;
    //圆心
    final Offset circleCenter = Offset(center, center);

    //设置背景圆半径,让背景圆的半径随着动画控制值的变化,此处变为按钮圆半径的1.5倍
    final double backGroundRadius = initRadius * (1 + (firstProgress / 2));
    //画背景圆
    canvas.drawCircle(circleCenter, backGroundRadius, backGroundPaint);

    // 按钮圆,按钮圆初始半径刚开始时应减去 进度条的宽度,在长按时按钮圆半径变小
    final double initBtnCircleRadius = initRadius - progressWidth;
    //长按时,按钮圆半径根据动画变为初始按钮圆的1/2倍
    final double circleRadius = initBtnCircleRadius * (1 / (1 + firstProgress));
    //画按钮圆
    canvas.drawCircle(circleCenter, circleRadius, btnPaint);

    // 第二阶段,进度条的绘制,表示第二阶段动画启动
    if (secondProgress > 0) {
      //secondProgress 值转化为度数
      final double angle = 360.0 * secondProgress;
      //角度转化为弧度
      final double sweepAngle = deg2Rad(angle);

      final double progressCircleRadius = backGroundRadius - progressWidth;
      final Rect arcRect =
          Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
      //这里画弧度的时候它默认起点是从3点钟方向开始
      // 所以这里的开始角度向前调整90度让它从12点钟方向开始画弧
      canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint);
    }
  }


页面控制

值得注意的是,平时我们基本都是一个动画控制器,所以混入的类是 SingleTickerProviderStateMixin,但这里有两个动画控制器,所以混入的类应该是:TickerProviderStateMixin。

接下来我们要做的就是控制动画控制器,在长按的时候启动动画控制器:

 _animationController2.forward();

在取消长按的时候重置以及还原动画控制器:

   _animationController2.reverse();
   _animationController3.value = 0;
   _animationController3.stop();

为触发这些控制,我们引入手势控件:

GestureDetector

插波广告,有兴趣的小伙伴关注公众号,和我一起学习,本文代码在文末。


完整代码

import 'dart:ui';

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

class PainterPageFirst extends StatefulWidget  {
  @override
  _PainterPageFirstState createState() => _PainterPageFirstState();
}

class _PainterPageFirstState extends State<PainterPageFirst>
    with TickerProviderStateMixin {
  AnimationController _animationController1;

  AnimationController _animationController2;

  AnimationController _animationController3;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _animationController1 =
    AnimationController(duration: Duration(seconds: 2), vsync: this)
      ..addListener(() {
        setState(() {});
      })
      ..repeat();

    _animationController2 =
        AnimationController(duration: Duration(milliseconds: 500), vsync: this)
          ..addListener(() {
            setState(() {});
          })
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              //按钮过渡动画完成后启动录制视频的进度条动画
              _animationController3.forward();
            }
          });

    //第二个控制器
    _animationController3 =
        AnimationController(duration: Duration(seconds: 8), vsync: this)
          ..addListener(() {
            setState(() {});
          });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Painter&Animation"),
      ),
      body: Container(
        margin: EdgeInsets.only(top: 10),
        alignment: Alignment.center,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Container(
              width: 200,
              height: 200,
              color: Colors.red,
              alignment: Alignment.center,
              child: CustomPaint(
                painter: CirclePainter1(progress: _animationController1.value),
                size: Size(150, 150),
              ),
            ),
            Container(
              margin: EdgeInsets.only(top: 20),
              width: 200,
              height: 200,
              color: Colors.black,
              alignment: Alignment.center,
              child: GestureDetector(
                onLongPress: () {
                  _animationController2.forward();
                },
                onLongPressUp: () {
                  _animationController2.reverse();
                  _animationController3.value = 0;
                  _animationController3.stop();
                },
                child: CustomPaint(
                  painter: WeChatShotVideoBtn(
                      _animationController2.value, _animationController3.value),
                  size: Size(100, 100),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    _animationController1?.dispose();
    _animationController3?.dispose();
    _animationController2?.dispose();
    super.dispose();
  }
}

class CirclePainter1 extends CustomPainter {
  Paint _paint = Paint()
    ..style = PaintingStyle.fill
    ..color = Colors.greenAccent;

  final double progress;

  CirclePainter1({this.progress});

  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint

    final double center = size.width * 0.5;
    final double radius = size.width * 0.5;
    // 圆的中心点位置
    final Offset centerOffset = Offset(center, center);

    final Rect rect = Rect.fromCircle(center: centerOffset, radius: radius);
    final double startAngle = 0;
    final double angle = 360.0 * progress;
    final double sweepAngle = (angle * (math.pi / 180.0));
    // 画圆弧 按照角度来画圆弧,后面看效果图会发现起点从0开始画的时候是3点钟方向开始的
    canvas.drawArc(rect, startAngle, sweepAngle, true, _paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }
}

class WeChatShotVideoBtn extends CustomPainter {
  final double firstProgress; //第一段动画控制值,值范围[0,1]

  final double secondProgress; //第二段动画控制值,值范围[0,1]

  //主按钮的颜色
  final Color buttonColor = Colors.white;

  //进度条相关参数
  final double progressWidth = 5; //进度条 宽度
  final Color progressColor = Colors.green; //进度条颜色
  final back90 = deg2Rad(-90.0); //往前推90度 从12点钟方向开始

  //主按钮背后一层的颜色,也是progress绘制时的背景色
  Color progressBackgroundColor;

  //背景圆的画笔
  Paint backGroundPaint;

  //主按钮画笔
  Paint btnPaint;

  //进度条画笔
  Paint progressPaint;

  WeChatShotVideoBtn(this.firstProgress, this.secondProgress) {
    progressBackgroundColor = buttonColor.withOpacity(0.7);

    //初始化画笔
    backGroundPaint = Paint()
      ..style = PaintingStyle.fill
      ..color = progressBackgroundColor;

    btnPaint = Paint()
      ..style = PaintingStyle.fill
      ..color = buttonColor;

    progressPaint = Paint()
      ..style = PaintingStyle.stroke
      ..color = progressColor
      ..strokeWidth = progressWidth;
  }

  @override
  void paint(Canvas canvas, Size size) {
    //初始化的圆半径,就是在动画开始前的圆半径
    final double initRadius = size.width * 0.5;

    // 底部最大的圆
    final double center = size.width * 0.5;
    //圆心
    final Offset circleCenter = Offset(center, center);

    //设置背景圆半径,让背景圆的半径随着动画控制值的变化,此处变为按钮圆半径的1.5倍
    final double backGroundRadius = initRadius * (1 + (firstProgress / 2));
    //画背景圆
    canvas.drawCircle(circleCenter, backGroundRadius, backGroundPaint);

    // 按钮圆,按钮圆初始半径刚开始时应减去 进度条的宽度,在长按时按钮圆半径变小
    final double initBtnCircleRadius = initRadius - progressWidth;
    //长按时,按钮圆半径根据动画变为初始按钮圆的1/2倍
    final double circleRadius = initBtnCircleRadius * (1 / (1 + firstProgress));
    //画按钮圆
    canvas.drawCircle(circleCenter, circleRadius, btnPaint);

    // 第二阶段,进度条的绘制,表示第二阶段动画启动
    if (secondProgress > 0) {
      //secondProgress 值转化为度数
      final double angle = 360.0 * secondProgress;
      //角度转化为弧度
      final double sweepAngle = deg2Rad(angle);

      final double progressCircleRadius = backGroundRadius - progressWidth;
      final Rect arcRect =
          Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
      //这里画弧度的时候它默认起点是从3点钟方向开始
      // 所以这里的开始角度向前调整90度让它从12点钟方向开始画弧
      canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

//角度转弧度
num deg2Rad(num deg) => deg * (math.pi / 180.0);

上一篇下一篇

猜你喜欢

热点阅读