革命性2D UI开发:Scenic全指南

革命性2D UI开发:Scenic全指南

引言:告别嵌入式UI开发的复杂性

你是否还在为嵌入式设备构建UI而烦恼?传统框架要么过于臃肿,要么缺乏跨平台一致性,更不用说与Elixir/OTP的无缝集成了。Scenic的出现彻底改变了这一局面——作为一款基于Elixir/OTP栈构建的客户端应用库,它让你能够在MacOS、Ubuntu、Nerves/Linux等所有支持平台上构建出运行一致的应用。本文将带你深入探索Scenic的核心架构、组件系统和实战技巧,让你在30分钟内从零开始构建第一个响应式UI。

读完本文你将获得:

  • 掌握Scenic的核心概念与架构设计
  • 从零构建包含按钮、滑块等组件的交互界面
  • 理解场景生命周期与状态管理的最佳实践
  • 学会性能优化与跨平台部署的关键技巧
  • 解决常见问题的实用方案与代码示例

Scenic项目概述:重新定义嵌入式UI开发

项目定位与核心目标

Scenic主要面向固定屏幕连接设备(IoT),但也可用于构建便携式应用。它的设计目标包括:

核心目标具体实现
高可用性充分利用OTP监督树,创建容错、自修复的应用
轻量高效仅依赖Erlang/OTP和OpenGL,资源占用极小
自包含性设备逻辑本地运行,即使服务不可用仍能操作
可维护性模块化设计使团队能专注于新产品开发
远程访问优化网络传输,支持互联网、蜂窝网络等场景
组件复用支持UI组件打包,可跨应用共享
灵活布局采用游戏开发中的矩阵变换实现精确定位
安全性摒弃浏览器和Javascript等复杂攻击面

技术选型与边界

Scenic明确界定了其能力范围,避免陷入功能蔓延:

支持的特性

  • 2D矢量图形渲染
  • 组件化UI架构
  • 响应式布局系统
  • 事件驱动编程模型
  • 静态与动态资源管理

非目标范围

  • Web浏览器功能(不支持HTML/CSS)
  • 3D图形渲染
  • 即时模式渲染(采用保留模式)
  • 复杂动画系统(基础动画需自行实现)

快速入门:15分钟上手Scenic

环境准备与依赖安装

在开始之前,请确保你的系统满足以下要求:

  • Elixir 1.16+
  • Erlang/OTP 24+
  • OpenGL开发环境
# 安装依赖(Ubuntu示例)
sudo apt-get install libglfw3-dev libglew-dev

# 安装Scenic项目生成器
mix archive.install hex scenic_new

构建第一个应用

# 创建新项目
mix scenic.new my_app
cd my_app

# 安装依赖并运行
mix do deps.get, scenic.run

上述命令将创建一个包含四个场景的 starter 应用:

  • Splash:启动动画场景,展示过渡效果
  • Sensor:模拟温度传感器界面,演示数据更新
  • Primitives:基础图元展示,包含各种形状和样式
  • Components:组件示例,展示按钮、滑块等交互元素

项目结构解析

my_app/
├── lib/
│   ├── my_app/
│   │   ├── components/    # 自定义组件
│   │   ├── scenes/        # 应用场景
│   │   └── application.ex # 应用入口
├── config/
│   └── config.exs         # 配置文件,含视口设置
├── assets/                # 静态资源
│   ├── fonts/             # 字体文件
│   └── images/            # 图像资源
└── mix.exs                # 项目依赖

核心配置示例(config/config.exs):

config :my_app, :viewport, [
  name: :main_viewport,
  size: {700, 600},
  default_scene: {MyApp.Scene.Splash, :start_data},
  drivers: [
    [
      module: Scenic.Driver.Local,
      name: :local,
      window: [resizeable: true, title: "My Scenic App"]
    ]
  ]
]

核心概念:理解Scenic的架构设计

场景(Scene):UI的基本构建块

场景是Scenic应用的核心,每个场景都是一个GenServer进程,负责管理图形状态和响应用户输入。场景的生命周期由视口(ViewPort)自动管理,其核心职责包括:

  1. 构建和维护图形(Graph)数据结构
  2. 响应输入事件和组件消息
  3. 管理子组件的生命周期
  4. 将更新后的图形推送到视口渲染
defmodule MyApp.Scene.Example do
  use Scenic.Scene
  alias Scenic.Graph
  import Scenic.Primitives

  # 编译时构建初始图形
  @graph Graph.build(font: :roboto, font_size: 24)
         |> text("Hello Scenic", translate: {350, 300}, text_align: :center)
         |> circle(100, fill: :blue, translate: {350, 300})

  @impl Scenic.Scene
  def init(scene, _params, _opts) do
    # 初始化场景状态并推送图形
    scene = 
      scene
      |> assign(graph: @graph)
      |> push_graph(@graph)
    
    {:ok, scene}
  end

  # 处理输入事件
  @impl Scenic.Scene
  def handle_input({:cursor_button, {:btn_left, 0, _, position}}, _, scene) do
    # 点击位置在position,更新图形
    graph = 
      scene.assigns.graph
      |> Graph.modify(:circle, &circle(&1, 100, fill: :red))
    
    scene = 
      scene
      |> assign(graph: graph)
      |> push_graph(graph)
    
    {:noreply, scene}
  end
end

图形(Graph):声明式UI描述

图形是Scenic的核心数据结构,采用声明式方式描述UI。它由节点(primitives/components)构成层次结构,支持样式继承和变换组合。图形具有不可变性,每次修改都会创建新的图形实例。

mermaid

图形操作的核心API

函数作用
Graph.build/1创建新图形,可设置根样式
Graph.modify/3修改指定ID的节点
Graph.add_to/3向指定组添加节点
push_graph/2将图形推送到视口渲染

图元(Primitives):UI的基本构建单元

Scenic提供了丰富的2D图元库,所有可见元素最终都由这些图元构成:

图元类型用途关键参数
text/3文本渲染font, font_size, text_align
rectangle/3矩形绘制width, height, radius (圆角)
circle/3圆形绘制radius
ellipse/3椭圆绘制{width, height}
line/3线段绘制points, stroke
path/3复杂路径commands (类似SVG路径)
group/3节点分组用于统一变换和样式
component/3组件引用module, data, opts

图元使用示例

Graph.build()
|> text("Scenic Primitives", font_size: 32, translate: {350, 50})
|> rectangle({200, 100}, fill: {:linear_gradient, [0,0, 200,100], [ {:white, 1.0}, {:blue, 1.0} ]}, translate: {250, 150})
|> circle(50, stroke: {3, :red}, fill: :yellow, translate: {450, 200})
|> line({{100, 300}, {600, 300}}, stroke: {5, :green}, cap: :round)
|> path("M100,400 Q350,300 600,400 T600,500", stroke: {2, :purple}, fill: :none)

组件(Components):可复用的UI元素

组件是Scenic的代码复用机制,本质上是特殊的场景,提供标准化的交互界面。Scenic内置了丰富的基础组件:

mermaid

自定义组件示例

defmodule MyApp.Component.Counter do
  use Scenic.Component
  import Scenic.Primitives
  alias Scenic.Graph

  # 验证输入数据
  @impl Scenic.Component
  def validate(count) when is_integer(count), do: {:ok, count}
  def validate(_), do: {:error, "计数器必须初始化为整数"}

  @impl Scenic.Scene
  def init(scene, count, opts) do
    # 构建组件图形
    graph = 
      Graph.build()
      |> text("#{count}", id: :count, font_size: 24)
      |> rectangle({60, 40}, id: :bg, fill: :light_grey, input: true)
    
    scene = 
      scene
      |> assign(graph: graph, count: count)
      |> push_graph(graph)
    
    {:ok, scene}
  end

  # 处理点击事件
  @impl Scenic.Scene
  def handle_input({:cursor_button, {:btn_left, 0, _, _}}, :bg, scene) do
    # 增加计数并更新显示
    new_count = scene.assigns.count + 1
    graph = 
      scene.assigns.graph
      |> Graph.modify(:count, &text(&1, "#{new_count}"))
    
    scene = 
      scene
      |> assign(count: new_count, graph: graph)
      |> push_graph(graph)
      |> send_parent_event({:count_updated, new_count})
    
    {:noreply, scene}
  end
end

在场景中使用组件

Graph.build()
|> MyApp.Component.Counter.add_to_graph(0, id: :counter, translate: {350, 300})
|> button("重置", id: :reset_btn, translate: {320, 350})

样式(Styles):视觉外观控制

Scenic提供了全面的样式系统,支持丰富的视觉效果,并遵循继承原则——子节点会继承父节点的样式,可在子节点上重写。

核心样式类型

样式类别包含属性
填充样式fill - 支持颜色、渐变、图像
描边样式stroke - 线宽和填充样式
字体样式font, font_size, text_align
变换样式translate, rotate, scale, pin
交互样式input - 启用输入事件
布局样式hidden, scissor (裁剪)

样式继承示例

Graph.build(font: :roboto, font_size: 20)  # 根样式
|> group(translate: {100, 100}, font_size: 24)  # 重写字体大小
|> text("继承样式")  # 继承 group 的 font_size:24
|> text("重写样式", font_size: 30, translate: {0, 50})  # 重写为30
|> end_group()
|> text("恢复根样式", translate: {100, 200})  # 恢复为根样式的20

渐变填充示例

# 线性渐变
rectangle({200, 150}, fill: {:linear_gradient, [0,0, 200,150], [ {:red, 0.0}, {:yellow, 0.5}, {:green, 1.0} ]})

# 径向渐变
circle(75, fill: {:radial_gradient, [100,100, 50, 100], [ {:blue, 0.0}, {:white, 1.0} ]})

变换(Transforms):精确控制元素位置

Scenic采用矩阵变换系统,支持平移、旋转、缩放等操作,所有变换可以组合应用:

mermaid

变换操作示例

Graph.build()
|> rectangle({100, 100}, fill: :blue)  # 无变换
|> rectangle({100, 100}, fill: :red, translate: {200, 0})  # 平移
|> rectangle({100, 100}, fill: :green, rotate: 0.785, translate: {400, 0})  # 旋转(45度)
|> rectangle({100, 100}, fill: :yellow, scale: 1.5, translate: {600, 50})  # 缩放
|> rectangle({100, 100}, fill: :purple, 
    translate: {200, 200}, 
    rotate: 0.785, 
    scale: 1.2, 
    pin: {50, 50})  # 组合变换,指定旋转中心

实战指南:构建交互式应用

事件处理与用户交互

Scenic采用事件驱动模型,所有用户输入和组件通信都通过事件系统完成。场景通过实现特定回调函数来处理不同类型的事件:

@impl Scenic.Scene
def init(scene, _params, _opts) do
  # 请求特定类型的输入事件
  scene = 
    scene
    |> request_input(:cursor_pos)  # 请求光标位置
    |> request_input(:cursor_button)  # 请求鼠标按钮事件
    |> request_input(:key)  # 请求键盘事件
  
  {:ok, scene}
end

# 处理光标移动事件
@impl Scenic.Scene
def handle_input({:cursor_pos, x, y}, _, scene) do
  # 更新光标位置显示
  graph = 
    scene.assigns.graph
    |> Graph.modify(:cursor_text, &text(&1, "光标位置: #{x}, #{y}"))
  
  {:noreply, push_graph(scene, graph)}
end

# 处理按钮点击事件
@impl Scenic.Scene
def handle_input({:cursor_button, {:btn_left, 0, _, _}}, :my_button, scene) do
  # 按钮释放事件,发送事件给父组件
  send_parent_event(scene, {:button_clicked, :my_button})
  {:noreply, scene}
end

# 处理键盘事件
@impl Scenic.Scene
def handle_input({:key, key, :press, _}, _, scene) do
  # 处理按键按下事件
  case key do
    "Escape" -> send_parent_event(scene, :close_window)
    "Enter" -> send_parent_event(scene, :submit_form)
    _ -> :ok
  end
  {:noreply, scene}
end

资源管理:字体与图像

Scenic 0.11引入了全新的资源管理系统,简化了字体、图像等静态资源的使用:

# 1. 配置资源模块
# config.exs
config :scenic, :assets, module: MyApp.Assets

# 2. 创建资源模块
defmodule MyApp.Assets do
  use Scenic.Assets.Static,
    otp_app: :my_app,
    alias: [
      background: "images/background.jpg",
      icon: "images/icon.png"
    ]
end

# 3. 在图形中使用资源
Graph.build()
|> rectangle({700, 600}, fill: {:image, :background})  # 使用别名
|> image("images/logo.png", translate: {50, 50})  # 使用路径
|> text("自定义字体", font: "fonts/custom.ttf", font_size: 36)

场景切换与导航

复杂应用通常包含多个场景,需要实现场景间的切换逻辑:

defmodule MyApp.Scene.Home do
  use Scenic.Scene
  import Scenic.Components

  @graph Graph.build()
         |> button("进入设置", id: :settings_btn, translate: {300, 250})
         |> button("查看数据", id: :data_btn, translate: {300, 320})

  @impl Scenic.Scene
  def init(scene, _params, _opts) do
    {:ok, push_graph(scene, @graph)}
  end

  @impl Scenic.Scene
  def handle_input({:click, :settings_btn}, _, scene) do
    # 切换到设置场景
    Scenic.ViewPort.set_root(scene.viewport, {MyApp.Scene.Settings, :from_home})
    {:noreply, scene}
  end

  def handle_input({:click, :data_btn}, _, scene) do
    # 切换到数据场景
    Scenic.ViewPort.set_root(scene.viewport, {MyApp.Scene.Data, []})
    {:noreply, scene}
  end
end

动态数据更新

实时数据展示是嵌入式应用的常见需求,Scenic通过高效的图形更新机制实现这一点:

defmodule MyApp.Scene.SensorMonitor do
  use Scenic.Scene
  import Scenic.Primitives
  alias Scenic.Graph

  @graph Graph.build()
         |> text("温度监测", font_size: 32, translate: {350, 50}, text_align: :center)
         |> rectangle({400, 200}, fill: :light_grey, translate: {150, 150}, id: :temp_bg)
         |> text("--- °C", id: :temp_value, font_size: 48, translate: {350, 250}, text_align: :center)
         |> line({{150, 400}, {550, 400}}, stroke: {3, :black}, id: :trend_line)

  @impl Scenic.Scene
  def init(scene, _params, _opts) do
    # 订阅传感器数据更新
    Scenic.PubSub.subscribe(:sensor_data)
    
    # 初始化趋势数据
    scene = 
      scene
      |> assign(graph: @graph, trend_data: [{150, 400}])
      |> push_graph(@graph)
    
    {:ok, scene}
  end

  # 处理传感器数据更新
  @impl Scenic.Scene
  def handle_info({:sensor_update, temp}, scene) do
    # 更新温度显示
    {new_trend, points} = update_trend(scene.assigns.trend_data, temp)
    
    graph = 
      scene.assigns.graph
      |> Graph.modify(:temp_value, &text(&1, "#{temp} °C"))
      |> Graph.modify(:trend_line, &line(&1, points))
    
    scene = 
      scene
      |> assign(trend_data: new_trend, graph: graph)
      |> push_graph(graph)
    
    {:noreply, scene}
  end

  # 更新趋势线数据
  defp update_trend(data, temp) do
    # 将温度值转换为Y坐标 (30-50°C映射到400-200像素)
    y = 400 - ((temp - 30) / 20) * 200
    x = List.last(data) |> elem(0) + 10
    
    # 保持最多40个数据点
    new_data = 
      data 
      |> Enum.concat([{x, y}])
      |> Enum.drop_while(fn {px, _} -> px < x - 400 end)
    
    {new_data, new_data}
  end
end

高级特性:释放Scenic全部潜力

脚本(Scripts):高性能绘图指令

Scenic 0.11引入了脚本系统,允许直接发送绘图指令,特别适合动态生成的复杂图形:

alias Scenic.Script

def create_custom_script do
  Script.start()
  |> Script.fill_color(:red)
  |> Script.begin_path()
  |> Script.move_to(100, 100)
  |> Script.line_to(200, 150)
  |> Script.bezier_curve_to(300, 50, 400, 250, 500, 150)
  |> Script.close_path()
  |> Script.fill()
  |> Script.stroke_color(:black)
  |> Script.stroke_width(2)
  |> Script.stroke()
  |> Script.finish()
end

# 在图形中使用脚本
Graph.build()
|> script(create_custom_script(), translate: {0, 100})

精灵图(Sprites):高效图像渲染

精灵图系统允许从单个图像文件中绘制多个子图像,减少资源加载和绘制调用:

# 1. 定义精灵图
defmodule MyApp.Sprites do
  use Scenic.Assets.Static,
    otp_app: :my_app,
    sprites: [
      buttons: [
        file: "images/buttons.png",
        frames: [
          normal: {0, 0, 100, 50},
          pressed: {0, 50, 100, 50},
          hover: {0, 100, 100, 50}
        ]
      ]
    ]
end

# 2. 在图形中使用精灵图
Graph.build()
|> sprites(MyApp.Sprites.buttons(), :normal, id: :btn, translate: {300, 300})

流纹理(Stream Textures):动态图像更新

流纹理系统支持高效更新动态图像,适用于摄像头输入、实时图表等场景:

# 1. 创建并初始化流纹理
{:ok, texture} = Scenic.Assets.Stream.Texture.build(:rgb, 640, 480, clear: :black, commit: true)
:ok = Scenic.Assets.Stream.put("camera_stream", texture)

# 2. 在图形中使用流纹理
Graph.build()
|> rectangle({640, 480}, fill: {:stream, "camera_stream"})

# 3. 更新纹理数据(在相机回调中)
def handle_frame(pixel_data) do
  # 获取当前纹理
  {:ok, texture} = Scenic.Assets.Stream.Texture.fetch("camera_stream")
  
  # 更新纹理数据(使用低级API直接操作像素)
  texture
  |> Scenic.Assets.Stream.Texture.mutable()
  |> Scenic.Assets.Stream.Texture.put_pixels(pixel_data)
  |> Scenic.Assets.Stream.Texture.commit()
  
  # 通知流系统纹理已更新
  :ok = Scenic.Assets.Stream.updated("camera_stream")
end

主题系统:统一应用风格

主题系统允许集中管理应用的颜色方案,支持动态切换:

# 1. 定义主题
defmodule MyApp.Themes do
  @primary %{
    text: :white,
    background: :dark_blue,
    accent: :light_blue,
    border: :blue,
    active: :cyan
  }
  
  @dark %{
    text: :light_grey,
    background: :black,
    accent: :grey,
    border: :dark_grey,
    active: :white
  }
  
  def get(:primary), do: @primary
  def get(:dark), do: @dark
  def get(_), do: @primary
end

# 2. 在组件中使用主题
defmodule MyApp.Component.ThemedButton do
  use Scenic.Component
  import Scenic.Primitives
  
  @impl Scenic.Component
  def init(scene, text, opts) do
    theme = Keyword.get(opts, :theme, :primary) |> MyApp.Themes.get()
    
    graph = 
      Graph.build()
      |> rrect({150, 40, 5}, fill: theme.background, stroke: {2, theme.border}, id: :btn)
      |> text(text, fill: theme.text, translate: {75, 25}, text_align: :center)
    
    scene = 
      scene
      |> assign(graph: graph, theme: theme)
      |> push_graph(graph)
    
    {:ok, scene}
  end
end

迁移指南:从0.10到0.11的关键变更

Scenic 0.11带来了多项重大改进,同时也引入了一些不兼容变更:

核心变更概览

变更类型0.10版本0.11版本
资产管理Scenic.Cache 模块Scenic.Assets.StaticStream
场景状态自定义状态结构%Scenic.Scene{} 结构体,类似Phoenix Socket
图形推送回调选项 push: graphpush_graph/2 函数
驱动系统多个驱动 (Glfw, RPi等)统一的 Scenic.Driver.Local
输入处理自动接收所有输入显式 request_input/2 请求
组件验证verify/1 回调validate/1 回调

迁移步骤

  1. 更新依赖
# mix.exs
defp deps do
  [
    {:scenic, "~> 0.11.0"},
    {:scenic_driver_local, "~> 0.11.0"}
  ]
end
  1. 更新视口配置
# 旧配置(0.10)
%{
  name: :main_viewport,
  size: {700, 600},
  default_scene: {MyApp.Scene.Main, nil},
  drivers: [
    %{
      module: Scenic.Driver.Glfw,
      name: :glfw,
      opts: [resizeable: true]
    }
  ]
}

# 新配置(0.11)
[
  name: :main_viewport,
  size: {700, 600},
  default_scene: {MyApp.Scene.Main, nil},
  drivers: [
    [
      module: Scenic.Driver.Local,
      name: :local,
      window: [resizeable: true]
    ]
  ]
]
  1. 场景状态迁移
# 旧代码(0.10)
def init(params, opts) do
  graph = Graph.build() |> text("Hello")
  {:ok, %{graph: graph}, push: graph}
end

# 新代码(0.11)
def init(scene, _params, _opts) do
  graph = Graph.build() |> text("Hello")
  scene = 
    scene
    |> assign(graph: graph)
    |> push_graph(graph)
  {:ok, scene}
end
  1. 资产系统迁移
# 旧代码(0.10)
{:ok, hash} = Scenic.Cache.File.load("assets/images/logo.png", :image)
graph |> rectangle(fill: {:image, hash})

# 新代码(0.11)
graph |> rectangle(fill: {:image, "images/logo.png"})

最佳实践:构建高质量Scenic应用

性能优化策略

  1. 图形构建优化

    • 尽量在编译时构建静态图形(使用@graph模块属性)
    • 合理使用组(group)减少修改范围
    • 避免频繁修改整个图形,优先使用Graph.modify/3
  2. 资源管理

    • 预加载所有静态资源
    • 合理使用精灵图减少纹理切换
    • 对大型图像使用适当分辨率
  3. 事件处理

    • 避免在输入事件回调中执行复杂计算
    • 使用capture_input/2release_input/2减少事件处理负担
    • 批量处理高频事件(如鼠标移动)

测试策略

Scenic应用的测试应覆盖组件行为和场景交互:

defmodule MyApp.Component.ButtonTest do
  use ExUnit.Case
  import Scenic.TestHelpers
  
  test "button sends click event when pressed" do
    # 创建测试场景
    {:ok, viewport} = Scenic.ViewPort.start({:test, 800, 600})
    {:ok, scene} = MyApp.Scene.TestScene.start(viewport)
    
    # 模拟点击按钮
    send_input(scene, {:cursor_button, {:btn_left, 0, [], {350, 300}}})
    
    # 验证事件是否发送
    assert_receive {:event, {:click, :test_button}}
  end
end

调试技巧

  1. 使用IEx进行交互式调试
iex -S mix scenic.run
  1. 图形检查工具
# 在场景中添加调试信息
defmodule MyApp.Scene.Debug do
  use Scenic.Scene
  import Scenic.Primitives
  
  def init(scene, _, _) do
    # 启用调试模式
    scene = Scene.put(scene, :debug, true)
    # ...
  end
  
  # 在更新时打印调试信息
  def handle_input(input, id, scene) do
    if scene.debug do
      IO.puts("Input received: #{inspect(input)} on #{id}")
    end
    # ...
  end
end
  1. 性能分析
# 使用:timer模块测量图形更新时间
{time, scene} = :timer.tc(fn -> push_graph(scene, graph) end)
IO.puts("Graph update took #{time} microseconds")

部署指南:跨平台发布

Nerves设备部署

Scenic特别适合在嵌入式设备上运行,结合Nerves项目可实现一键部署:

# mix.exs
def project do
  [
    app: :my_app,
    version: "0.1.0",
    elixir: "~> 1.16",
    deps: deps() ++ nerves_deps(),
    # ...
  ]
end

def nerves_deps do
  [
    {:nerves, "~> 1.10", runtime: false},
    {:nerves_system_rpi4, "~> 1.23", runtime: false},
    {:scenic_driver_local, "~> 0.11", targets: @all_targets}
  ]
end

# 部署命令
mix firmware
mix firmware.burn

桌面平台打包

对于桌面平台,可使用mix release创建独立可执行文件:

# 创建发布
MIX_ENV=prod mix release

# 运行发布
_build/prod/rel/my_app/bin/my_app start

结论:开启Scenic之旅

Scenic为嵌入式UI开发带来了Elixir/OTP的强大能力,通过声明式图形描述、组件化架构和响应式设计,让构建跨平台设备界面变得前所未有的简单。无论你是开发工业控制面板、智能家居设备还是消费电子品,Scenic都能帮助你快速交付高质量的UI解决方案。

下一步行动

  1. 访问项目仓库:https://gitcode.com/gh_mirrors/scen/scenic
  2. 尝试示例项目:mix scenic.new.example my_app
  3. 查阅API文档:https://hexdocs.pm/scenic
  4. 加入社区讨论:https://elixirforum.com/c/elixir-libraries/scenic

你准备好用Elixir构建下一个革命性的嵌入式UI了吗?立即开始你的Scenic之旅!


如果你觉得本文对你有帮助,请点赞、收藏并关注作者,获取更多Scenic开发技巧和最佳实践。下一篇我们将深入探讨自定义驱动开发和高级动画技术!

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值