SDK 冲刺批①+②+⑤: meow_box 线性布局 + meow_anim 动画基座 + meow_scroll 滚动物理

- meow_box(include/meow_ui_box.h + meow_ui_box.c):竖直/水平容器;子项=固定|weight|wrap;
  margin/padding/gap;主轴对齐 start/center/end/space-between/around/evenly;交叉轴 start/center/end/stretch;
  嵌套(深度≤16 静态栈,单遍求解,无递归爆炸)。可作 meow_view 布局提供者
  (meow_view_set_layout/relayout,opt-in;未设布局的 view 保持现有绝对坐标行为)。
  新增 meow_widget_set_rect(只写字段不标脏)作为 box 写坐标的通道。
- meow_anim(include/meow_ui_anim.h + meow_ui_anim.c):时间驱动 tween(from/to/duration/easing:
  linear/ease-out/ease-in-out/overshoot)+ update/done 回调;静态池≤16、取消、并行多动画;
  meow_anim_set_region 让每帧只 flush 动画影响区(不整屏)。meow_anim_tick 挂主循环 on_tick,
  无动画零开销(只查计数即返回)。
- meow_scroll(include/meow_ui_scroll.h + meow_ui_scroll.c):速度跟踪(最近 N 采样加权)+ 松手 fling
  (减速度曲线,用 anim 基座驱动)+ 边界过冲 clamp + 简单回弹(不做 spring)+ 可选 snap;
  跟手拖动行为不变(track 内部调既有 set_scroll);每帧只 redraw 列表视口区;不引 libm。
- 追加式:既有 meow_row/column/box 与全部 meow_*_create/hit/scroll 签名一行未改;旧绝对坐标永远有效;
  既有 App 免改源码。
- 验证:宿主新测试 box(42)/anim(32)/scroll(17) 全 PASS;既存 9 回归(widgets/sdk_input_keys/ime_fix/
  pinyin/overlay_suppress/notif_gesture/sdk_fixes/sb_collapse/p1p2p3_repro)全 PASS;
  离屏 demo(demo_box_scroll.c)渲染 box 布局 + 列表 fling 正常;zig-mipsel -Wall -Wextra 全 TU 0 warning;
  meow_ui.a md5 83dd6a2d0d24cc748a8659c29f1de2b5(未重链/部署 hello/files/ncm/reader,由主会话统一处理)。
This commit is contained in:
MeowOS committed 2026-09-01 23:49:24 +08:00
1 parent 553a7d5d9e
commit 5a5e995ebd
19 files changed
+1839 -2

No files matched your search

+293
View File
@@ -0,0 +1,293 @@
/*
* meow_ui_scroll.c — 滚动物理(§12.3)。
*
* 速度采样:track 时把 (pos, t) 记进定长环形(≤MEOW_SCROLL_SAMPLES),fling 时取
* 最近 MEOW_SCROLL_WINDOW ms 窗口,按「间隔速度 × 近者权重」加权得出初速(px/s)。
* 位置模型:ease-out quad 的位移曲线(末速恰为 0),初速与输入 v 对齐,时长 = 2×tau。
* 边界:free 目标越界 → clamp 到 [0,max];若越界速度大 → 补两段短 IN_OUT 回弹
* (overshoot 超过边界方向的内侧再回位),不做弹簧。停止后可选 snap 到 step 倍数。
*
* 不引 libm(SDK 约定,免 -lm);只用整数 + 纯浮点四则。
*/
#include "meow_ui.h"
#include "include/meow_ui_scroll.h"
#include "meow_ui_internal.h" /* meow_widget_rect(定位 redraw 视口区) */
#include "include/meow_ui_anim.h"
#include <string.h>
#define MEOW_SCROLL_POOL 8
#define MEOW_SCROLL_SAMPLES 8 /* 速度采样点数(最近 N 加权) */
#define MEOW_SCROLL_WINDOW 110 /* 速度窗口(ms) */
#define MEOW_SCROLL_TAU 180 /* 默认摩擦时间常数(ms) */
#define MEOW_SCROLL_MIN_SPEED 60 /* 低于此的初速不 fling(px/s) */
#define MEOW_SCROLL_MAX_DUR 900 /* fling 时长上限(ms) */
#define MEOW_SCROLL_BOUNCE_MS 110 /* 每段回弹时长(ms) */
#define MEOW_SCROLL_SNAP_MS 120
typedef enum {
MEOW_SCROLL_IDLE = 0,
MEOW_SCROLL_FLING,
MEOW_SCROLL_BOUNCE1,
MEOW_SCROLL_BOUNCE2,
MEOW_SCROLL_SNAP,
} meow_scroll_phase;
struct meow_scroll {
meow_scroll_set_fn set;
meow_scroll_max_fn max;
meow_scroll_get_fn get;
meow_scroll_redraw_fn redraw;
void *ctx;
int snap_step; /* >0 = 吸附步长 */
int tau_ms; /* 摩擦时间常数 */
/* 速度采样(顺序数组,满则前移,≤SAMPLES) */
int v_pos[MEOW_SCROLL_SAMPLES];
uint64_t v_t[MEOW_SCROLL_SAMPLES];
int v_n;
meow_scroll_phase phase;
meow_anim *anim;
int maxv; /* fling 开始时缓存 max_scroll */
int bounce; /* fling 边界回弹幅度(带符号;0=不回弹) */
int bounce_start; /* 回弹回位起点(= clamp 边界) */
int snap_to; /* snap 目标 */
};
static meow_scroll g_pool[MEOW_SCROLL_POOL];
static unsigned char g_used[MEOW_SCROLL_POOL];
/* ---- 生命周期 ---- */
meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
meow_scroll_get_fn get, meow_scroll_redraw_fn redraw,
void *ctx)
{
if (!set || !max || !get) return NULL;
for (int i = 0; i < MEOW_SCROLL_POOL; i++) {
if (!g_used[i]) {
g_used[i] = 1;
meow_scroll *s = &g_pool[i];
memset(s, 0, sizeof *s);
s->set = set; s->max = max; s->get = get; s->redraw = redraw; s->ctx = ctx;
s->tau_ms = MEOW_SCROLL_TAU;
return s;
}
}
return NULL;
}
void meow_scroll_free(meow_scroll *s)
{
if (!s) return;
if (s->anim) meow_anim_cancel(s->anim);
s->anim = NULL;
for (int i = 0; i < MEOW_SCROLL_POOL; i++)
if (&g_pool[i] == s) { g_used[i] = 0; return; }
}
/* ---- wrap:包一个 meow_widget 列表 ---- */
typedef struct { meow_widget *w; int lv; } wrap_ctx;
static void wrap_set(void *c, int off)
{ wrap_ctx *wc = c; if (wc->lv) meow_listview_set_scroll(wc->w, off); else meow_list_set_scroll(wc->w, off); }
static int wrap_max(void *c)
{ wrap_ctx *wc = c; return wc->lv ? meow_listview_max_scroll(wc->w) : meow_list_max_scroll(wc->w); }
static int wrap_get(void *c)
{ wrap_ctx *wc = c; return wc->lv ? meow_listview_scroll(wc->w) : meow_list_scroll(wc->w); }
static void wrap_redraw(void *c)
{
wrap_ctx *wc = c;
if (!wc->w) return;
int r[4]; meow_widget_rect(wc->w, r);
if (r[2] > 0 && r[3] > 0) meow_ui_flush_region(r[0], r[1], r[2], r[3]); /* 只刷视口区 */
}
static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL];
static int g_wrap_used[MEOW_SCROLL_POOL];
meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview)
{
if (!list) return NULL;
meow_scroll *s = meow_scroll_create(wrap_set, wrap_max, wrap_get, wrap_redraw, NULL);
if (!s) return NULL;
for (int i = 0; i < MEOW_SCROLL_POOL; i++) {
if (!g_wrap_used[i]) {
g_wrap_used[i] = 1;
g_wrap_ctx[i].w = list; g_wrap_ctx[i].lv = is_listview ? 1 : 0;
s->ctx = &g_wrap_ctx[i];
return s;
}
}
meow_scroll_free(s);
return NULL;
}
/* ---- 速度采样 ---- */
static void sample_push(meow_scroll *s, int pos, uint64_t t)
{
if (s->v_n >= MEOW_SCROLL_SAMPLES) { /* 满:前移丢弃最旧 */
for (int i = 1; i < s->v_n; i++) { s->v_pos[i - 1] = s->v_pos[i]; s->v_t[i - 1] = s->v_t[i]; }
s->v_n--;
}
s->v_pos[s->v_n] = pos; s->v_t[s->v_n] = t; s->v_n++;
}
/* 加权初速:取窗口内采样,按间隔速度 × 近者权重加权平均(px/s)。 */
static int sample_velocity(meow_scroll *s, uint64_t now)
{
int lo = 0;
while (lo < s->v_n && now > s->v_t[lo] && (now - s->v_t[lo]) > MEOW_SCROLL_WINDOW) lo++;
if (s->v_n - lo < 2) return 0;
long long sum_w = 0, sum_vw = 0;
for (int i = lo; i < s->v_n - 1; i++) {
uint64_t dt = s->v_t[i + 1] - s->v_t[i];
if (dt == 0) continue;
long long v = (long long)(s->v_pos[i + 1] - s->v_pos[i]) * 1000LL / (long long)dt;
long long w = (i - lo + 1); /* 近者权重高 */
sum_vw += v * w; sum_w += w;
}
if (sum_w == 0) return 0;
return (int)(sum_vw / sum_w);
}
int meow_scroll_velocity(meow_scroll *s)
{
if (!s) return 0;
return sample_velocity(s, meow_ui_time_ms());
}
void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms)
{
if (!s || dy == 0) return;
int off = s->get(s->ctx) + dy; /* set 会 clamp 到 [0,max](与 scroll_by 一致) */
s->set(s->ctx, off);
int got = s->get(s->ctx); /* clamp 后的真实 offset */
sample_push(s, got, t_ms);
}
/* ---- 动画驱动回调 ---- */
static void fling_update(meow_anim *a, float v, void *ctx)
{
(void)a;
meow_scroll *s = (meow_scroll *)ctx;
s->set(s->ctx, (int)v);
if (s->redraw) s->redraw(s->ctx);
}
static void fling_done(meow_anim *a, int canceled, void *ctx)
{
(void)a;
meow_scroll *s = (meow_scroll *)ctx;
s->anim = NULL;
if (canceled) { s->phase = MEOW_SCROLL_IDLE; return; }
if (s->phase == MEOW_SCROLL_FLING) {
if (s->bounce != 0) {
/* 边界回弹段1:向内侧过冲 → 段2 回位(bounce 已带符号;顶界 +,底界 -) */
int from = s->get(s->ctx);
int to = from + s->bounce; /* bounce 已带符号 */
if (to < 0) to = 0;
if (to > s->maxv) to = s->maxv;
if (to == from) { s->phase = MEOW_SCROLL_IDLE; return; }
s->phase = MEOW_SCROLL_BOUNCE1;
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_BOUNCE_MS;
d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
return;
}
/* 无回弹 → snap 或收尾 */
if (s->snap_step > 0 && s->get(s->ctx) != s->snap_to) {
int from = s->get(s->ctx);
int to = s->snap_to;
if (to < 0) to = 0;
if (to > s->maxv) to = s->maxv;
if (to != from) {
s->phase = MEOW_SCROLL_SNAP;
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_SNAP_MS;
d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
return;
}
}
s->phase = MEOW_SCROLL_IDLE;
return;
}
if (s->phase == MEOW_SCROLL_BOUNCE1) {
int from = s->get(s->ctx);
/* 回位到 bounce 起点(= clamp 边界) */
int to = s->bounce_start;
if (to < 0) to = 0;
if (to > s->maxv) to = s->maxv;
s->phase = MEOW_SCROLL_BOUNCE2;
if (to == from) { s->phase = MEOW_SCROLL_IDLE; return; }
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_BOUNCE_MS;
d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
return;
}
if (s->phase == MEOW_SCROLL_BOUNCE2 || s->phase == MEOW_SCROLL_SNAP) {
s->phase = MEOW_SCROLL_IDLE;
return;
}
s->phase = MEOW_SCROLL_IDLE;
}
/* ---- fling ---- */
void meow_scroll_fling(meow_scroll *s)
{
if (!s) return;
if (s->anim) { meow_anim_cancel(s->anim); s->anim = NULL; }
s->phase = MEOW_SCROLL_IDLE;
int v = sample_velocity(s, meow_ui_time_ms());
if (v > -MEOW_SCROLL_MIN_SPEED && v < MEOW_SCROLL_MIN_SPEED) return; /* 太慢不 fling */
int max = s->max(s->ctx);
if (max < 0) max = 0;
s->maxv = max;
int pos0 = s->get(s->ctx);
int dist = (int)((long long)v * s->tau_ms / 1000LL); /* 摩擦滑行总位移(px) */
int free_target = pos0 + dist;
int clamped = free_target, over = 0;
if (clamped > max) { over = clamped - max; clamped = max; }
if (clamped < 0) { over = -clamped; clamped = 0; }
if (clamped == pos0) return; /* 无位移 */
int dir; /* 回弹方向:底界向 -1(往内上),顶界向 +1(往内下) */
if (free_target > max) dir = -1;
else if (free_target < 0) dir = 1;
else dir = 0;
int dur = 2 * s->tau_ms; /* ease-out quad:初速与 v 对齐,末速为 0 */
if (dur > MEOW_SCROLL_MAX_DUR) dur = MEOW_SCROLL_MAX_DUR;
s->bounce = 0;
s->snap_to = pos0;
if (s->snap_step > 0) {
int half = s->snap_step / 2;
s->snap_to = ((pos0 + half) / s->snap_step) * s->snap_step;
if (s->snap_to > max) s->snap_to = max;
}
if (over > 12) { /* 快速撞墙:clamp + 简单回弹 */
int amt = over / 10; if (amt < 3) amt = 3; if (amt > 14) amt = 14;
s->bounce = dir * amt; /* 带符号的向内幅度 */
/* 记录回位起点 = clamp 边界 */
s->bounce_start = clamped;
}
s->phase = MEOW_SCROLL_FLING;
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)pos0; d.to = (float)clamped; d.duration_ms = (uint32_t)dur;
d.ease = MEOW_EASE_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
}
void meow_scroll_stop(meow_scroll *s)
{
if (!s) return;
if (s->anim) { meow_anim_cancel(s->anim); s->anim = NULL; }
s->phase = MEOW_SCROLL_IDLE;
}
int meow_scroll_active(meow_scroll *s) { return (s && s->phase != MEOW_SCROLL_IDLE) ? 1 : 0; }
int meow_scroll_offset(meow_scroll *s) { return s ? s->get(s->ctx) : 0; }
void meow_scroll_set_snap(meow_scroll *s, int step) { if (s) s->snap_step = step > 0 ? step : 0; }
void meow_scroll_set_friction(meow_scroll *s, int tau_ms)
{ if (s) s->tau_ms = (tau_ms > 20 && tau_ms <= 2000) ? tau_ms : MEOW_SCROLL_TAU; }