解析 ‘Skia’ 在 React 里的集成:如何利用 `react-native-skia` 实现 120FPS 的高性能滤镜与动画?

各位开发者,大家好!

欢迎来到今天的讲座。今天我们将深入探讨一个激动人心的话题:如何在 React 环境中,特别是借助 react-native-skia 库,实现并驾驭 Skia 图形引擎的强大能力,从而打造出流畅至 120FPS 的高性能滤镜与动画。

移动应用的用户体验标准正在日益提高,流畅的 UI 交互和精美的视觉效果已成为衡量应用质量的关键指标。传统的基于 DOM 或原生视图层级的动画与图形渲染,在面对复杂场景、高帧率要求时,往往会暴露出性能瓶颈。这时,一个底层、高效、且跨平台的图形渲染库就显得尤为重要。Skia 正是这样一款明星产品。

1. Skia:高性能图形渲染的基石

首先,我们来认识一下 Skia。Skia 是一个开源的 2D 图形渲染引擎,由 Google 维护并广泛应用于其多个核心产品中,包括 Chrome 浏览器、Android 操作系统、Flutter 框架等。它的核心优势在于:

  1. 硬件加速 (GPU Accelerated):Skia 能够充分利用 GPU 的强大并行计算能力进行渲染,而不是仅仅依赖 CPU。这意味着复杂的图形操作(如路径绘制、图像处理、滤镜应用)可以被卸载到 GPU 上并行处理,极大地提高了渲染效率。
  2. 跨平台:Skia 提供了 C++ API,可以在 Windows、macOS、Linux、iOS、Android 等多个平台上无缝运行,确保了渲染结果的一致性。
  3. 高质量渲染:Skia 支持高级的抗锯齿、颜色管理、文本渲染、矢量图形和位图操作,能够生成高质量的视觉输出。
  4. 丰富的功能集:从基本的几何图形绘制、路径操作,到复杂的图像滤镜、着色器(Shaders)、文字排版,Skia 提供了一整套全面的图形处理能力。

简而言之,Skia 是一个功能强大、性能卓越的 2D 图形引擎,它为我们构建高性能、高质量的视觉体验提供了坚实的基础。

2. react-native-skia:将 Skia 引入 React Native

既然 Skia 如此强大,那么如何在 React Native 这个流行的跨平台框架中使用它呢?答案就是 react-native-skia

react-native-skia 是由 Shopify 团队开发并维护的一个库,它将 Skia 引擎封装成 React Native 组件,允许开发者通过声明式的方式在 JavaScript 中直接利用 Skia 进行高性能的图形绘制。它的出现,极大地扩展了 React Native 在图形渲染方面的能力,使得实现复杂、高帧率的自定义 UI、数据可视化、游戏甚至图像编辑应用成为可能。

2.1 为什么选择 react-native-skia

  • GPU 加速渲染:这是最核心的原因。react-native-skia 内部将所有的绘图指令转化为 Skia 的 C++ 调用,然后由 Skia 利用底层图形 API (如 OpenGL ES 或 Metal) 在 GPU 上完成渲染。这避免了传统 React Native 视图层级渲染的 CPU 瓶颈。
  • 声明式 API:与 React 的开发范式完美契合。你只需声明你想要绘制什么,而不是如何绘制。组件化的设计使得图形元素的组合和复用变得非常简单。
  • 与 React Native 生态集成:可以与现有的 React Hooks、Context API、以及像 react-native-reanimated 这样的动画库无缝协作,构建出响应式的、高性能的动画和交互。
  • 跨平台一致性:基于 Skia 引擎,确保了在 iOS 和 Android 平台上渲染结果的像素级一致性。
  • 避免桥接开销:对于动画和高频更新的图形,react-native-skia 结合 react-native-reanimated 的 Worklet 机制,可以在 UI 线程上直接执行动画逻辑,避免了 JavaScript 桥的频繁通信开销。

3. 120FPS 挑战与 react-native-skia 的解决方案

实现 120FPS (帧每秒) 的流畅体验在移动应用中是一个不小的挑战。这意味着每一帧的渲染和逻辑处理必须在大约 8.33 毫秒内完成。传统的 React Native 动画和图形更新,如果处理不当,很容易受到以下因素的限制:

  1. JavaScript 线程瓶颈:React Native 的 UI 逻辑和动画通常运行在 JavaScript 线程上。如果 JS 线程被大量计算、网络请求或复杂的状态更新阻塞,它将无法及时发送 UI 更新指令,导致掉帧。
  2. 桥接通信开销:JavaScript 线程和原生 UI 线程之间通过桥进行通信。频繁且大量的数据传输会引入显著的延迟,尤其是在高帧率动画中。
  3. 原生视图层级限制:原生视图(如 View, Text, Image)的层级越深、数量越多,渲染和布局的开销就越大。复杂的用户界面和动画在原生视图层级上可能会面临性能瓶限制。
  4. CPU 渲染限制:如果图形渲染主要依赖 CPU 进行像素计算,那么在处理复杂图形和实时滤镜时,CPU 可能会成为瓶颈。

react-native-skia 如何应对这些挑战,从而实现 120FPS 呢?

  • GPU 主导渲染:这是最根本的。Skia 将绘图指令转化为 GPU 可以理解的命令。GPU 擅长并行处理大量像素和几何图形,这使得它在执行复杂滤镜和绘制大量图形元素时远超 CPU。
  • 声明式与优化绘制react-native-skia 的声明式 API 允许它在底层进行智能优化。它知道何时只需重绘画布的局部区域,或者如何批处理绘图命令,减少重复工作。
  • react-native-reanimated 深度集成:这是实现 120FPS 动画的关键。react-native-reanimated 允许将动画逻辑(所谓的 "Worklets")直接编译并运行在原生 UI 线程上,完全绕过 JavaScript 线程和桥接。react-native-skia 的动画值(SkiaValue)可以与 ReanimatedSharedValue 协同工作,这意味着 Skia 的图形属性可以在 UI 线程上实时更新,而无需任何 JS 线程的介入,从而实现原生级别的流畅动画。
  • 单一画布优化:与多个原生视图层叠渲染不同,react-native-skia 通常在一个单一的 Canvas 组件内部完成所有绘制。这减少了视图层级的复杂性,也更容易让底层图形引擎进行全局优化。

理解了这些背景,我们现在就深入到 react-native-skia 的具体 API 和实践中。

4. react-native-skia 核心概念与 API 详解

react-native-skia 的核心是 Canvas 组件,它提供了一个绘图表面。在这个表面上,我们可以使用各种 Skia 提供的组件来绘制几何图形、路径、文本、图像,并应用各种样式和效果。

4.1 Canvas 组件

Canvas 是所有 Skia 绘图的根组件。它接受 style 属性来定义其大小和位置,就像普通的 React Native View 一样。

import { Canvas } from '@shopify/react-native-skia';
import React from 'react';
import { View } from 'react-native';

const MySkiaCanvas = () => {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Canvas style={{ width: 300, height: 300, backgroundColor: '#eee' }}>
        {/* 在这里绘制 Skia 元素 */}
      </Canvas>
    </View>
  );
};

export default MySkiaCanvas;

4.2 Paint:绘制的画笔

在 Skia 中,Paint 对象定义了如何绘制一个图形。它包含了颜色、描边样式、填充模式、模糊效果、混合模式等一系列属性。几乎所有 Skia 元素都可以接受一个 Paint 属性来控制其外观。

react-native-skia 提供了一个 Paint 组件,你可以像这样使用它:

import { Canvas, Circle, Paint, Skia } from '@shopify/react-native-skia';
import React from 'react';

const BasicShapes = () => {
  const paint = Skia.Paint(); // 创建一个 Skia Paint 对象
  paint.setColor(Skia.Color('red')); // 设置颜色
  paint.setAntiAlias(true); // 开启抗锯齿
  paint.setStrokeWidth(5); // 设置描边宽度
  paint.setStyle(Skia.PaintStyle.Stroke); // 设置为描边模式

  return (
    <Canvas style={{ width: 200, height: 200 }}>
      <Circle cx={100} cy={100} r={50} paint={paint} />
      {/* 也可以直接在组件内部定义 Paint 属性 */}
      <Circle cx={50} cy={50} r={20}>
        <Paint color="blue" style="fill" />
      </Circle>
    </Canvas>
  );
};

export default BasicShapes;

以下是 Paint 组件的一些常用属性:

属性名称 类型 描述 示例
color Color 绘制颜色。可以是 CSS 颜色字符串、十六进制字符串或 Skia.Color() color="red", color="#FF0000", color="rgba(255,0,0,0.5)"
style PaintStyle ("fill", "stroke") 绘制样式:填充或描边。 style="fill", style="stroke"
strokeWidth number 描边宽度。仅当 style="stroke" 时有效。 strokeWidth={2}
strokeCap StrokeCap ("butt", "round", "square") 描边端点样式。 strokeCap="round"
strokeJoin StrokeJoin ("miter", "round", "bevel") 描边连接点样式。 strokeJoin="round"
antiAlias boolean 是否开启抗锯齿。 antiAlias={true}
blendMode BlendMode 混合模式。定义了源像素如何与目标像素混合。 blendMode="multiply", blendMode="screen" (详见 Skia BlendMode 文档)
opacity number (0-1) 整体不透明度。 opacity={0.5}
shader SkiaShader 应用一个着色器。可以实现渐变、图像纹理、复杂效果等。 <LinearGradient color1="red" color2="blue" /> (作为 Paint 的子组件)
imageFilter ImageFilter 应用一个图像滤镜。如模糊、阴影、颜色矩阵等。 <Blur sigmaX={5} sigmaY={5} /> (作为 Paint 的子组件)
colorFilter ColorFilter 应用一个颜色滤镜,用于调整颜色。 <ColorMatrix colorMatrix={...} /> (作为 Paint 的子组件)

4.3 几何图形绘制

react-native-skia 提供了丰富的几何图形组件:

  • Rect: 矩形
  • RRect: 圆角矩形
  • Circle: 圆形
  • Oval: 椭圆
  • Line: 直线
  • Path: 路径 (最灵活,可绘制任意复杂形状)
import { Canvas, Rect, RRect, Circle, Line, Path, Paint } from '@shopify/react-native-skia';
import React from 'react';

const GeometryDemo = () => {
  return (
    <Canvas style={{ width: 400, height: 400, borderWidth: 1, borderColor: 'gray' }}>
      {/* 填充红色矩形 */}
      <Rect x={10} y={10} width={80} height={80}>
        <Paint color="red" style="fill" antiAlias={true} />
      </Rect>

      {/* 蓝色描边圆角矩形 */}
      <RRect x={100} y={10} width={80} height={80} rx={10} ry={10}>
        <Paint color="blue" style="stroke" strokeWidth={3} antiAlias={true} />
      </RRect>

      {/* 绿色圆形 */}
      <Circle cx={250} cy={50} r={40}>
        <Paint color="green" style="fill" antiAlias={true} />
      </Circle>

      {/* 黄色直线 */}
      <Line p1={{ x: 10, y: 150 }} p2={{ x: 190, y: 150 }}>
        <Paint color="yellow" style="stroke" strokeWidth={5} strokeCap="round" antiAlias={true} />
      </Line>

      {/* 使用 Path 绘制一个三角形 */}
      <Path
        path="M 250 150 L 300 200 L 200 200 Z" // M: move to, L: line to, Z: close path
        color="purple"
        style="fill"
        antiAlias={true}
      />

      {/* 绘制一个更复杂的 Path,例如贝塞尔曲线 */}
      <Path
        path="M 50 250 C 150 150, 250 350, 350 250" // M: move to, C: cubic bezier curve
        color="orange"
        style="stroke"
        strokeWidth={4}
        antiAlias={true}
      />
    </Canvas>
  );
};

export default GeometryDemo;

Path 组件的 path 属性接受 SVG 路径数据字符串,这使得你可以轻松地从矢量图形软件中导入复杂形状。

4.4 变换 (Transformations)

react-native-skia 提供了 Group 组件来对一组元素应用变换。Group 可以嵌套。

  • translate: 平移
  • rotate: 旋转
  • scale: 缩放
  • skew: 倾斜

这些变换会按照它们在 XML 结构中的顺序应用。

import { Canvas, Rect, Group, Paint } from '@shopify/react-native-skia';
import React from 'react';

const TransformationsDemo = () => {
  return (
    <Canvas style={{ width: 300, height: 300, borderWidth: 1, borderColor: 'gray' }}>
      <Group origin={{ x: 150, y: 150 }} rotate={45}>
        <Group x={-50} y={-50} scale={0.8}>
          <Rect x={100} y={100} width={100} height={100}>
            <Paint color="teal" style="fill" antiAlias={true} />
          </Rect>
        </Group>
      </Group>

      {/* 一个平移和倾斜的矩形 */}
      <Group translate={{ x: 50, y: 200 }} skewX={0.2}>
        <Rect x={0} y={0} width={80} height={80}>
          <Paint color="purple" style="fill" antiAlias={true} />
        </Rect>
      </Group>
    </Canvas>
  );
};

export default TransformationsDemo;

注意 origin 属性用于设置旋转和缩放的中心点。如果没有指定 origin,则默认以元素的左上角或画布的 (0,0) 点进行变换。

4.5 文本和图像

Text 组件用于在画布上渲染文本。Image 组件用于渲染位图图像。

import { Canvas, Text, Paint, Image, useImage } from '@shopify/react-native-skia';
import React from 'react';
import { StyleSheet } from 'react-native';

const TextAndImageDemo = () => {
  const image = useImage(require('./assets/example.png')); // 确保你有这个图片文件

  if (!image) {
    return null; // 图片加载中
  }

  return (
    <Canvas style={styles.canvas}>
      <Text x={20} y={50} text="Hello Skia!" fontStyle="bold" size={30}>
        <Paint color="navy" antiAlias={true} />
      </Text>

      <Text x={20} y={100} text="Custom Font Example" size={20}>
        {/* 如果需要自定义字体,需要先加载字体文件 */}
        <Paint color="gray" antiAlias={true} />
      </Text>

      <Image image={image} x={50} y={150} width={200} height={150} fit="contain">
        <Paint antiAlias={true} />
      </Image>

      {/* 带有滤镜的图像 */}
      <Image image={image} x={280} y={150} width={100} height={100} fit="cover">
        <Paint antiAlias={true}>
          <ColorMatrix
            matrix={[
              0.2126, 0.7152, 0.0722, 0, 0, // R
              0.2126, 0.7152, 0.0722, 0, 0, // G (灰度化)
              0.2126, 0.7152, 0.0722, 0, 0, // B
              0,      0,      0,      1, 0, // A
            ]}
          />
        </Paint>
      </Image>
    </Canvas>
  );
};

const styles = StyleSheet.create({
  canvas: {
    width: 400,
    height: 400,
    borderWidth: 1,
    borderColor: 'lightgray',
  },
});

export default TextAndImageDemo;

关于字体: Text 组件的 fontStylesize 属性可以控制基本字体样式。对于自定义字体,你需要使用 useFont hook 加载 SkiaFont 对象,然后将其传递给 Text 组件。

import { Canvas, Text, useFont } from '@shopify/react-native-skia';
// ...
const MyText = () => {
  const font = useFont(require('./assets/my-custom-font.ttf'), 24); // 加载字体文件

  if (!font) {
    return null;
  }

  return (
    <Canvas style={{ width: 300, height: 100 }}>
      <Text x={50} y={50} text="Custom Font Text" font={font} color="black" />
    </Canvas>
  );
};

5. 驾驭 Shaders:图形渲染的魔法

Shaders (着色器) 是 GPU 上运行的小程序,它们定义了如何计算每个像素的颜色。Skia 提供了强大的着色器功能,允许你创建复杂的视觉效果,如渐变、纹理、扭曲、粒子系统等。在 react-native-skia 中,你可以通过 Paint 组件的 shader 属性来应用着色器。

5.1 内置着色器

react-native-skia 提供了一些开箱即用的着色器组件:

  • LinearGradient: 线性渐变
  • RadialGradient: 径向渐变
  • SweepGradient: 扫描渐变
  • TwoPointConicalGradient: 两点圆锥渐变
  • ImageShader: 使用图像作为纹理
  • ColorShader: 单一颜色着色器 (很少直接用,通常直接设置 Paintcolor)
  • PerlinNoiseShader: 柏林噪声,用于生成自然纹理
import { Canvas, Rect, Paint, LinearGradient, RadialGradient, ImageShader, useImage } from '@shopify/react-native-skia';
import React from 'react';

const ShaderDemo = () => {
  const image = useImage(require('./assets/pattern.png')); // 确保有纹理图片

  if (!image) {
    return null;
  }

  return (
    <Canvas style={{ width: 400, height: 400, borderWidth: 1, borderColor: 'gray' }}>
      {/* 线性渐变矩形 */}
      <Rect x={10} y={10} width={180} height={80}>
        <Paint>
          <LinearGradient
            start={{ x: 0, y: 0 }}
            end={{ x: 180, y: 80 }}
            colors={['#FF0000', '#0000FF']}
            positions={[0, 1]}
          />
        </Paint>
      </Rect>

      {/* 径向渐变矩形 */}
      <Rect x={200} y={10} width={180} height={80}>
        <Paint>
          <RadialGradient
            c={{ x: 290, y: 50 }} // 中心点
            r={80} // 半径
            colors={['#00FF00', '#FFFF00']}
            positions={[0, 1]}
          />
        </Paint>
      </Rect>

      {/* 图像纹理矩形 */}
      <Rect x={10} y={100} width={180} height={180}>
        <Paint>
          <ImageShader
            image={image}
            fit="cover" // 适应方式
            tx="repeat" // 纹理重复模式 (repeat, clamp, mirror)
            ty="repeat"
          />
        </Paint>
      </Rect>

      {/* 图像纹理圆形 */}
      <Circle cx={290} cy={190} r={80}>
        <Paint>
          <ImageShader
            image={image}
            fit="cover"
            tx="repeat"
            ty="repeat"
          />
        </Paint>
      </Circle>
    </Canvas>
  );
};

export default ShaderDemo;

5.2 自定义 GLSL 着色器

Skia 真正强大的地方在于它允许你编写自定义的 GLSL (OpenGL Shading Language) 着色器。这些着色器可以直接在 GPU 上运行,实现前所未有的自定义视觉效果。react-native-skia 通过 Shader 组件暴露了这个能力。

GLSL 着色器通常需要以下几个要素:

  • uniform 变量:从 JavaScript 传递到着色器的数据(如时间、鼠标位置、颜色等)。
  • vec2 uv 坐标:当前像素在纹理空间中的坐标,通常范围是 [0, 1]。
  • color(uv) 函数:计算并返回当前 uv 坐标对应的像素颜色。
import { Canvas, Rect, Paint, Shader, useClockValue, useValue } from '@shopify/react-native-skia';
import React from 'react';
import { Dimensions } from 'react-native';

const { width, height } = Dimensions.get('window');

const CustomShaderDemo = () => {
  const clock = useClockValue(); // 获取一个随时间变化的 SkiaValue
  const progress = useValue(0); // 创建一个可动画的 SkiaValue

  // 一个简单的 GLSL 着色器,实现一个波浪效果
  // uniform float iTime: 时间变量
  // uniform float iResolution_x, iResolution_y: 画布尺寸
  // uniform float u_progress: 动画进度
  const shaderCode = `
    uniform float iTime;
    uniform float iResolution_x;
    uniform float iResolution_y;
    uniform float u_progress;

    vec4 color(vec2 uv) {
      vec2 st = uv;
      st.x *= iResolution_x / iResolution_y; // 宽高比修正

      float freq = 5.0;
      float amplitude = 0.05 + u_progress * 0.1; // 振幅随进度变化
      float wave = sin(st.x * freq + iTime * 2.0) * amplitude;

      vec3 colorA = vec3(0.0, 0.5, 1.0); // 蓝色
      vec3 colorB = vec3(1.0, 0.2, 0.0); // 橙色

      vec3 finalColor = mix(colorA, colorB, smoothstep(0.0, 1.0, st.y + wave));

      return vec4(finalColor, 1.0);
    }
  `;

  // 你可以通过 Reanimated 来驱动 progress 的变化
  // 例如:useEffect(() => { progress.current = withTiming(1, { duration: 2000 }); }, []);

  return (
    <Canvas style={{ flex: 1, width: width, height: height }}>
      <Rect x={0} y={0} width={width} height={height}>
        <Paint>
          <Shader
            source={shaderCode}
            uniforms={{
              iTime: clock,
              iResolution_x: width,
              iResolution_y: height,
              u_progress: progress, // 传递动画值
            }}
          />
        </Paint>
      </Rect>
    </Canvas>
  );
};

export default CustomShaderDemo;

GLSL 学习资源:

自定义着色器是实现高性能、独特视觉效果的核心。它们直接在 GPU 上运行,性能极高,非常适合实现复杂的实时滤镜和动画背景。

6. 实现高性能滤镜

滤镜是图像处理中常见的需求,例如模糊、灰度、亮度调整、色彩分离等。react-native-skia 提供了 ImageFilterColorFilter 组件来应用这些效果。

6.1 ImageFilter:图像像素级处理

ImageFilter 应用于整个图像或绘制的元素,可以实现模糊、阴影、位移等效果。

  • Blur: 模糊
  • DropShadow: 阴影
  • DisplacementMap: 位移映射 (利用一张图的颜色通道来扭曲另一张图)
  • Morphology: 形态学操作 (膨胀、侵蚀)
  • Offset: 偏移
import { Canvas, Rect, Paint, Blur, DropShadow, Image, useImage } from '@shopify/react-native-skia';
import React from 'react';
import { StyleSheet } from 'react-native';

const FilterDemo = () => {
  const image = useImage(require('./assets/mountain.jpg')); // 确保有图片

  if (!image) {
    return null;
  }

  return (
    <Canvas style={styles.canvas}>
      {/* 原始图像 */}
      <Image image={image} x={10} y={10} width={150} height={100} fit="cover" />

      {/* 模糊图像 */}
      <Image image={image} x={170} y={10} width={150} height={100} fit="cover">
        <Paint>
          <Blur sigmaX={5} sigmaY={5} /> {/* 5像素的X和Y方向模糊 */}
        </Paint>
      </Image>

      {/* 带有阴影的矩形 */}
      <Rect x={50} y={150} width={100} height={80}>
        <Paint color="blue">
          <DropShadow dx={5} dy={5} blur={5} color="rgba(0,0,0,0.5)" />
        </Paint>
      </Rect>

      {/* 带有内外模糊的圆形 */}
      <Circle cx={250} cy={190} r={60}>
        <Paint color="green" style="fill">
          <Blur sigmaX={10} sigmaY={10} mode="outer" /> {/* 外侧模糊 */}
        </Paint>
      </Circle>
    </Canvas>
  );
};

const styles = StyleSheet.create({
  canvas: {
    width: 400,
    height: 300,
    borderWidth: 1,
    borderColor: 'gray',
  },
});

export default FilterDemo;

ImageFilter 可以嵌套,形成复杂的效果链。例如,先模糊再加阴影。

6.2 ColorFilter:颜色矩阵与色彩调整

ColorFilter 主要用于调整颜色的属性,如亮度、对比度、饱和度、色相、灰度化、反色等。最强大的工具是 ColorMatrix

ColorMatrix 接受一个 20 元素的数组,表示一个 5×4 的颜色转换矩阵:

[
  R_R, R_G, R_B, R_A, R_Offset,
  G_R, G_G, G_B, G_A, G_Offset,
  B_R, B_G, B_B, B_A, B_Offset,
  A_R, A_G, A_B, A_A, A_Offset,
]

其中:

  • R, G, B, A 列是原像素的 R, G, B, A 分量对新像素 R, G, B, A 的影响权重。
  • Offset 列是 R, G, B, A 通道的偏移量 (0-255)。

示例:灰度化滤镜

灰度化通常是将 R, G, B 分量按一定权重 (例如亮度感知权重 0.2126, 0.7152, 0.0722) 加权平均后,赋给新的 R, G, B。

import { Canvas, Image, useImage, Paint, ColorMatrix } from '@shopify/react-native-skia';
import React from 'react';
import { StyleSheet } from 'react-native';

const ColorFilterDemo = () => {
  const image = useImage(require('./assets/city.jpg'));

  if (!image) {
    return null;
  }

  // 灰度化矩阵
  const grayscaleMatrix = [
    0.2126, 0.7152, 0.0722, 0, 0,
    0.2126, 0.7152, 0.0722, 0, 0,
    0.2126, 0.7152, 0.0722, 0, 0,
    0,      0,      0,      1, 0,
  ];

  // 亮度增加矩阵 (R,G,B 各增加 50)
  const brightnessMatrix = [
    1, 0, 0, 0, 50,
    0, 1, 0, 0, 50,
    0, 0, 1, 0, 50,
    0, 0, 0, 1, 0,
  ];

  // 反色矩阵
  const invertMatrix = [
    -1, 0, 0, 0, 255,
    0, -1, 0, 0, 255,
    0, 0, -1, 0, 255,
    0, 0, 0, 1, 0,
  ];

  return (
    <Canvas style={styles.canvas}>
      {/* 原始图像 */}
      <Image image={image} x={10} y={10} width={120} height={80} fit="cover" />

      {/* 灰度化图像 */}
      <Image image={image} x={140} y={10} width={120} height={80} fit="cover">
        <Paint>
          <ColorMatrix matrix={grayscaleMatrix} />
        </Paint>
      </Image>

      {/* 亮度增加图像 */}
      <Image image={image} x={10} y={100} width={120} height={80} fit="cover">
        <Paint>
          <ColorMatrix matrix={brightnessMatrix} />
        </Paint>
      </Image>

      {/* 反色图像 */}
      <Image image={image} x={140} y={100} width={120} height={80} fit="cover">
        <Paint>
          <ColorMatrix matrix={invertMatrix} />
        </Paint>
      </Image>
    </Canvas>
  );
};

const styles = StyleSheet.create({
  canvas: {
    width: 300,
    height: 200,
    borderWidth: 1,
    borderColor: 'gray',
  },
});

export default ColorFilterDemo;

通过组合 ImageFilterColorFilter,我们可以实现各种复杂的实时图像处理效果。由于这些操作都在 GPU 上执行,因此即使是 120FPS 的视频流或图像序列,也能保持极高的性能。

7. 高性能动画:react-native-skiaReanimated 的强强联合

实现 120FPS 动画是 react-native-skia 的一大亮点。这主要得益于其与 react-native-reanimated 的深度集成。

7.1 SkiaValue:可动画的 Skia 值

react-native-skia 引入了 SkiaValue 概念,它是一个特殊的引用类型,其值可以在 UI 线程上更新。你可以使用 useValue Hook 来创建它:

import { useValue } from '@shopify/react-native-skia';

const myValue = useValue(0); // 创建一个初始值为 0 的 SkiaValue
// myValue.current 可以读取和设置当前值

SkiaValue 自身不能直接动画化,但它可以作为 ReanimatedSharedValue 的包装器,或者在 Reanimated 的 Worklet 中被直接修改。

7.2 useComputedValue:响应式计算

useComputedValue 允许你基于一个或多个 SkiaValueSharedValue 派生出一个新的 SkiaValue。当依赖的 Value 改变时,useComputedValue 的回调函数会在 UI 线程上重新执行,并更新其结果。

import { useValue, useComputedValue } from '@shopify/react-native-skia';
import { useSharedValue, withTiming } from 'react-native-reanimated';
import { useEffect } from 'react';

const AnimationExample = () => {
  const rotation = useSharedValue(0); // Reanimated SharedValue
  const xOffset = useValue(0); // SkiaValue

  useEffect(() => {
    // 使用 Reanimated 驱动 rotation 动画
    rotation.value = withTiming(360, { duration: 2000 });
    // 直接修改 SkiaValue
    xOffset.current = 100;
  }, []);

  // 派生出一个新的 SkiaValue,基于 rotation 和 xOffset
  const animatedPosition = useComputedValue(() => {
    // 这里的代码会在 UI 线程执行
    const angle = rotation.value * Math.PI / 180;
    const newX = 150 + Math.sin(angle) * xOffset.current;
    const newY = 150 + Math.cos(angle) * xOffset.current;
    return { x: newX, y: newY };
  }, [rotation, xOffset]); // 依赖项

  return (
    <Canvas style={{ width: 300, height: 300 }}>
      {/* 使用 animatedPosition.current.x 和 animatedPosition.current.y */}
      <Circle cx={animatedPosition.current.x} cy={animatedPosition.current.y} r={20} color="red" />
    </Canvas>
  );
};

7.3 react-native-reanimated 集成:UI 线程动画

为了实现真正的 120FPS 动画,我们需要利用 react-native-reanimated 的 Worklet 机制。这允许动画逻辑在 UI 线程上执行,完全脱离 JavaScript 线程。

react-native-skia 的组件可以直接接受 SharedValue 作为属性。

示例:一个跳动的球

import { Canvas, Circle, Paint } from '@shopify/react-native-skia';
import { useSharedValue, withSpring, useDerivedValue, cancelAnimation, useFrameCallback } from 'react-native-reanimated';
import React, { useEffect } from 'react';
import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native';

const { width, height } = Dimensions.get('window');

const BouncingBall = () => {
  const y = useSharedValue(50); // 球的 Y 坐标
  const velocity = useSharedValue(0); // 球的 Y 轴速度
  const gravity = 0.5; // 重力加速度
  const restitution = 0.8; // 恢复系数 (弹性)
  const floorY = height - 50; // 地板 Y 坐标

  // 使用 useFrameCallback 替代 requestAnimationFrame,在 UI 线程执行
  const frameCallback = useFrameCallback(({ timeSinceLastFrame }) => {
    'worklet'; // 标记为 Worklet

    if (timeSinceLastFrame === null) return; // 第一次调用时 timeSinceLastFrame 可能为 null

    // 物理模拟步进
    velocity.value += gravity; // 施加重力
    y.value += velocity.value; // 更新位置

    // 碰撞检测与反弹
    if (y.value >= floorY) {
      y.value = floorY; // 修正位置
      velocity.value *= -restitution; // 反弹并衰减速度
      if (Math.abs(velocity.value) < 1) { // 速度过低时停止动画
        velocity.value = 0;
        cancelAnimation(frameCallback); // 停止帧回调
      }
    }
  });

  const startAnimation = () => {
    y.value = 50; // 重置位置
    velocity.value = 0; // 重置速度
    frameCallback.start(); // 启动帧回调
  };

  useEffect(() => {
    startAnimation();
    return () => cancelAnimation(frameCallback); // 组件卸载时停止
  }, []);

  return (
    <View style={styles.container}>
      <Canvas style={styles.canvas}>
        <Circle cx={width / 2} cy={y} r={40}>
          <Paint color="orange" antiAlias={true} />
        </Circle>
      </Canvas>
      <Pressable onPress={startAnimation} style={styles.button}>
        <Text style={styles.buttonText}>Restart Animation</Text>
      </Pressable>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f0f0f0',
  },
  canvas: {
    flex: 1,
    width: '100%',
    height: '100%',
  },
  button: {
    position: 'absolute',
    bottom: 50,
    alignSelf: 'center',
    backgroundColor: 'blue',
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderRadius: 5,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
  },
});

export default BouncingBall;

在这个例子中:

  • yvelocity 都是 useSharedValue 创建的,它们的值更新会在 UI 线程进行。
  • useFrameCallback 提供了一个在 UI 线程上每帧执行的回调函数,并且通过 'worklet' 标记使其编译为 Worklet。
  • Circle 组件的 cy 属性直接绑定到 y 这个 SharedValue,Skia 会自动在 UI 线程上更新圆的位置并重绘。

这种模式完全绕过了 JavaScript 线程,实现了原生级别的 120FPS 动画。

7.4 复杂路径动画与 SVG 路径变形

react-native-skia 还可以动画化 Path 的形状。你可以使用 useSharedValueuseDerivedValue 来动态生成路径字符串。

例如,创建一个路径变形动画:

import { Canvas, Path, Paint } from '@shopify/react-native-skia';
import { useSharedValue, withRepeat, withTiming, useDerivedValue } from 'react-native-reanimated';
import React, { useEffect } from 'react';
import { Dimensions } from 'react-native';

const { width } = Dimensions.get('window');

const PathMorphAnimation = () => {
  const animationProgress = useSharedValue(0);

  useEffect(() => {
    animationProgress.value = withRepeat(withTiming(1, { duration: 2000 }), -1, true); // 0 -> 1 -> 0 循环
  }, []);

  // 定义两个路径的 SVG 字符串
  const pathA = "M 50 50 L 150 50 L 150 150 L 50 150 Z"; // 方形
  const pathB = "M 50 100 C 50 50, 150 50, 150 100 C 150 150, 50 150, 50 100 Z"; // 菱形或水滴状

  // 使用 useDerivedValue 在 UI 线程计算插值路径
  const animatedPath = useDerivedValue(() => {
    'worklet';
    // 这里我们假设有一个 path 插值函数,例如 interpolatePath
    // 实际项目中可能需要一个更复杂的库来做 SVG path 的插值
    // 简化示例:这里只是演示如何使用 derived value
    const pA = Skia.Path.MakeFromSVGString(pathA);
    const pB = Skia.Path.MakeFromSVGString(pathB);

    if (!pA || !pB) {
        return Skia.Path.Make();
    }

    // 这是一个示意性的插值,Skia 本身没有直接的 Path 插值函数
    // 真实的 Path 插值通常需要处理路径段的匹配和点之间的插值
    // For a real implementation, you might use a library like 'd3-interpolate-path'
    // or implement a custom logic based on the path commands and points.
    // For simplicity, let's just return one of them based on progress
    if (animationProgress.value < 0.5) {
        return pA;
    } else {
        return pB;
    }
  }, [animationProgress]);

  // 真正的路径插值需要一个更复杂的函数,例如:
  // const interpolatedPath = interpolatePath(pathA, pathB, animationProgress.value);
  // 对于 `react-native-skia`,可能需要手动插值路径的各个点或使用外部库。
  // 为了本讲座的简洁性,我们将跳过复杂的 Path 插值实现细节,
  // 重点在于 `useDerivedValue` 在 UI 线程计算动画值的能力。

  const pathPaint = Skia.Paint();
  pathPaint.setColor(Skia.Color('teal'));
  pathPaint.setStyle(Skia.PaintStyle.Fill);
  pathPaint.setAntiAlias(true);

  return (
    <Canvas style={{ width: width, height: 200, borderWidth: 1, borderColor: 'gray' }}>
      <Path path={animatedPath} paint={pathPaint} />
    </Canvas>
  );
};

export default PathMorphAnimation;

注意: 上述 animatedPathuseDerivedValue 中的路径插值部分是一个简化示例。Skia 并没有内置的 SVG 路径字符串直接插值功能。实现真正的路径变形通常需要:

  1. 将 SVG 路径解析为一系列点和命令。
  2. 确保两个路径有兼容的结构(相同数量的命令和点)。
  3. 对对应的点进行线性插值或贝塞尔曲线插值。
  4. 将插值后的点重新构建成新的 SVG 路径字符串。
    这通常需要一个外部的 JavaScript 库(如 d3-interpolate-path)或自定义实现,并且需要确保插值逻辑在 Worklet 中可用。

8. 性能优化策略

即使 react-native-skia 提供了强大的性能,不当的使用仍然可能导致性能问题。以下是一些优化策略:

  1. 最小化重绘区域:Skia 智能地只重绘需要更新的区域。但如果你在一个大画布上频繁更新一个小元素,确保只有这个小元素的绘制指令被重新执行。避免不必要的 Canvas 重新渲染。
  2. 利用 useMemouseCallback:对于 Skia 组件的 props,特别是那些复杂的对象(如 PaintPathShader 的配置对象),使用 useMemouseCallback 来缓存它们,避免在每次组件渲染时都创建新的对象实例,从而减少不必要的重绘。
  3. 避免在 Paint 内部创建复杂对象:例如,不要在 Paintchildren 中每次都创建新的 LinearGradient 实例,如果它的属性没有变化。
  4. 合理使用 GroupGroup 可以批处理变换和 Paint 属性。对一组需要应用相同变换或 Paint 的元素使用 Group 可以提高效率。
  5. 着色器优化
    • 计算复杂性:GLSL 着色器越复杂,每像素的计算量越大,GPU 压力也越大。尽量简化着色器逻辑。
    • 纹理采样:过多的纹理采样操作会降低性能。
    • uniform 变量:减少 uniform 变量的数量,避免频繁更新。
  6. Reanimated Worklet 最佳实践
    • 确保所有动画逻辑都在 Worklet 中执行,避免回退到 JS 线程。
    • Worklet 内部避免复杂的非数学计算、网络请求或访问非共享变量。
    • 避免在 Worklet 中创建大量临时对象。
  7. 内存管理
    • 图像:加载大尺寸图像时要小心,确保图像资源得到有效管理和释放。useImage 会自动管理,但如果你手动创建 Skia.Image,要注意其生命周期。
    • 路径:复杂的路径可能会占用较多内存。
  8. 使用调试工具
    • React Native Debugger:用于查看 JS 线程性能。
    • Chrome DevTools (Performance Tab):分析 JS 线程和渲染性能。
    • 原生调试工具 (Xcode Instruments, Android Studio Profiler):深入分析 GPU 和 CPU 使用情况。
    • Skia 提供了自己的调试工具(如 SKP 文件),但集成到 react-native-skia 中可能需要更高级的配置。

9. 展望未来:移动图形的新篇章

react-native-skia 不仅仅是一个简单的绘图库,它是 React Native 生态系统在图形渲染领域的一个重大飞跃。它将 Google 强大的 Skia 引擎带到了 React Native 开发者手中,以声明式、高性能的方式解决了许多传统移动图形开发的痛点。

通过 react-native-skia,我们可以:

  • 构建出与 Flutter 媲美的高性能自定义 UI。
  • 实现复杂的实时数据可视化,如交互式图表、仪表盘。
  • 开发具备电影级视觉效果的图像处理应用和滤镜。
  • 创建流畅、响应式的游戏和动画体验。
  • 突破 React Native 在高性能图形方面的局限性。

随着移动设备硬件的不断升级和用户对视觉体验要求的提高,react-native-skia 必将在未来的 React Native 应用开发中扮演越来越重要的角色。它赋予了开发者前所未有的自由度和能力,去创造那些曾经被认为是原生专属的、令人惊叹的视觉交互。

本次讲座就到这里。感谢大家的参与!希望今天的分享能为大家在 React Native 中探索高性能图形渲染打开一扇新的大门。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注