/* * 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 #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; }