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);
複製程式碼