在 Flutter 中使用设置的 FPS 绘制大量简单、填充颜色的矩形的最高效方法是啥?

Posted

技术标签:

【中文标题】在 Flutter 中使用设置的 FPS 绘制大量简单、填充颜色的矩形的最高效方法是啥?【英文标题】:What is the most performant way to draw a large number of simple, colour-filled rectangles with a set FPS in Flutter?在 Flutter 中使用设置的 FPS 绘制大量简单、填充颜色的矩形的最高效方法是什么? 【发布时间】:2021-03-26 19:05:17 【问题描述】:

我想用大量随机的、颜色填充的矩形来填充一个方形的 CustomPaint 小部件。动画需要以设定的 FPS 速率更新。我有一个工作演示,但即使只有 100x100 随机矩阵,我的手机上的最大 FPS 似乎也在 30 左右。代码需要跨平台(安卓和ios)。

我是 Flutter 的新手,我想知道是否有更高效的方式来生成这个动画。逻辑一点也不复杂,但我想大量的随机值生成和成千上万的小矩形是及时生成的挑战。在原生 android 上,我使用了 openGL 和 GLSurfaceView,我希望 Flutter 有同样的性能......

这是我的代码的链接:https://gist.github.com/ize8/f734b9d62d78c74667a845f211e06fb7

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

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() 
  runApp(MyApp());


class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'CustomPaint Animation'),
    );
  


class MyHomePage extends StatefulWidget 
  MyHomePage(Key key, this.title) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();


class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin 
  
  bool _isRunning = true; //is the animation running?
  var _fps = new List.filled(20, 0.0); //list for the last 20 FPS
  int _pos = 0; //position for logging FPS

  final _rnd = Random();
  final _rowSize = 100;//how many rectangles in a row
  final _freq = -1; //the maximum FPS, -1 if no limit

  List _matrix;

  Ticker _ticker;
  Duration lastTick = Duration(milliseconds: 0);

  @override
  void initState() 
    super.initState();
    _matrix = _generateMatrix();//generate random set
    _ticker = createTicker((Duration elapsed) 
      var diff = elapsed.inMicroseconds - lastTick.inMicroseconds;//time elapsed since last tick
      var currentFps = 1000000 / diff;//calculate current FPS
      if (currentFps <= _freq || _freq == -1) 
        setState(() 
          _fps[_pos] = currentFps;//save FPS into list
          lastTick = elapsed;
          _matrix = _generateMatrix(); //generate new random set
        );
        if (_pos < _fps.length - 1)
          _pos += 1;
        else
          _pos = 0;
      
    );
    _ticker.start();//start ticker
  

  @override
  void dispose() 
    _ticker.dispose();
    super.dispose();
  

  //generate new random set of color codes 0-3
  List _generateMatrix() 
    return new List.generate(_rowSize * _rowSize, (int i) => _rnd.nextInt(4),
        growable: false);
  

  void _toggleDynamic(bool isRunning) 
    setState(() 
      _isRunning = isRunning;
    );
    if (isRunning)
      _ticker.start();
    else
      _ticker.stop();
  

  //calculate average FPS from list
  double _getAvgFps() 
    return _fps.reduce((value, element) => value + element) / _fps.length;
  

  @override
  Widget build(BuildContext context) 

    return Scaffold(
      appBar: AppBar(

        title: Text(widget.title),
      ),
      body: OrientationBuilder(builder: (context, orientation) 
        return Center(

          child: Container(
            color: Colors.grey[850],
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text(
                      "Run!",
                      style: TextStyle(
                          color: Colors.blue[300],
                          fontWeight: FontWeight.bold,
                          fontSize: 20.0),
                    ),
                    Switch(
                        value: _isRunning,
                        onChanged: (bool val) => _toggleDynamic(val)
                        ),
                  ],
                ),
                Divider(),
                Container(
                  width: 400,
                  height: 400,
                  decoration: BoxDecoration(color: Colors.white, boxShadow: [
                    BoxShadow(
                      color: Colors.black,
                      blurRadius: 4.0,
                      spreadRadius: 2.0,
                    )
                  ]),
                  child: CustomPaint(
                    painter: MyPainter(_matrix, 400/_rowSize, _rowSize),//fill out the 400x400 container
                    isComplex: true,
                    willChange: true,
                  ),
                ),
                Divider(),
                Text(
                  "$_getAvgFps().toStringAsFixed(2) FPS",
                  style: TextStyle(color: Colors.blue[300]),
                ),
              ],
            ),
          ),
        );
      ),
    );
  


class MyPainter extends CustomPainter 
  List _matrix;
  double _pixSize;
  int _rowSize;

  final redPaint = Paint()..color = Colors.red;
  final greenPaint = Paint()..color = Colors.green[900];
  final bluePaint = Paint()..color = Colors.blueGrey;
  final purplePaint = Paint()..color = Colors.purple;

  Paint getPaint(int code) 
    switch (code) 
      case 0:
        return bluePaint;
        break;
      case 1:
        return purplePaint;
        break;
      case 2:
        return greenPaint;
        break;
      case 3:
        return redPaint;
        break;
      default:
        return bluePaint;
    
  

  MyPainter(List matrix, double pixSize, int rowSize) 
    this._matrix = matrix;
    this._pixSize = pixSize;
    this._rowSize = rowSize;
  

  @override
  void paint(Canvas canvas, Size size) 
    for (var i = 0; i < _matrix.length; i++) 
      var col = (i / _rowSize).floor();
      var row = i % _rowSize;
      canvas.drawRect(
          Rect.fromLTWH(col * _pixSize, row * _pixSize, _pixSize, _pixSize),
          getPaint(_matrix[i]));
    
  

  @override
  bool shouldRepaint(MyPainter oldDelegate) 
    return false;
  

也许将位图生成为原始字节码并将其显示在画布上而不是绘制所有这些矩形会更快?

P.S.[1]:我发现 decodeImageFromPixels() 看起来很有希望,虽然同时也是一个 PITA...

【问题讨论】:

【参考方案1】:

decodeImageFromPixels() 然后将回调中的 Image 绘制到 CustomPaint 上提供了令人满意的性能提升。虽然还没有达到Android+GLSurfaceView的水平,但还是可以接受的:)

【讨论】:

以上是关于在 Flutter 中使用设置的 FPS 绘制大量简单、填充颜色的矩形的最高效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

60 FPS 是啥意思在 Flutter 中如何一个屏幕每秒有 60 帧

FPS游戏:D3D内部游戏菜单实现

iOS之性能优化·列表异步绘制

android帧的绘制过程以及fps的获取

重复触发 AVAudioPlayer 时,大量 fps 下降(Sprite Kit)

Flutter 自定义 View 介绍