The CustomPainter class, as described in the previous section, is in the Bezier curve, but it is not difficult, its main function is to allow users to draw a variety of controls. This paper mainly records and explains the effect realization of wechat photo button, and its button effect is generally as follows:

rendering

Realization observation

From the renderings, the effect of this button is divided into two stages.

  • Stage 1: There are two circles, which I call the button circle (foreground circle) and the background circle. When the button is pressed long: the background circle becomes larger and the button circle becomes smaller. Here I set the radius change to 1.5 times, controlled by animation.

  • Stage 2: After the radius change is completed, draw a circular progress bar on the background circle, and the progress bar is controlled by animation.

Variable definition and initialization

We need three brushes to draw the background circle, the button circle, the circular progress bar, and the animation value to control the radius change and the animation value to control the progress bar.

  • define

  final double firstProgress; // first animation control value, value range [0,1]

  final double secondProgress; // second animation control value, value range [0,1]

  // The color of the main button
  final Color buttonColor = Colors.white;
  // Progress bar parameters
  final double progressWidth = 5; // Progress bar width
  final Color progressColor = Colors.green; // Progress bar color


  // The color behind the main button is also the background color for progress drawing
  Color progressBackgroundColor;

  // Background round brush
  Paint backGroundPaint;

  // Main button brush
  Paint btnPaint;

  // Progress bar brushes
  Paint progressPaint;

Copy the code
  • Initialize the
  WeChatShotVideoBtn(this.firstProgress, this.secondProgress) {
    progressBackgroundColor = buttonColor.withOpacity(0.7);

    // Initialize the brushbackGroundPaint = Paint() .. style = PaintingStyle.fill .. color = progressBackgroundColor; btnPaint = Paint() .. style = PaintingStyle.fill .. color = buttonColor; progressPaint = Paint() .. style = PaintingStyle.stroke .. color = progressColor .. strokeWidth = progressWidth; }Copy the code

draw

To draw a circle, we need to know the center of the circle and the radius. The center of the three circles is the same. We can draw a circle according to the previous analysis idea. The details are drawn as follows, with full comments.

 @override
  void paint(Canvas canvas, Size size) {
    // The initial radius of the circle is the radius of the circle before the animation starts
    final double initRadius = size.width * 0.5;

    // The largest circle at the bottom
    final double center = size.width * 0.5;
    / / circle
    final Offset circleCenter = Offset(center, center);

    // Set the radius of the background circle, so that the radius of the background circle changes with the animation control value, which becomes 1.5 times the radius of the button circle
    final double backGroundRadius = initRadius * (1 + (firstProgress / 2));
    // Draw the background circle
    canvas.drawCircle(circleCenter, backGroundRadius, backGroundPaint);

    // Button circle, the initial radius of the button circle should be subtracted from the width of the progress bar at the beginning, the radius of the button circle becomes smaller in the long time
    final double initBtnCircleRadius = initRadius - progressWidth;
    // In a long time, the radius of the button circle becomes 1/2 times of the original button circle according to the animation
    final double circleRadius = initBtnCircleRadius * (1 / (1 + firstProgress));
    // Draw the button circle
    canvas.drawCircle(circleCenter, circleRadius, btnPaint);

    // In the second stage, the progress bar is drawn, indicating that the second stage animation starts
    if (secondProgress > 0) {
      // The secondProgress value is converted to degrees
      final double angle = 360.0 * secondProgress;
      // Angle is converted to radians
      final double sweepAngle = deg2Rad(angle);

      final double progressCircleRadius = backGroundRadius - progressWidth;
      final Rect arcRect =
          Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
      // The default starting point for radians is 3 o 'clock
      // So the starting Angle here is adjusted 90 degrees forward so that it begins to draw an arc at 12 o 'clock
      canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint); }}Copy the code

Page control

It is important to note that we are a basic animation controller at ordinary times, so in class is SingleTickerProviderStateMixin, but there are two animation controller, so in class should be: TickerProviderStateMixin.

The next thing we need to do is control the animation controller and start the animation controller with a long press:

 _animationController2.forward();
Copy the code

Reset and restore the animation controller when unpressing:

   _animationController2.reverse();
   _animationController3.value = 0;
   _animationController3.stop();
Copy the code

To trigger these controls, we introduce gesture controls:

GestureDetector
Copy the code

The complete code

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) {
              // Start the progress bar animation for recording video after the button transition animation is complete_animationController3.forward(); }});// The second controller
    _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;
    // The center of the circle
    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));
    Draw the arc according to the Angle. If you look at the illustration later, you will find that the starting point is 0 and the starting point is 3 o 'clock
    canvas.drawArc(rect, startAngle, sweepAngle, true, _paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true; }}class WeChatShotVideoBtn extends CustomPainter {
  final double firstProgress; // first animation control value, value range [0,1]

  final double secondProgress; // second animation control value, value range [0,1]

  // The color of the main button
  final Color buttonColor = Colors.white;

  // Progress bar parameters
  final double progressWidth = 5; // Progress bar width
  final Color progressColor = Colors.green; // Progress bar color
  final back90 = deg2Rad(90.0); // Push forward 90 degrees from 12 o 'clock

  // The color behind the main button is also the background color for progress drawing
  Color progressBackgroundColor;

  // Background round brush
  Paint backGroundPaint;

  // Main button brush
  Paint btnPaint;

  // Progress bar brushes
  Paint progressPaint;

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

    // Initialize the brushbackGroundPaint = 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) {
    // The initial radius of the circle is the radius of the circle before the animation starts
    final double initRadius = size.width * 0.5;

    // The largest circle at the bottom
    final double center = size.width * 0.5;
    / / circle
    final Offset circleCenter = Offset(center, center);

    // Set the radius of the background circle, so that the radius of the background circle changes with the animation control value, which becomes 1.5 times the radius of the button circle
    final double backGroundRadius = initRadius * (1 + (firstProgress / 2));
    // Draw the background circle
    canvas.drawCircle(circleCenter, backGroundRadius, backGroundPaint);

    // Button circle, the initial radius of the button circle should be subtracted from the width of the progress bar at the beginning, the radius of the button circle becomes smaller in the long time
    final double initBtnCircleRadius = initRadius - progressWidth;
    // In a long time, the radius of the button circle becomes 1/2 times of the original button circle according to the animation
    final double circleRadius = initBtnCircleRadius * (1 / (1 + firstProgress));
    // Draw the button circle
    canvas.drawCircle(circleCenter, circleRadius, btnPaint);

    // In the second stage, the progress bar is drawn, indicating that the second stage animation starts
    if (secondProgress > 0) {
      // The secondProgress value is converted to degrees
      final double angle = 360.0 * secondProgress;
      // Angle is converted to radians
      final double sweepAngle = deg2Rad(angle);

      final double progressCircleRadius = backGroundRadius - progressWidth;
      final Rect arcRect =
          Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
      // The default starting point for radians is 3 o 'clock
      // So the starting Angle here is adjusted 90 degrees forward so that it begins to draw an arc at 12 o 'clock
      canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint); }}@override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true; }}// The Angle turns radians
num deg2Rad(num deg) => deg * (math.pi / 180.0);

Copy the code