Zero: Brief description of the effect of this paper

This article introduces the use of Flutter drawing and Flutter animation through a small case study. Here is a colorful circle with two animated effects:

  • [1].Some surrounding halo will occurDiffusion and contraction animation.
  • [2]There is an outer circle of the circletimeIt goes around the circle.


First, static effect rendering

1. Drawing of the outer ring

The following defines a CircleHalo component to display the content drawn by the CircleHaloPainter artboard. Since you will be animating later, use this definition as a StatefulWidget.

class CircleHalo extends StatefulWidget {
  const CircleHalo({Key key}) : super(key: key);

  @override
  _CircleHaloState createState() => _CircleHaloState();
}

class _CircleHaloState extends State<CircleHalo> {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(200.200), painter: CircleHaloPainter(), ); }}Copy the code

So let me draw a circle, and this shouldn’t be too hard for you to do.

class CircleHaloPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    
    finalPaint paint = Paint() .. style = PaintingStyle.stroke .. strokeWidth =1;

    final Path circlePath = Path();
    circlePath.addOval(
        Rect.fromCenter(center: Offset(0.0), width: 100, height: 100));

    canvas.drawPath(circlePath, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Copy the code

Blur can be used to blur the brush, and the blurstyle. solid mode will create a blurred shadow around the brush when it is drawn. The second parameter determines the degree of fuzziness, and the following are the effects of fuzziness of 2, 4 and 6 respectively.

Sigma: 2 Sigma: 4 6 sigma:
finalPaint paint = Paint() .. style = PaintingStyle.stroke .. strokeWidth =1;

/ / set the maskFilter
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);
Copy the code

The next step is to set the color brush. You can set the shader of the Paint object using the shader property. Here is a colorful scan gradient.

import 'dart:ui' as ui ;

List<Color> colors = [
  Color(0xFFF60C0C),
  Color(0xFFF3B913),
  Color(0xFFE7F716),
  Color(0xFF3DF30B),
  Color(0xFF0DF6EF),
  Color(0xFF0829FB),
  Color(0xFFB709F4)]; colors.addAll(colors.reversed.toList());final List<double> pos = List.generate(colors.length, (index) => index / colors.length);
/ / set the shader
paint.shader =
    ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0.2 * pi);
/ / set the maskFilter
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);
Copy the code

At this point, we are about a quarter of the way through, and the halo spread and shrink animation is really just changing the sigma of the blur mask on the fly.


2. Static effect of outer ring streamer

The static effect of the outer circle rotation is shown below as a crescent-shaped arc on the far left. It can be obtained by combining two circular paths through difference. The centers of the two circles have a slight offset in the transverse direction. The larger the offset is, the fatter the crescent is.

class CircleHaloPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    finalPaint paint = Paint() .. style = PaintingStyle.stroke; paint.maskFilter = MaskFilter.blur(BlurStyle.solid,4);
    
		1 / / path
    finalPath circlePath = Path().. addOval( Rect.fromCenter(center: Offset(0.0), width: 100, height: 100));
    2 / / pathPath circlePath2 = Path().. addOval( Rect.fromCenter(center: Offset(- 1.0), width: 100, height: 100));
		// Join paths
    Path result = Path.combine(PathOperation.difference, circlePath, circlePath2);
    // Color fillpaint.. style = PaintingStyle.fill.. color = Color(0xff00abf2);
    canvas.drawPath(result, paint); / / to draw
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Copy the code

Second, the outer circle of animation

First, the following halo diffusion and contraction animation is implemented. The animation cycle is 2s, and the execution is repeated.


1. Status class processing

To handle the animation yourself, create the animation controller first. Because the animation controller structure requires a TickerProvider into, can let _CircleHaloState mixed with SingleTickerProviderStateMixin make state class itself TickerProvider implementation class. As follows, a 2s animator is created in initState and repeated through the repeat method. The animator is used as an input parameter when constructing the CircleHaloPainter.

class _CircleHaloState extends State<CircleHalo> with SingleTickerProviderStateMixin {
  
  AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2)); _ctrl.repeat(); }@override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return  CustomPaint(
        size: Size(200.200), painter: CircleHaloPainter(_ctrl), ); }}Copy the code

2. Animation processing of the drawing board

Our goal is to make the second parameter of maskfilter. blur change with the animator to achieve the animation effect. CustomPainter subclass constructs can associate Listenable objects with super(repaint: Visible Listening object). Since the animator Animation is a subclass of Listenable, it associates the animator so that when the animator value changes, it tells the artboard to redraw.

In the following processing, the more important point is to define a Tween that changes back and forth through the TweenSequence. For example, the animation length is 2s, and it varies from 0 to 4 in the first second and from 4 to 0 in the second. In this way, the value can be achieved once and for all in an animation cycle. In addition, passing in a CurveTween object through the chain method increases the animation curve effect by making the current value of the Animatable change.

class CircleHaloPainter extends CustomPainter {
  Animation<double> animation;

  CircleHaloPainter(this.animation) : super(repaint: animation);

  final Animatable<double> breatheTween = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 0, end: 4),
        weight: 1,
      ),
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 4, end: 0),
        weight: 1,
      ),
    ],
  ).chain(CurveTween(curve: Curves.decelerate));

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    finalPaint paint = Paint() .. style = PaintingStyle.stroke .. strokeWidth =1; Path circlePath = Path() .. addOval(Rect.fromCenter(center: Offset(0.0), width: 100, height: 100));

    List<Color> colors = [
      Color(0xFFF60C0C), Color(0xFFF3B913), Color(0xFFE7F716), 
      Color(0xFF3DF30B), Color(0xFF0DF6EF),  Color(0xFF0829FB),
      Color(0xFFB709F4)]; colors.addAll(colors.reversed.toList());final List<double> pos = List.generate(colors.length, (index) => index / colors.length);
    
    paint.shader =
        ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0.2 * pi);
    
    paint.maskFilter =
        MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
    
    canvas.drawPath(circlePath, paint);
  }

  @override
  bool shouldRepaint(covariantCircleHaloPainter oldDelegate) => oldDelegate.animation ! = animation; }Copy the code

3. The animation of streamer

When broken down, the outer ring rotates as follows. Animation is also very simple, is based on the value of the animator, so that the arc continues to rotate.

@override
void paint(Canvas canvas, Size size) {
  canvas.translate(size.width / 2, size.height / 2);
  finalPaint paint = Paint() .. style = PaintingStyle.stroke .. strokeWidth =1;
  paint.maskFilter =
      MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
  
  finalPath circlePath = Path().. addOval( Rect.fromCenter(center: Offset(0.0), width: 100, height: 100)); Path circlePath2 = Path().. addOval( Rect.fromCenter(center: Offset(- 1.0), width: 100, height: 100));
  Path result = Path.combine(PathOperation.difference, circlePath, circlePath2);
  
  canvas.save();
  canvas.rotate(animation.value * 2* pi); paint.. style = PaintingStyle.fill.. color = Color(0xff00abf2);
  canvas.drawPath(result, paint);
  canvas.restore();
}
Copy the code

And finally, when you draw, you just draw both of these things.


In addition, this drawing has been put into the drawing collection of FlutterUnit, you can update and check.


The following all the code posted some, you can run a play.

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

class CircleHalo extends StatefulWidget {
  const CircleHalo({Key key}) : super(key: key);

  @override
  _CircleHaloState createState() => _CircleHaloState();
}

class _CircleHaloState extends State<CircleHalo>
    with SingleTickerProviderStateMixin {
  AnimationController _ctrl;

  @override
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2)); _ctrl.repeat(); }@override
  void dispose() {
    _ctrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return  CustomPaint(
        size: Size(200.200), painter: CircleHaloPainter(_ctrl), ); }}class CircleHaloPainter extends CustomPainter {
  Animation<double> animation;

  CircleHaloPainter(this.animation) : super(repaint: animation);

  final Animatable<double> rotateTween = Tween<double>(begin: 0, end: 2 * pi)
      .chain(CurveTween(curve: Curves.easeIn));

  final Animatable<double> breatheTween = TweenSequence<double>(
    <TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 0, end: 4),
        weight: 1,
      ),
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 4, end: 0),
        weight: 1,
      ),
    ],
  ).chain(CurveTween(curve: Curves.decelerate));

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    finalPaint paint = Paint() .. strokeWidth =1. style = PaintingStyle.stroke; Path circlePath = Path() .. addOval(Rect.fromCenter(center: Offset(0.0), width: 100, height: 100)); Path circlePath2 = Path() .. addOval( Rect.fromCenter(center: Offset(- 1.0), width: 100, height: 100));
    Path result =
        Path.combine(PathOperation.difference, circlePath, circlePath2);

    List<Color> colors = [
      Color(0xFFF60C0C), Color(0xFFF3B913), Color(0xFFE7F716), 
      Color(0xFF3DF30B), Color(0xFF0DF6EF), Color(0xFF0829FB), Color(0xFFB709F4)]; colors.addAll(colors.reversed.toList());final List<double> pos =
        List.generate(colors.length, (index) => index / colors.length);

    paint.shader =
        ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0.2 * pi);

    paint.maskFilter =
        MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
    canvas.drawPath(circlePath, paint);

    canvas.save();
    canvas.rotate(animation.value * 2* pi); paint .. style = PaintingStyle.fill .. color = Color(0xff00abf2);
    paint.shader=null;
    canvas.drawPath(result, paint);
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariantCircleHaloPainter oldDelegate) => oldDelegate.animation ! = animation; }Copy the code