Home
打开 App 看到的第一屏。一对巨大标题做了 hero 动画,设备信息刷条带,三张快捷卡片——每张都是长按可开扇形菜单。
一句话
一张 HoverText 双行标题 + 设备信息 strip + 三张功能卡片,所有光效走 GPU。
源码:src/screens/HomeScreen.tsx(~260 行)
实拍
屏幕长这样
┌─────────────────────────────────────────┐
│ [ ≡ ] │ ← 汉堡菜单
│ │
│ │
│ S H A D O W │ ← HoverText (大标题)
│ N A V │
│ │
│ REACT NATIVE · SM8750 · ANDROID 16 │ ← 设备信息条
│ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 📁 File → │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ ⚙️ Setting → │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 🔌 Drivers → │ │
│ └──────────────────────────────────┘ │
│ │
│ ─ shadow paging ─ │ ← footer
└─────────────────────────────────────────┘Hero 动画(最花时间的一段)
两行大字 SHADOW / NAV:
- App 启动时,两行字从屏幕正中央开始
- 0.6s 后平滑 lerp 到左上角位置
- 同时透明度从 0 淡入到 1
- 全程
position: absolute+ 手算坐标,不走 RN 布局系统
代码骨架
const titleX = useSharedValue(centerX);
const titleY = useSharedValue(centerY);
const opacity = useSharedValue(0);
// 动画
useEffect(() => {
opacity.value = withTiming(1, { duration: 600 });
titleX.value = withDelay(300, withTiming(leftX,
{ duration: 800, easing: Easing.out(Easing.cubic) }));
titleY.value = withDelay(300, withTiming(topY,
{ duration: 800, easing: Easing.out(Easing.cubic) }));
}, []);
const titleStyle = useAnimatedStyle(() => ({
position: 'absolute',
left: titleX.value,
top: titleY.value,
opacity: opacity.value,
}));为什么不用 RN 的 FlexBox 改 margin / padding 做动画?布局会重排——每帧都要 measure/layout,大几率掉帧。
absolute+ transform / left/top 全程只走合成线程。
HoverText ——“会发光的字”
大标题本身不是普通文字——它是 <HoverText>,一个 Skia Canvas 画出来的特效组件。
三层叠加:
Layer 1 (底): 半亮的文字填充,主题色低饱和版本
Layer 2 (中): 径向渐变光圈,跟着字形做 mask——像"一束光扫过字里"
Layer 3 (顶): 深色描边 outline,让字保持清晰锐利径向渐变的轨道
光圈中心不是静止的——它在字的包围盒里绕 8 字形转:
const clock = useClock();
// 中心点按 x = cos(t*ωx), y = sin(t*ωy),两个不同频率组合成 Lissajous
const cx = width/2 + amplitude * Math.cos(clock.value * 0.001);
const cy = height/2 + amplitude * Math.sin(clock.value * 0.0007);这样光圈轨道永远不重复,视觉上保持活力。
字体 fallback
Inter-Black 作主字形,CJK 字符走 Skia Paragraph API——自动解析系统字体、拼出完整行。即使标题里混”SHADOW 导航”也不会断字。
设备信息条
Hero 底下一行 small caps:
REACT NATIVE · SM8750 · ANDROID 16数据来源:DeviceInfoModule(Kotlin)一次性 syscall 拿:
soc_manufacturer+soc_model(Android 31+ API)release+sdkboard+manufacturer
// native/deviceInfo.ts
export async function getDeviceInfo(): Promise<DeviceInfo> {
return NativeModules.DeviceInfoModule.getInfo();
}
// 模块级缓存 —— 重挂载秒出
let cached: DeviceInfo | null = null;重点:首次拿到就 cache 在模块里,组件重挂载瞬间拿到不闪。
三张快捷卡片(StripCard)
视觉
每张卡片:
- 宽度铺满,高度固定 72px
- 圆角 18px
- 底色:半透明磨砂 + 主题色光晕
- 左边一个大图标(emoji 或 svg)
- 中间文字:标题 + 副标题
- 右边一个
→指示器 - 边框:1px 主题色 低 alpha,按住时变亮闪烁
每张卡的右边有条”装饰条”(StripDecos)
一条竖向渐变条,用主题的 stripColors[i] 三色混——视觉上像每张卡有自己的”色彩签名”:
| 卡 | 色彩 |
|---|---|
| File | 主题色 1 |
| Setting | 主题色 2 |
| Drivers | 主题色 3 |
用户在 Settings 里调主题色,条带会即时变色。
卡片的两种手势
每张 StripCard 同时响应轻点 & 长按——Race gesture 竞争:
轻点 = 切屏
<StripCard
onPress={() => navigate('file')}
...
/>tap 直接路由到对应主屏。
长按 = 弹扇形菜单
长按 0.4s,立刻冒出一个扇形——几条”辐射光束”从卡片边缘伸出:
[辐射光1] → Quick 1
╱
[卡片] ╱
╲─ [辐射光2] → Quick 2
╲
[辐射光3] → Quick 3每条辐射光终点是一个选项(例如 Setting 卡的 fan 给你直接跳到”安装 CA 证书”、“开发者选项”、“关于手机”)。
按住手指不放滑到某根光束上——该选项就高亮。松手 = 选择。
扇形菜单的动效
用 useFanGesture hook 统一管理:
- 扇形打开:80ms 内三条光束
scale 0 → 1 - 每条光束尾端的点在 300ms 内绕中心顺时针转一圈,吸引注意
- hover 状态:手指在某光束上停 → 该光束亮度 ×1.5 + 半径 +8px
- 松手:若选中了某光束,光束”闪一下白色”后全部收回;未选中则直接收回
汉堡菜单(左上角)
<HamburgerMenu> — 顶栏的 ≡ 按钮。
点开 = 弹出悬浮层(不 push 新屏):
┌────────────────┐
│ ⚙ Settings │
│ ℹ About │
│ 🗑 Exit │
└────────────────┘Exit 不是真的杀 App——是让 App 回到背景(moveTaskToBack)——Android 的”按 home 键”行为。
Footer
底部一个淡淡的:”— shadow paging —” 签名——表明这个 App 是 Shadow Cheat Engine 整个生态的控制面板。点一下会跳到项目主页 URL(如果设了)。
首屏加载顺序
为什么 App 启动不卡?
T=0ms App 启动,HomeScreen 挂载
T=0ms Hero 标题用默认占位文字("Shadow Nav")先画上
T=5ms SkiaOrbBackground 后台异步加载 4 层特效(blur 预缓存)
T=30ms DeviceInfoModule.getInfo() 发起(native 调用 ~10ms)
T=40ms 设备信息回来,更新 hero_sub 从"REACT NATIVE"变"REACT NATIVE · SM8750 · ..."
T=100ms 卡片 StripCard fade-in 完成
T=600ms Hero 中→左上角动画开始
T=1400ms 一切就位,可交互首帧 <100ms 可交互——所有卡片都可以点了,hero 动画只是装饰。
性能注意
- Skia 背景层 blur 预缓存:每次打开 Home 不重新 blur,4 层底图各存在独立
SkPicture里 - DeviceInfo 缓存:模块级
let cached持久 - StripCard 重绘:只有主题色变了才重画(
useMemodeps 锁定) - fan gesture 节流:
manualActivation等长按 400ms 稳定后才激活 Pan,避免误触发 pan 事件
相关文件
src/screens/HomeScreen.tsx— 主屏src/components/StripCard.tsx— 快捷卡片src/components/StripDecos.tsx— 卡片装饰条src/components/HamburgerMenu.tsx— 顶栏汉堡src/gpu/HoverText.tsx— 发光标题src/hooks/useFanGesture.ts— 长按扇形菜单src/native/deviceInfo.ts— 设备信息 bridge