PythonIDE Docs
中文
简体中文

动画

animate、animation、transition、matched geometry 和 phase animator。

appui 提供两类动画能力:隐式动画.animation(type, value) 在依赖变化时插值)与 显式动画appui.animate(action, type) 包裹一次状态更新)。此外还有 过渡.transition)、变换.scale_effect.rotation_effect.rotation_3d_effect)、共享几何.matched_geometry_effect)、内容过渡.content_transition)与 相位动画.phase_animator)。

从 AppUI Presentation Engine 起,show_**_presented 等呈现状态字段变化会自动触发 spring 动画;sheet / alert / cover 的进出通常不必再手写 appui.animate()

第四档 PresentationCoordinator 会在注册表有效时跳过 body() 与整树 JSON,直接更新原生 presentation binding;可用 appui.presentation_present / appui.presentation_dismiss_all 显式控制。完整规范见 Presentation Engine Spec


#预期效果

示例会展示隐式动画、显式动画、转场和共享几何效果如何响应状态变化。

#概念速览

能力API
隐式动画.animation(type='spring', value=...)type 可为 defaultlineareaseIneaseOuteaseInOutspringinterpolatingSpring
显式动画appui.animate(action, type)
出现 / 消失.transition(type)opacityslidescalemovepushidentity
2D / 3D 变换.scale_effect.rotation_effect.rotation_3d_effect
共享元素.matched_geometry_effect(ns_id, namespace, is_source)
文本或数字切换.content_transition(type)
循环呼吸效果.phase_animator(phases)

#隐式 vs 显式:怎么选

  • 隐式:适合「依赖项变化 → 自动插值」的 UI,如计数、开关、列表插入;把 .animation(..., value=state.x) 放在 依赖该字段的子树 上。
  • 显式:适合一次事务内多处 State 同步更新(例如先收起面板再改标题)。把更新逻辑放进命名函数,再用 appui.animate(update_state, "spring") 保证同一帧动画曲线一致。

#animationtype 取值

defaultlineareaseIneaseOuteaseInOutspringinterpolatingSpring。未列出的字符串请避免使用,以免系统忽略。


#基础示例

appui.animate 更新计数,并对数字附加 .animation('spring', value=...)

python
import appui

state = appui.State(score=0)


def decrease_value():
    state.score -= 1


def increase_value():
    state.score += 1


def decrease_score():
    appui.animate(decrease_value, "easeOut")


def increase_score():
    appui.animate(increase_value, "spring")


def body():
    return appui.NavigationStack(
        appui.VStack(
            [
                appui.Text(f"得分: {state.score}")
                .font("largeTitle")
                .bold()
                .animation("spring", value=state.score),
                appui.HStack(
                    [
                        appui.Button(
                            "−",
                            action=decrease_score,
                        ).button_style("bordered"),
                        appui.Button(
                            "+",
                            action=increase_score,
                        ).button_style("bordered_prominent"),
                    ],
                    spacing=24,
                ),
            ],
            spacing=24,
        )
        .padding()
        .navigation_title("显式动画")
    )


appui.run(body, state=state)

#进阶示例

transition + scale_effect + rotation_effect + rotation_3d_effect + matched_geometry_effect + content_transition + phase_animator

python
import appui

state = appui.State(show_panel=False, label="轻点切换")


def toggle_panel():
    state.batch_update(
        show_panel=not state.show_panel,
        label="面板已打开" if not state.show_panel else "面板已关闭",
    )


def body():
    panel = (
        appui.RoundedRectangle(corner_radius=18)
        .fill("systemTeal")
        .frame(width=120, height=120)
        .scale_effect(0.92)
        .rotation_effect(6)
        .matched_geometry_effect(ns_id="card", namespace="demo", is_source=True)
        .transition("scale")
    )
    placeholder = (
        appui.Circle()
        .fill("systemGray4")
        .frame(width=120, height=120)
        .matched_geometry_effect(ns_id="card", namespace="demo", is_source=False)
        .transition("opacity")
    )
    return appui.NavigationStack(
        appui.ScrollView(
            [
                appui.VStack(
                    [
                        appui.Text(state.label)
                        .font("title2")
                        .content_transition("interpolate"),
                        appui.Button(
                            action=toggle_panel,
                            content=appui.Text("切换面板").padding(),
                        ).button_style("bordered_prominent"),
                        panel if state.show_panel else placeholder,
                        appui.Text("3D 倾斜")
                            .font("footnote")
                            .foreground_color("secondaryLabel"),
                        appui.Rectangle()
                        .fill("systemOrange")
                        .frame(width=100, height=44)
                        .rotation_3d_effect(18, x=0, y=1, z=0),
                        appui.Text("相位动画")
                            .font("footnote")
                            .foreground_color("secondaryLabel"),
                        appui.Capsule()
                        .fill("systemPink")
                        .frame(width=160, height=36)
                        .phase_animator([0, 0.4, 1.0, 0.4, 0]),
                    ],
                    spacing=20,
                )
                .padding()
            ]
        )
        .navigation_title("动画组合")
    )


appui.run(body, state=state)

#常见误区

  1. value 与隐式动画.animation(..., value=state.x) 仅在 value 引用发生变化时触发动画;若传入常量则不会按预期刷新。
  2. appui.animate 的第一个参数:必须是 无参可调用对象(推荐命名函数里批量改 State),不要写成 appui.animate(state.count + 1)
  3. matched_geometry_effect:成对视图需 相同的 namespacens_id,且通常配合显式布局;在复杂导航栈里要注意 is_source 切换时机。
  4. phase_animator:依赖 iOS 17+ 能力,在旧系统上可能降级或无动画。
  5. transition 与条件渲染:过渡只在视图插入/移除时生效;若仅修改子视图文字,应优先 .content_transition
  6. interpolatingSpringspring:二者曲线不同;若出现振荡或回弹过大,优先改回 defaulteaseInOut 验证是否由曲线本身引起。
  7. rotation_3d_effect 与透视:极端角度可能导致子视图不可读;建议限制在 ±45° 内做 UI 提示级动效。

#练习题

  1. ZStack 叠放两个 Text,通过 state 切换 id(...),并分别设置 .transition('slide').transition('move')
  2. 给按钮增加 .sensory_feedback(style='success', trigger=str(state.score)),观察与 animate 联动的手感。
  3. 对照 入口函数 API,说明为何在回调里应优先使用 batch_update

#延伸阅读(仓库内)

  • 修饰符 API:查看 animationtransitionphase_animator 等签名。

#附录:transitionopacity 最小片段

python
import appui

flag = True


def toggle():
    global flag
    flag = not flag


root = appui.Group(
    [
        appui.Text("A").opacity(1).transition("opacity"),
        appui.Text("B").transition("push"),
    ]
)
assert root is not None

#调试建议

若动画「时有时无」,先在浏览器式调试思路下 二分法 排查:去掉 matched_geometry_effectphase_animator 等高级修饰符,仅保留 .animation(..., value=...),确认基础路径正常后再逐项加回。


#DrawingContext 与动画的关系

本章专注视图修饰符与 animate();若需要在画布上做连续帧动画,常见做法是:用 State 保存相位或采样数组,在 Timer 或后台线程里更新 State,由 Canvas(..., commands=...)DrawingContext 重建命令列表。此类模式属于「数据驱动重绘」,而不是 transition 插值。


#ReactiveState / 实时属性通道

高频控件(如 Slider)在 ReactiveState 场景下可能走实时属性通道;此时 .animation(..., value=...) 仍绑定在普通 Text 上即可。避免同一字段既高频写入,又驱动整棵页面频繁重建。若动画曲线和预期不一致,先拆分「需要动画的只读展示」与「高频写控件」,再查看 状态 API