type
status
date
slug
summary
tags
category
icon
password
<ins/>

中文翻译:

我是一个 Flutter 开发者,主要专注于 iOS 和 Android 平台的开发。我喜欢开发娱乐和教育类应用。
最近,我被要求添加一个绘图模块。这个功能非常简单:用户用手指在屏幕上移动即可绘制一条线:
notion image
实现时,我并没有花太多时间思考,采取了以下步骤:
  1. 创建一个类,用于存储线条的主要特性(点的坐标、颜色和笔触宽度)。
    1. 实现了 CustomPaint 小部件的 FingerPainter 类,定义了绘制线条的逻辑。
      1. 实现了可以绘图的屏幕界面
        上述代码实现了一个绘图功能,足够用于简单的开始。接下来可以逐步增加复杂度、优化功能和进行定制。

        当前实现存在哪些问题?

         
        让我们对我自己的代码进行简短的代码审查。

        问题一:绘制所有线条时整体重绘

        一次画出所有的线条。在 paint 方法中,每次都会启动一个循环,其中列表中的每一行都是按顺序绘制的。让我们纠正这个问题,将任务留给 FingerPainter 只绘制一条特定的线条:
        我们还对屏幕本身进行更正::
        现在,我们在 Stack (堆栈) 小部件中有一个循环。在 GestureDetector 中,属性行为已更改为 HitTestBehavior.opaque。默认情况下,GestureDetector 会响应对 child 或 stack 中的子项的点击。如果没有子项,则不会创建新行。
         
        为什么将 loop 专门放在堆栈中会更好?我稍后会回到这个问题。
         

        问题二:shouldRepaint 始终返回 true

        其次,shouldRepaint 始终为 true。此参数告诉我们何时重绘小部件(即调用 paint 方法)。如果它始终为 true,则相应地,对于任何微小的更改,或者例如,在 parent中调用 build,将发生重绘,并且所有 N 行都将重绘。让我们添加一个条件,以便仅当线条中的点数发生变化时才进行重绘。
        如果我们按照本文中的步骤操作,在这个阶段,我们的线条将停止再次绘制。这是因为条件始终返回 false。
        原因是 LineObject 类不是不可变的,我们可以毫无顾虑地更改 List<Offset> points 属性。在某个时间点之前,这似乎没有什么问题。
        当我们第一次创建 LineObject 类的实例时,我们会在内存中创建一个对部分的引用。当我们更改 points 属性时,我们总是在同一个对象、同一个内存部分进行。
        由于在 FingerPainter 的构造函数中,我们将引用传递给 LineObject 对象,因此在调用 shouldRepaint 的任何时间点,我们都会引用同一个对象。因此,points 属性具有相同的长度。
        为了避免这种情况,可以故意使类完全不可变,例如使用 freezed 库。然后,如果其他开发人员出现,他们将被迫创建类的新实例,而不是更改现有实例的属性。
        在本文中,我们先创建实例的副本,添加方法 copyWith:
        更新 _onPanStart_onPanUpdate 方法:
        现在,当添加新点时,将在线列表中创建并替换一个新对象。

        问题三:重绘不是孤立的

        有一个非常有用的小部件,称为 RepaintBoundary。它允许将一个 widget 与其他 widget 隔离开来,就好像 “保护” 不必要地重绘其他 widget 时,它会同时 “保护” 自身(当其他 widget 发生变化时)和其他 widget(当它被重绘时)进行重绘。
        因此,重绘仅发生在列表中的最后一个小组件中。
         
        正如我前面提到的,最好将循环移动到 Stack,因为它很方便,如上面的示例所示。
         
        FragmentShader 在哪里?
        我们已尽最大努力优化了当前版本的代码。但是,在此实现中还可能存在哪些问题呢?
        notion image
        如您所见,Widget 树随着行数的增加而线性增长,在这种情况下,每个 Widget 都有一个对应的 RenderObject,该对象至少存在于内存中,最多在屏幕上的其中一个层中渲染。我不会详细介绍渲染和栅格化,但会用简单的术语来描述它。
         
        假设有一个擦除功能。最有可能的是,它是带有背景颜色的某行(在我们的例子中,是一条白线)。
        notion image
         
         
        如果我们画一千条红线并开始以这种方式擦除它们,最终,我们可能只会得到一个白屏。但实际上,那些仍然是相同的千条红线,被另一层白色遮住了。显然,当存在许多此类层时,这会影响性能。
         
        我将提供现有应用程序中的具体示例,其中实现相同,但添加了各种类型的线条、颜色、纹理等:
         
        具有大量行的示例
        具有大量行的示例
        具有大量线条的帧渲染速度
        具有大量线条的帧渲染速度
        使用空画布的示例
        使用空画布的示例
         
        为了解决在屏幕上显示大量元素的问题,我想到了不要在屏幕上显示大量元素的想法:)
        从本质上讲,绘制的线条是静态元素。因此,您可以将它们转换为图像并将其保留在背景中。添加新行时,请更新此图像。
         
        为了实现这个想法,RepaintBoundary 小部件来救援,这次与 GlobalKey 结合使用:
         
         
        我们将 Stack 小部件包装在 RepaintBoundary 中。然后,使用该键,我们从 widget 创建一个图像。pixelRatio 参数应设置为 1,因为我们需要 1:1 的大小。顺便说一句,在其他情况下,可以将此参数设置为大于 1 的值,以提高结果图像的分辨率。
         
        一般来说,这足以确保 renderObject 的数量不会增加,并且 widget 树总是看起来像这样:
         
        notion image
         
        我们希望最大限度地提高性能并减少操作数量。我们该怎么做呢?
         
        在 CustomPainter 中,有一种绘制图像的方法。让我们为它编写一个实现:
         
        让我们更新代码:
         
         
        我们可以在这一点上停下来,因为从这里开始事情会变得更加复杂。
         
        我注意到在 CustomPainter 中绘制的图像质量可能会显著降低;它们变得像素化。当您将常规的精美图像(例如,从 assets 转换为 ui)时,这一点会变得很明显。在 Painter 中对其进行成像和绘制。
         
        notion image
         
        为了避免质量损失,我们可以使用 FragmentShader
         
        让我们在资产中创建一个着色器文件:
        notion image
         
        将其添加到 pubspec.yaml 中:
        为着色器编写以下代码:

        代码解析

        • uniform vec2 uResolution
          • 表示屏幕的分辨率(宽度和高度)。在使用着色器时需要设置这个参数。
        • uniform sampler2D uTexture
          • 基本上是一个 ui.Image,需要传递该参数给着色器。
        • out vec4 fragColor
          • 一个包含四个参数的变量,表示像素的颜色,它是最终的输出结果。
        • vec2 st
          • 当前像素的相对坐标。
        • vec4 applyAntiAliasing(vec2 coord)
          • 一个用于平滑处理(去除像素化效果)的函数。

        更新 ImagePainter 以使用着色器

         
        _getShader 方法中,我们设置了以下参数:
        1. 分辨率(widget 的宽度和高度)。
        1. 纹理(图像本身)。
        通过这些步骤,图像的绘制质量得到了优化,同时避免了像素化问题。
         
        _getShader 方法中,我们设置了必要的参数:widget 的尺寸和图像。

        更新后的完整代码


        总结

        通过减少不必要的重建,并将绘制的对象转换为图像,我们实现了性能优化。此外,借助 FragmentShader 保留了图像的高质量。
        希望本文的内容能帮助您在项目中解决类似问题,同时激励您找到非传统方式解决复杂任务的灵感。
        <ins/>