CustomPainter——微信拍視訊按鈕效果實現

HuangSir發表於2020-04-21

CustomPainter 這個類前文講過,就在貝塞爾曲線那塊,不瞭解的可以爬樓,但這個類不難,其主要功能就是提供使用者繪製各種各樣的控制元件。 本文主要記錄講解微信拍照按鈕的效果實現,其按鈕效果大體如下:

效果圖

CustomPainter——微信拍視訊按鈕效果實現

實現思路觀察

由效果圖可知,這個按鈕效果分兩個階段。

  • 第一階段:有兩個圓,我稱之為按鈕圓(前景圓),背景圓。當長按按鈕時:背景圓變大,按鈕圓變小。此處我設定半徑變化倍率為 1.5 倍,由動畫控制。

  • 第二階段:當半徑變化完成之後,在背景圓上畫圓形進度條,進度條進度通過動畫控制。

變數定義與初始化

我們需要三隻畫筆,分別畫背景圓,按鈕圓,圓形進度條,以及控制半徑變化的動畫值以及控制進度條的動畫值。

  • 定義

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

複製程式碼

相關文章