[Flutter drawing collection] the second painting: streamer

Zero: brief description of the effect of this paper

Through a small case, this paper introduces the use of fluent drawing and fluent animation. Below is a colorful circle with two animation effects:

  • [1]. Some halos around will spread and shrink animation.
  • [2]. The outer ring of the circle has a segment of streamer rotating around the ring.

1, Static effect drawing

1. Drawing of outer ring

A CircleHalo component is defined below to display the contents drawn by the CircleHaloPainter palette. Because of the animation to be performed later, it is defined as StatefulWidget in this way.

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(),
    );
  }
}

Let's draw a circle first. It shouldn't be difficult for everyone.

class CircleHaloPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    
    final Paint 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;
}

Now let's look at how to generate halo: Paint objects can set the maskFilter attribute through maskFilter Blur lets the brush blur, blurstyle Solid mode will make the brush produce fuzzy shadows around when drawing. The second parameter determines the degree of fuzziness. The effects of fuzziness of 2, 4 and 6 are as follows.

final Paint paint = Paint()
  ..style = PaintingStyle.stroke
  ..strokeWidth = 1;

// Set maskFilter
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);

The next step is to set the color brush. You can set the shader through the shader attribute of the Paint object. The following is a colorful scanning gradient shading.

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 shader
paint.shader =
    ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0, 2 * pi);
// Set maskFilter
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);

At this point, we have completed 1 / 4. The halo diffusion and contraction animation is actually changing the sigma value of the blur mask dynamically.

2. Streamer static effect of outer ring

The static effect of the outer ring rotation is as follows. The leftmost is a crescent shaped arc. It can be obtained through the combination of two circular paths through difference. The centers of the two circles have a slight offset in the transverse direction. The larger the offset, the fatter the crescent. Here is the effect of offset = 1.

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

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

2, Animation of the outer ring

Firstly, the following halo diffusion and contraction animation is realized. The animation cycle is 2s and is executed repeatedly.

1. Status processing

To handle the animation yourself, first create an animation controller. Because a TickerProvider input parameter is required when constructing the animation controller, you can_ CircleHaloState is mixed with SingleTickerProviderStateMixin, making the state class itself the implementation class of TickerProvider. As follows, a 2s animator is created in initState, and the animation is repeated through the repeat method. When constructing a CircleHaloPainter, use the animator as an input parameter.

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),
    );
  }
}
2. Animation processing of drawing board

Our goal is to make maskfilter The second parameter value of blur changes with the animator to achieve the effect of Animation. The subclass construction of CustomPainter can associate the Listenable object through super (reply: visible object). Because the animator Animation is a subclass of Listenable, the animator is associated here, so that when the value of the animator changes, it will notify the drawing board to redraw.

In the following processing, the more important point is to define a tweet that changes back and forth through tweetsequence. For example, the animation duration is 2s, which changes between 0 ~ 4 in the first second and 4 ~ 0 in the second second. In this way, the value can come back and forth in an animation cycle. In addition, the CurveTween object is passed in through the chain method, which can make the value of the current Animatable change and increase the effect of the animation curve.

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);
    final Paint 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(covariant CircleHaloPainter oldDelegate) =>
      oldDelegate.animation != animation;
}

3, Streamer animation

Take it apart and see that the rotation effect of the outer ring is as follows. Animation is also very simple, that is, according to the value of the animator, let the arc rotate continuously.

@override
void paint(Canvas canvas, Size size) {
  canvas.translate(size.width / 2, size.height / 2);
  final Paint paint = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = 1;
  paint.maskFilter =
      MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
  
  final 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);
  
  canvas.save();
  canvas.rotate(animation.value * 2 * pi);
  paint..style = PaintingStyle.fill..color = Color(0xff00abf2);
  canvas.drawPath(result, paint);
  canvas.restore();
}

Finally, when drawing, just draw both things.

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

Now paste all the code, you can run it and 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);
    final Paint 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(covariant CircleHaloPainter oldDelegate) =>
      oldDelegate.animation != animation;
}

Added by maplist on Tue, 08 Mar 2022 10:42:24 +0200