PythonIDE Docs
中文
简体中文

widget 时间线和动画

timeline/entry、state 数字动画、flip 翻页与 countdown 倒计时。

Widget 动画由 WidgetKit 在数据更新时触发:timeline entry 切换、AppIntent 改写 widget.state,或系统按计划刷新。不是 App 里的 60fps 循环动画。

边界:本篇讲 w.timelinewidget.entry、数字过渡与 flip。布局防裁切见 布局与尺寸;按钮触发 state 见 状态和交互

#本篇目标

说明
两类数据源widget.state(桌面点击)与 widget.entry(时间线条目字段)
数字动画.content_transition("numericText") + .monospaced_digit() + .id(...)
时间线w.timeline(entries=[...], update=, after=) 声明未来快照
翻页块w.flip(current, previous=...) 配合 entry 前后值
系统计时w.countdown / w.timer_text — WidgetKit 原生倒计时
限制无持续循环;刷新时机由系统调度

#快速开始

桌面点「消费」后余额数字滚动过渡——这是 state + numericText,不依赖 timeline。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget

balance = widget.state.int("balance", 1280)
accent = widget.param.color("主色", "#10B981")
num_size = widget.family_value(42, small=34, large=52)

w = widget.Widget(background=("#ECFDF5", "#064E3B"), padding=14)
w.text("零钱罐", size=14, color="secondary").line_limit(1)
(
    w.value(balance.text("¥{}"))
    .id("balance")
    .font_size(num_size)
    .monospaced_digit()
    .content_transition("numericText")
    .line_limit(1)
    .min_scale(0.65)
)
(
    w.button("消费 ¥10", action=balance.decrement(by=10), background=accent, color="#FFFFFF")
    .line_limit(1)
    .min_scale(0.72)
)
w.render()

要点:

  • balance.text("¥{}"):把 state 格式化成带货币符号的展示串,同时保留 state 绑定。
  • .content_transition("numericText"):数值变化时系统数字滚动动画。
  • .id("balance"):给变化节点稳定身份,动画更可靠。
  • 预览里点按钮即可看动画;桌面需发布后在小组件上点击。

#心智模型

text
                    ┌─────────────────────────────────────┐
                    │         WidgetKit 刷新时机           │
                    └─────────────────────────────────────┘
         系统调度 / timeline 到期 / AppIntent 改 state
                              ↓
              当前 entry 字段 或  state 新值 写入快照
                              ↓
        节点带 content_transition / flip / animation / transition
                              ↓
                    系统播放一次过渡动画

#widget.state vs widget.entry

widget.statewidget.entry
谁写入桌面 AppIntent(按钮、开关)w.timeline entries 每条快照
典型用途计数器、余额、开关按计划变化的展示数、翻页前后值
绑定写法widget.state.int("key", 0)widget.entry.int("price", 0.58)
格式化count.text("{} 次")price.text("{:.2f}")
动画触发点击后 state 变预览/桌面切到下一 entry

同一张卡片可以同时有 state(交互)和 entry(定时),但动画字段要各绑各的 key,不要混用。

#动画 API 怎么选

API适合
.content_transition("numericText")value() / 数字 state / entry 变化
.content_transition("opacity")文案淡入淡出
.animation("smooth", value_by="price")entry 字段变化时额外平滑(常配合 numericText)
.transition("push", edge="bottom")节点插入/替换方向
w.flip(current, previous=...)翻页钟、日历块、前后对比数字
w.countdown(..., target=)距某时刻剩余时间(系统驱动)

#时间线示例

#分时电价(timeline + entry + 数字动画)

声明 3 条未来快照;price / prev_price 为 entry 字段。预览里时间线会按条目轮播(Studio 约每秒切一帧)。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget
from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
price = widget.entry.float("price", 0.62)
prev_price = widget.entry.float("prev_price", 0.68)
accent = widget.param.color("强调色", "#F59E0B")

w = widget.Widget(background=("#FFFBEB", "#292524"), padding=14)
w.timeline(
    entries=[
        {"date": now.isoformat(), "price": 0.68, "prev_price": 0.72},
        {"date": (now + timedelta(hours=1)).isoformat(), "price": 0.62, "prev_price": 0.68},
        {"date": (now + timedelta(hours=2)).isoformat(), "price": 0.55, "prev_price": 0.62},
    ],
    update="end",
)
w.text("分时电价", size=14, color="secondary").line_limit(1)
(
    w.value(price.text("{:.2f}"), unit="元/度")
    .content_transition("numericText")
    .animation("smooth", duration=0.35, value_by="price")
    .monospaced_digit()
    .line_limit(1)
    .min_scale(0.72)
)
if not widget.context.is_family("small"):
    (
        w.text(prev_price.text("上一档 {:.2f}"), size=11, color="secondary")
        .line_limit(1)
        .min_scale(0.72)
    )
w.render()
  • 每条 entry 是 dict必须含 date(ISO 8601 字符串,建议 UTC)。
  • entry 里其它 key(priceprev_price)与 widget.entry.*("price", ...) 的 name 对应。
  • update="end":在最后一条 entry 之后请求刷新(映射 WidgetKit .atEnd)。

#翻页分钟(flip

flip 需要当前值previous 两个 entry 字段;适合时钟、日历翻页。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget
from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
minute = widget.entry.int("minute", 41)
prev_minute = widget.entry.int("prev_minute", 40)
accent = widget.param.color("数字色", "#38BDF8")

flip_w = widget.family_value(110, small=88, large=130)
flip_h = widget.family_value(72, small=58, large=88)
flip_sz = widget.family_value(44, small=32, large=52)

w = widget.Widget(background=("#0B1220", "#020617"), padding=16)
w.timeline(
    entries=[
        {"date": now.isoformat(), "minute": 41, "prev_minute": 40},
        {"date": (now + timedelta(minutes=1)).isoformat(), "minute": 42, "prev_minute": 41},
        {"date": (now + timedelta(minutes=2)).isoformat(), "minute": 43, "prev_minute": 42},
    ],
    update="end",
)
w.text("当前分钟", size=13, color="#94A3B8").line_limit(1).min_scale(0.72).line_limit(1)
(
    w.flip(
        minute.text("{:02d}"),
        previous=prev_minute.text("{:02d}"),
        width=flip_w,
        height=flip_h,
        size=flip_sz,
        color="#F8FAFC",
        background="#1E293B",
        corner_radius=10,
        direction="up",
    )
    .id("flip-minute")
)
w.render()
  • minute.text("{:02d}"):entry 整数格式化为两位(与 Python 占位符一致)。
  • direction="up" / "down" 控制翻页方向;尺寸用 family_value 防 small 溢出。

#阶段文案(entry + transition

非数字也可用 entry 驱动;文案切换加 content_transition / transition

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget
from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc).replace(second=0, microsecond=0)
phase = widget.entry.str("phase", "候场")
accent = widget.param.color("主色", "#8B5CF6")

w = widget.Widget(background=("#FAF5FF", "#1E1B4B"), padding=14)
w.timeline(
    entries=[
        {"date": now.isoformat(), "phase": "候场"},
        {"date": (now + timedelta(minutes=3)).isoformat(), "phase": "演讲进行中"},
        {"date": (now + timedelta(minutes=20)).isoformat(), "phase": "问答环节"},
    ],
    update="end",
)
w.text("活动状态", size=14, color="secondary").line_limit(1)
(
    w.text(phase, size=18, weight="semibold", color=accent)
    .id("phase-label")
    .content_transition("opacity")
    .transition("push", edge="bottom")
    .line_limit(1)
    .min_scale(0.72)
)
w.render()

#系统倒计时(无需手写 entries)

距目标时刻的剩余时间由 WidgetKit 自动走时;适合会议、车次、截止时间。

预期效果:小组件预览面板显示与脚本一致的布局与交互。

python
import widget
from datetime import datetime, timedelta, timezone

eta = datetime.now(timezone.utc) + timedelta(hours=1, minutes=25)
accent = widget.param.color("主色", "#6366F1")

w = widget.Widget(background=("#EEF2FF", "#1E1B4B"), padding=14)
w.text("会议倒计时", size=14, weight="semibold").line_limit(1).min_scale(0.72)
(
    w.countdown(
        "距离会议",
        target=eta.isoformat(),
        subtitle="三楼 302 · 可提前 5 分钟入场",
        accent=accent,
    )
)
w.render()
  • target 接受 ISO 8601 字符串或 datetime
  • 底层使用 timer_text;与 w.timeline 互补:一个管「走到某时刻」,一个管「预先排好的多条快照」。

#w.timeline 刷新策略

update行为
"after"(默认)after 指定时刻之后请求下次刷新;或用 interval(秒)
"end" / "atEnd"在最后一条 entry 之后刷新
"never"不再自动刷新
"rapid"向系统请求更频繁刷新;不保证实时频率,仍受 WidgetKit 预算合并
python
from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc)
w.timeline(
    entries=[{"date": now.isoformat(), "value": 1}],
    update="after",
    after=(now + timedelta(minutes=30)).isoformat(),
)

# 或用间隔秒数(桌面映射为 after = now + interval)
w.timeline(entries=[...], update="after", interval=900)

注意:

  • 没有 timeline 时,桌面主要靠系统刷新预算 + 用户点击改 state。
  • update="rapid" 适合「希望尽量跟紧」的场景(如短周期电价、临近截止的倒计时),但仍是请求而非定时器;系统可能合并或延后刷新,构建诊断可能给出 warning。
  • 预览里 Studio 轮播 entry(约每秒一帧)不等于桌面刷新频率;桌面由 WidgetKit 调度。
  • .animation(..., duration=, value_by=)duration 控制过渡时长;value_by 填 entry 字段名(如 "price"),与 content_transition("numericText") 常一起用。

#推荐写法速查

#交互数字(state)

python
score = widget.state.int("score", 0)
(
    w.value(score, unit="分")
    .id("score")
    .monospaced_digit()
    .content_transition("numericText")
)
w.button("+", action=score.increment())

#时间线数字(entry)

python
amount = widget.entry.int("amount", 100)
w.timeline(entries=[
    {"date": "2026-06-10T08:00:00Z", "amount": 100},
    {"date": "2026-06-10T09:00:00Z", "amount": 128},
], update="end")
(
    w.value(amount.text("{}"))
    .content_transition("numericText")
    .animation("smooth", value_by="amount")
)

#稳定身份

变化频繁的节点建议 .id("唯一串");entry 绑定节点也可自动生成 entry:price 类身份,显式 id 更利于排查。


#常见错误

错误写法后果修正
只有 value(count)content_transition数字跳变无动画.content_transition("numericText")
widget.state 绑 timeline 字段entry 不随时间线变timeline 字段用 widget.entry.*
entry 无 date顺序错乱或只用间隔推算每条写 ISO date
flip 只有当前值无 previous翻页效果弱同时声明 previous=prev_*.text(...)
期望 60fps 循环Widget 不支持改用 timeline 分段或 countdown
改 timeline 不重新发布桌面仍旧快照重新运行发布

#失败路径

现象处理
点击按钮数字不动确认 action 来自 widget.state;已加 content_transition
预览 timeline 不轮播检查 entries 非空、date 合法;重新运行预览
数字无动画、直接跳.monospaced_digit().id(...)content_transition
flip 不翻页确认 previous 绑 entry;timeline 至少 2 条
桌面时间不更新发布最新构建;countdowntarget 是否在未来
预览正常、桌面无动画系统可能合并刷新;删小组件重装;查 排错
动画卡顿或缺失减少同屏动画节点;small 降低 font_size / flip 尺寸

#相关文档

文档用途
widget 概览总览
状态和交互widget.state、AppIntent
布局与尺寸family_value、防裁切
从脚本到桌面发布后在桌面验证
排错预览/桌面不一致
API 参考timelineentryflip、修饰符签名

相关 API:widget-api-reference.md