Flutter优化CustomPainter动画性能

7

我需要一个可以将正弦和余弦函数绘制到画布上的加载小部件。我使用CustomPaint小部件和CustomPainter编写了代码,但是当我对其进行分析时,发现它运行在约49fps而不是60fps。UI线程工作良好,每帧大约需要6ms,但光栅线程需要更长时间。我尝试在画布上绘制更少的点(在for循环中使用i=i+5代替i++),但结果几乎相同。

有人能否建议我如何提高性能的想法?下面是该小部件的代码,以及每帧光栅线程执行情况的DevTools截图,如果有用的话。

import 'dart:math';

import 'package:flutter/material.dart';

class LoadingChart extends StatefulWidget{
  final Color color1;
  final Color color2;
  final double lineWidth;
  final bool line;
  final Size size;

  const LoadingChart({
    @required this.color1,
    @required this.color2,
    @required this.size,
    @required this.lineWidth,
    this.line = true,
    Key key
  }): super(key: key);

  @override
  State<StatefulWidget> createState() => _LoadingChartState();

}

class _LoadingChartState extends State<LoadingChart>
  with SingleTickerProviderStateMixin{
  AnimationController _controller;


  double randomHeight(Random random, double max){
    return random.nextDouble()*max;
  }

  @override
  void initState() {
    _controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
    _controller.addListener(() {setState(() {});});
    _controller.repeat();

    super.initState();
  }

  @override
  void dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: widget.size.height,
      width: widget.size.width,
      child: CustomPaint(
        painter: PathPainter(
          color1: widget.color1,
          color2: widget.color2,
          value: _controller.value,
          line: widget.line,
        ),
      )
    );
  }

}

class PathPainter extends CustomPainter {
  final Color color1;
  final Color color2;
  final double lineWidth;
  final bool line;

  final double value;

  PathPainter({
    @required this.value,
    this.color1=Colors.red,
    this.color2=Colors.green,
    this.line = true,
    this.lineWidth=4.0,
  }): super();

  @override
  void paint(Canvas canvas, Size size) {
    final height = size.height;
    final width = size.width;

    Paint paint1 = Paint()
      ..color = color1
      ..style = PaintingStyle.stroke
      ..strokeWidth = lineWidth;

    Paint paint2 = Paint()
      ..color = color2
      ..style = PaintingStyle.stroke
      ..strokeWidth = lineWidth;

    Path path1 = Path();
    Path path2 = Path();

    /* If line is true, draw sin and cos functions, otherwise, just some points */
    for (double i = 0; i < width; i=i+5){
      double f = i*2*pi/width + 2*pi*value;
      double g = i*2*pi/width - 2*pi*value;
      if (i == 0){
        path1.moveTo(0, height/2 + height/6*sin(f));
        path2.moveTo(0, height/2 + height/6*cos(g));
        continue;
      }

      path1.lineTo(i, height/2 + height/6*sin(f));
      path2.lineTo(i, height/2 + height/6*cos(g));
    }

    /* Draw both lines */
    canvas.drawPath(path1, paint1);
    canvas.drawPath(path2, paint2);
  }

  @override
  bool shouldRepaint(PathPainter oldDelegate) {
   return oldDelegate.value != value || oldDelegate.color1 != color1
     || oldDelegate.color2 != color2 || oldDelegate.line != line
     || oldDelegate.lineWidth != lineWidth;
  }
}

输入图像描述 输入图像描述

PS:我正在使用配置文件模式运行应用程序,因此这不应该是问题。另外,我想提一下,它是屏幕上唯一重新绘制的小部件。 非常感谢!

1个回答

8

CustomPainter可以接收一个可监听的对象,因此您可以在那里使用动画控制器来在每个tick更新它。

class _LoadingChartState extends State<LoadingChart>
    with SingleTickerProviderStateMixin{
  AnimationController _controller;


  double randomHeight(Random random, double max){
    return random.nextDouble()*max;
  }

  @override
  void initState() {
    _controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
    //_controller.addListener(() {setState(() {});}); no need to setState
    _controller.repeat();

    super.initState();
  }

  @override
  void dispose(){
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
        height: widget.size.height,
        width: widget.size.width,
        child: CustomPaint(
          willChange: true, //this can help (Whether the raster cache should be told that this painting is likely)
          painter: PathPainter(
            color1: widget.color1,
            color2: widget.color2,
            line: widget.line,
            listenable: _controller //pass the controller as it is (An animationController extends a Listenable)
          ),
        )
    );
  }
}

在 PathPainter 中,您需要将可监听对象传递给构造函数,并将其传递给 CustomPainter 构造函数,该构造函数接受一个名为 repaint 的可监听对象。
class PathPainter extends CustomPainter {
  final Animation listenable;
  final Color color1;
  final Color color2;
  final double lineWidth;
  final bool line;

  PathPainter({
    this.listenable,
    this.color1=Colors.red,
    this.color2=Colors.green,
    this.line = true,
    this.lineWidth=4.0,
  }): super(repaint: listenable); //don't forget calling the CustomPainter constructor with super

  @override
  void paint(Canvas canvas, Size size) {
    double value = listenable.value; // get its value here
    final height = size.height;
    final width = size.width;

    Paint paint1 = Paint()
      ..color = color1
      ..style = PaintingStyle.stroke
      ..strokeWidth = lineWidth;

    Paint paint2 = Paint()
      ..color = color2
      ..style = PaintingStyle.stroke
      ..strokeWidth = lineWidth;

    Path path1 = Path();
    Path path2 = Path();

    /* If line is true, draw sin and cos functions, otherwise, just some points */
    for (double i = 0; i < width; i=i+5){
      double f = i*2*pi/width + 2*pi*value;
      double g = i*2*pi/width - 2*pi*value;
      if (i == 0){
        path1.moveTo(0, height/2 + height/6*sin(f));
        path2.moveTo(0, height/2 + height/6*cos(g));
        continue;
      }

      path1.lineTo(i, height/2 + height/6*sin(f));
      path2.lineTo(i, height/2 + height/6*cos(g));
    }

    /* Draw both lines */
    canvas.drawPath(path1, paint1);
    canvas.drawPath(path2, paint2);
  }

  @override
  bool shouldRepaint(PathPainter oldDelegate) {
    //delete the oldDelegate.value, it doesn't exists anymore
    return oldDelegate.color1 != color1
        || oldDelegate.color2 != color2 || oldDelegate.line != line
        || oldDelegate.lineWidth != lineWidth;
  }
}

enter image description here

我目前处于调试模式,因此我希望您在性能分析模式下获得更好的性能。

好的,这样我得到了稍微更好的结果,但我觉得很奇怪,一个相对简单的动画像这样在光栅线程上花费的时间比其他更复杂的动画,如小部件切换在AnimatedSwitcher上要多得多。为什么会这样呢? 无论如何,还是谢谢! - Javierd98
我认为问题在于您正在使用AnimationController,但没有使用可以侦听或更新该值的小部件,而是每个tick都使用setState,这会更慢。尝试不要在AnimationController中使用setState,而是使用AnimatedBuilder包装CustomPaint,这应该优化事物。 - EdwynZN
我不认为那是问题。如果你看一下AnimatedBuilder的代码,它只是一个AnimatedWidget的包装器,而AnimatedWidget在每个控制器更新时只调用setState,所以基本上我在这里做的就是一样的。 - Javierd98

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接