feat(iced): 反馈类家族四件(Progress / Snackbar / Dialog / EmptyState)
D39 五家族并行的反馈类线(M78 / COMPFEED)。全部对消息类型泛型、不 import app::Message, 数值来自令牌或登记为 G17(令牌层没有反馈类的 ComponentMetrics 与骨架位)。 - Progress:linear(确定态 FillPortion 占比 + 官方 1750ms 双段不定态 keyframes)与 circular(40/4 环,弧长 = 数值×360°,不定态走官方 6000ms 弧长+旋转 keyframes)。 环形用几何 SVG(canvas feature 没开),组件不计时,相位由调用方给。 - Snackbar:48/68 两种高、上限宽 600(A1 ContainerMaxWidth)、12 外边距、贴顶/贴底、 action(InversePrimary)+ 关闭键(InverseOnSurface);**组件不计时**,超时归调用方。 iced 的 Length 没有 min-width ⇒ 344 的最小宽由宿主保证(登记 G17-d,与按钮 G15 同墙)。 - Dialog:scrim(黑@0.32 预合成实色,D27)+ R28 + SurfaceContainerHigh + pad 24 + 标题/正文/动作三区 + 无动作/单动作/双动作;官方 280..560 区间内取 480。 - EmptyState:**官方没有这个组件**,96 图标块/32 图标/HeadlineSmall 全部标 [估]。 - action.rs:Dialog/Snackbar 共用的文字/图标动作(内容角色作参数,状态层仍走 interaction)。 - gallery/feedback.rs:状态矩阵;家族内再切一段 ULL_FEEDBACK_PART(all|progress|snackbar| dialog|empty),开关本身住在 feedback/gallery_parts.rs,不动 gallery/mod.rs。 - feedback/probe.rs:对着真截图量像素(渲染层断言),四类几何各逐项钉住。 - feedback/tests.rs:几何/节奏/角色/scrim 的纯函数断言。 gates: fmt --all --check · clippy -p unlucky-app-iced --all-targets -D warnings · test -p unlucky-app-iced(144 通过 / 0 失败)
This commit is contained in:
1 parent
27f0d2a551
commit
36f58d7904
14 files changed
+2900
-39
No files matched your search
@@ -0,0 +1,76 @@
|
||||
//! 不定态的**节奏**:把官方的 keyframes 摊成纯函数 `phase → 进度`。
|
||||
//!
|
||||
//! ## 为什么组件不计时
|
||||
//!
|
||||
//! 契约 §2:组件不持有时钟/状态所有权。`indeterminate` 的相位由**调用方**给(`phase` 0..1,
|
||||
//! 由页面的 `Subscription` 每帧/每 tick 推进)。好处不只「符合契约」:截图 harness 也才能拍到
|
||||
//! 确定的中间帧——否则每张图都是一次抓阄。
|
||||
//!
|
||||
//! ## 数据来源(A1,`javap` 读本机 AAR 得到,不是凭记忆)
|
||||
//!
|
||||
//! ```text
|
||||
//! ~/.gradle/caches/.../material3-android/1.4.0/.../material3.aar → classes.jar
|
||||
//! javap -p -c -cp classes.jar androidx.compose.material3.ProgressIndicatorKt
|
||||
//! ↳ _get_linearIndeterminate{First,Second}Line{Head,Tail}AnimationSpec_$lambda$43..46
|
||||
//! ```
|
||||
//!
|
||||
//! Compose 的 `KeyframesSpec.at(value, time).using(easing)` 是「**从该点出发的那一段**用这条缓动」,
|
||||
//! 所以 [`KeyPoint::easing`] 的语义就是段起点的缓动;第一点之前 / 最后一点之后一律**保持端点值**
|
||||
//! (Compose 对未覆盖的区间就是保持,这也是「段跑到头以后停在满格」的来源)。
|
||||
|
||||
use unlucky_design::motion::Easing;
|
||||
|
||||
/// 一个关键帧:`ms` 时刻的值 + **从它出发那一段**的缓动。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct KeyPoint {
|
||||
/// 相对周期起点的毫秒数。
|
||||
pub ms: u32,
|
||||
/// 该时刻的进度(0..1,环形是「弧长占比」,旋转是「度数 / 360」)。
|
||||
pub value: f32,
|
||||
/// 从本点出发到下一个点的那一段使用的缓动。
|
||||
pub easing: Easing,
|
||||
}
|
||||
|
||||
/// 一条 keyframes 曲线(一个周期内可采样任意相位)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Keyframes {
|
||||
/// 周期长度(毫秒)。`phase` 按它折算成时间,再对周期取模(相位可越界/可循环)。
|
||||
pub cycle_ms: u32,
|
||||
/// 关键帧(按 `ms` 升序)。
|
||||
pub points: &'static [KeyPoint],
|
||||
}
|
||||
|
||||
impl Keyframes {
|
||||
/// 采样:`phase`(任意实数,按周期取模)→ 进度。
|
||||
///
|
||||
/// 空列表给 0;`cycle_ms == 0` 时直接给首点的值(没有可循环的时间轴)。
|
||||
pub fn sample(&self, phase: f32) -> f32 {
|
||||
let Some(first) = self.points.first() else {
|
||||
return 0.0;
|
||||
};
|
||||
if self.cycle_ms == 0 || !phase.is_finite() {
|
||||
return first.value;
|
||||
}
|
||||
|
||||
let cycle = self.cycle_ms as f32;
|
||||
let ms = phase.rem_euclid(1.0) * cycle;
|
||||
if ms <= first.ms as f32 {
|
||||
return first.value;
|
||||
}
|
||||
|
||||
for pair in self.points.windows(2) {
|
||||
let (from, to) = (pair[0], pair[1]);
|
||||
if ms > to.ms as f32 {
|
||||
continue;
|
||||
}
|
||||
let span = to.ms as f32 - from.ms as f32;
|
||||
if span <= 0.0 {
|
||||
return to.value;
|
||||
}
|
||||
let t = (ms - from.ms as f32) / span;
|
||||
return from.value + (to.value - from.value) * from.easing.sample(t);
|
||||
}
|
||||
|
||||
self.points[self.points.len() - 1].value
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user