react-native-gradient-mask 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.eslintrc.js +5 -0
  2. package/PUBLISHING.md +171 -0
  3. package/README.md +360 -0
  4. package/android/build.gradle +43 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/expo/modules/gradientmask/GradientMaskModule.kt +33 -0
  7. package/android/src/main/java/expo/modules/gradientmask/GradientMaskView.kt +198 -0
  8. package/build/AnimatedGradientMaskView.d.ts +36 -0
  9. package/build/AnimatedGradientMaskView.d.ts.map +1 -0
  10. package/build/AnimatedGradientMaskView.js +37 -0
  11. package/build/AnimatedGradientMaskView.js.map +1 -0
  12. package/build/AnimatedGradientMaskView.web.d.ts +17 -0
  13. package/build/AnimatedGradientMaskView.web.d.ts.map +1 -0
  14. package/build/AnimatedGradientMaskView.web.js +100 -0
  15. package/build/AnimatedGradientMaskView.web.js.map +1 -0
  16. package/build/GradientMask.types.d.ts +38 -0
  17. package/build/GradientMask.types.d.ts.map +1 -0
  18. package/build/GradientMask.types.js +2 -0
  19. package/build/GradientMask.types.js.map +1 -0
  20. package/build/GradientMaskModule.d.ts +3 -0
  21. package/build/GradientMaskModule.d.ts.map +1 -0
  22. package/build/GradientMaskModule.js +4 -0
  23. package/build/GradientMaskModule.js.map +1 -0
  24. package/build/GradientMaskModule.web.d.ts +3 -0
  25. package/build/GradientMaskModule.web.d.ts.map +1 -0
  26. package/build/GradientMaskModule.web.js +3 -0
  27. package/build/GradientMaskModule.web.js.map +1 -0
  28. package/build/GradientMaskView.d.ts +4 -0
  29. package/build/GradientMaskView.d.ts.map +1 -0
  30. package/build/GradientMaskView.js +7 -0
  31. package/build/GradientMaskView.js.map +1 -0
  32. package/build/GradientMaskView.web.d.ts +8 -0
  33. package/build/GradientMaskView.web.d.ts.map +1 -0
  34. package/build/GradientMaskView.web.js +99 -0
  35. package/build/GradientMaskView.web.js.map +1 -0
  36. package/build/index.d.ts +6 -0
  37. package/build/index.d.ts.map +1 -0
  38. package/build/index.js +7 -0
  39. package/build/index.js.map +1 -0
  40. package/expo-module.config.json +9 -0
  41. package/ios/GradientMask.podspec +29 -0
  42. package/ios/GradientMaskModule.swift +29 -0
  43. package/ios/GradientMaskView.swift +133 -0
  44. package/package.json +83 -0
  45. package/rn-gradient-mask-design.md +657 -0
  46. package/src/AnimatedGradientMaskView.tsx +60 -0
  47. package/src/AnimatedGradientMaskView.web.tsx +149 -0
  48. package/src/GradientMask.types.ts +43 -0
  49. package/src/GradientMaskModule.ts +4 -0
  50. package/src/GradientMaskModule.web.ts +2 -0
  51. package/src/GradientMaskView.tsx +11 -0
  52. package/src/GradientMaskView.web.tsx +129 -0
  53. package/src/index.ts +7 -0
  54. package/tsconfig.json +9 -0
@@ -0,0 +1,657 @@
1
+ # CharacterChatScreen 動畫與 Mask 系統分析
2
+
3
+ > 此文檔是 `rn-gradient-mask` 原生套件重新設計的重要參考
4
+
5
+ ---
6
+
7
+ ## 一、現有架構概覽
8
+
9
+ ### 元件層級結構
10
+
11
+ ```
12
+ CharacterChatScreen
13
+ ├── BackgroundImage (角色背景圖)
14
+ ├── Header (Animated.View, opacity: uiOpacity)
15
+ ├── overlayUI (Animated.View, opacity: uiOpacity)
16
+ │ ├── Coin
17
+ │ ├── Affinity
18
+ │ └── RemainingFreeMessageBubble
19
+ ├── Animated.View (translateY 跟隨鍵盤)
20
+ │ ├── messagesContainer (Animated.View, opacity: uiOpacity)
21
+ │ │ └── MaskedView
22
+ │ │ ├── maskElement: AnimatedMaskGradient
23
+ │ │ │ ├── LinearGradient (漸層)
24
+ │ │ │ └── Animated.View (白色覆蓋層)
25
+ │ │ └── children: FlashList (訊息列表)
26
+ │ └── InputArea (Animated.View, opacity: uiOpacity)
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 二、主要動畫系統
32
+
33
+ ### 1. uiOpacity - 整體 UI 淡入/淡出
34
+
35
+ | 項目 | 說明 |
36
+ |-----|------|
37
+ | **來源** | `useUIState.ts:28` |
38
+ | **類型** | `Animated.Value` (React Native 原生動畫) |
39
+ | **驅動方式** | `Animated.timing()` with `useNativeDriver: true` |
40
+ | **用途** | 控制整體 UI 元素的顯示/隱藏 |
41
+
42
+ **影響範圍**:
43
+ - Header (`opacity: uiOpacity`)
44
+ - overlayUI (Coin, Affinity 等)
45
+ - messagesContainer
46
+ - InputArea 區域
47
+
48
+ **動畫程式碼**:
49
+ ```typescript
50
+ // 檔案: index.tsx:1105, 1903
51
+ Animated.timing(uiOpacity, {
52
+ toValue: 1,
53
+ duration: 300,
54
+ useNativeDriver: true,
55
+ }).start();
56
+ ```
57
+
58
+ ---
59
+
60
+ ### 2. maskOpacity - 漸層遮罩效果 ⭐ 重點
61
+
62
+ | 項目 | 說明 |
63
+ |-----|------|
64
+ | **來源** | `useMaskedView.ts:12` |
65
+ | **類型** | `SharedValue<number>` (Reanimated) |
66
+ | **驅動方式** | `withTiming()` |
67
+ | **用途** | 控制 MaskedView 的漸層透明效果強度 |
68
+
69
+ **動畫程式碼**:
70
+ ```typescript
71
+ // 檔案: useMaskedView.ts:25-28
72
+ maskOpacity.value = withTiming(want ? 1 : 0, {
73
+ duration: want ? 600 : 400, // 顯示 600ms, 隱藏 400ms
74
+ easing: want ? Easing.in(Easing.quad) : Easing.out(Easing.quad),
75
+ });
76
+ ```
77
+
78
+ **AnimatedMaskGradient 內部實作**:
79
+ ```typescript
80
+ // 檔案: AnimatedMaskGradient.tsx:28-30
81
+ const overlayAnimatedStyle = useAnimatedStyle(() => ({
82
+ opacity: 1 - maskOpacity.value,
83
+ }));
84
+
85
+ // 結構
86
+ <View>
87
+ <LinearGradient colors={BASE_MASK_COLORS} locations={MASK_GRADIENT_LOCATIONS} />
88
+ <Animated.View style={[{ backgroundColor: "white" }, overlayAnimatedStyle]} />
89
+ </View>
90
+ ```
91
+
92
+ **漸層配置**:
93
+ ```typescript
94
+ // 檔案: AnimatedMaskGradient.tsx:9-13
95
+ const MASK_GRADIENT_ALPHA_STOPS = [0, 0, 0.2, 0.6, 0.9, 1];
96
+ const MASK_GRADIENT_LOCATIONS = [0, 0.42, 0.45, 0.48, 0.5, 1];
97
+ const BASE_MASK_COLORS = MASK_GRADIENT_ALPHA_STOPS.map(
98
+ stop => `rgba(255,255,255,${stop})` // 注意:使用白色
99
+ );
100
+ ```
101
+
102
+ ---
103
+
104
+ ### 3. translateY - 鍵盤跟隨動畫
105
+
106
+ | 項目 | 說明 |
107
+ |-----|------|
108
+ | **來源** | `useKeyboardAnimations.ts:45` |
109
+ | **類型** | `Animated.Value` |
110
+ | **驅動方式** | `Animated.timing()` with `useNativeDriver: true` |
111
+ | **用途** | 讓訊息區域和輸入框跟隨鍵盤移動 |
112
+
113
+ ---
114
+
115
+ ## 三、使用者旅程與動畫觸發時機
116
+
117
+ ### 場景 1: 進入聊天畫面
118
+
119
+ ```
120
+ 使用者動作: 點擊角色進入聊天
121
+ ─────────────────────────────────────
122
+ 1. 畫面載入
123
+ └─ uiOpacity: 0 → 1 (300ms 淡入)
124
+ └─ maskOpacity: 0 (無遮罩效果,訊息完全可見)
125
+
126
+ 2. 歷史訊息載入完成
127
+ └─ maskOpacity 維持 0
128
+ ```
129
+
130
+ ### 場景 2: 滾動訊息列表
131
+
132
+ ```
133
+ 使用者動作: 開始滾動訊息
134
+ ─────────────────────────────────────
135
+ 1. onScrollBeginDrag 觸發
136
+ └─ setMaskTargetVisible(true)
137
+ └─ maskOpacity: 0 → 1 (600ms, ease-in-quad)
138
+ └─ 效果: 頂部漸變透明,露出背景圖
139
+
140
+ 2. 滾動結束 (onMomentumScrollEnd)
141
+ └─ 延遲後 setMaskTargetVisible(false)
142
+ └─ maskOpacity: 1 → 0 (400ms, ease-out-quad)
143
+ └─ 效果: 漸層消失,訊息完全可見
144
+ ```
145
+
146
+ ### 場景 3: 點擊背景隱藏 UI
147
+
148
+ ```
149
+ 使用者動作: 點擊背景區域
150
+ ─────────────────────────────────────
151
+ 1. tapGesture 觸發
152
+ └─ uiOpacity: 1 → 0 (隱藏所有 UI)
153
+ └─ maskOpacity: 0 (重置)
154
+
155
+ 2. 再次點擊
156
+ └─ uiOpacity: 0 → 1 (顯示所有 UI)
157
+ ```
158
+
159
+ ### 場景 4: 離開聊天畫面
160
+
161
+ ```
162
+ 使用者動作: 點擊返回按鈕
163
+ ─────────────────────────────────────
164
+ 1. handleBackPress 觸發
165
+ └─ cancelAnimation(maskOpacity)
166
+ └─ maskOpacity.value = 0 (立即重置)
167
+ └─ navigation.goBack()
168
+ ```
169
+
170
+ ---
171
+
172
+ ## 四、效能瓶頸分析
173
+
174
+ | 動畫 | 類型 | 效能評估 | 問題 |
175
+ |-----|------|---------|------|
176
+ | **uiOpacity** | RN Animated | ✅ 好 | `useNativeDriver: true`,在原生層執行 |
177
+ | **translateY** | RN Animated | ✅ 好 | `useNativeDriver: true`,在原生層執行 |
178
+ | **maskOpacity** | Reanimated | ⚠️ 有問題 | 需要更深入分析 |
179
+
180
+ ### maskOpacity 效能問題詳解
181
+
182
+ ```
183
+ 現有架構:
184
+ ┌─────────────────────────────────────────────────────────┐
185
+ │ JS Thread │
186
+ │ ┌─────────────────────────────────────────────────┐ │
187
+ │ │ useMaskedView │ │
188
+ │ │ maskOpacity = useSharedValue(0) │ │
189
+ │ │ maskOpacity.value = withTiming(1, {...}) │ │
190
+ │ └─────────────────────────────────────────────────┘ │
191
+ │ │ │
192
+ │ ▼ │
193
+ │ ┌─────────────────────────────────────────────────┐ │
194
+ │ │ AnimatedMaskGradient │ │
195
+ │ │ useAnimatedStyle(() => ({ │ │
196
+ │ │ opacity: 1 - maskOpacity.value ◄── 每幀執行 │ │
197
+ │ │ })) │ │
198
+ │ └─────────────────────────────────────────────────┘ │
199
+ └─────────────────────────────────────────────────────────┘
200
+
201
+
202
+ ┌─────────────────────────────────────────────────────────┐
203
+ │ Native (MaskedView - JS 元件) │
204
+ │ ┌─────────────────────────────────────────────────┐ │
205
+ │ │ maskElement 每幀重新渲染? │ │
206
+ │ │ FlashList 可能受影響 │ │
207
+ │ └─────────────────────────────────────────────────┘ │
208
+ └─────────────────────────────────────────────────────────┘
209
+ ```
210
+
211
+ **問題點**:
212
+ 1. `MaskedView` 是 JS 元件,不是純原生
213
+ 2. `useAnimatedStyle` 雖然在 UI thread 執行,但 MaskedView 的 re-render 可能在 JS thread
214
+ 3. 動畫期間可能影響 FlashList 的滾動效能
215
+
216
+ ---
217
+
218
+ ## 五、相關檔案清單
219
+
220
+ ### 核心檔案
221
+
222
+ | 檔案 | 用途 |
223
+ |-----|------|
224
+ | `src/screens/CharacterChatScreen/index.tsx` | 主畫面,整合所有動畫 |
225
+ | `src/hooks/useMaskedView.ts` | maskOpacity 狀態與動畫控制 |
226
+ | `src/screens/CharacterChatScreen/hooks/useUIState.ts` | uiOpacity 狀態 |
227
+ | `src/screens/CharacterChatScreen/hooks/useKeyboardAnimations.ts` | translateY 鍵盤動畫 |
228
+ | `src/screens/CharacterChatScreen/components/AnimatedMaskGradient.tsx` | MaskedView 的 maskElement |
229
+
230
+ ### 第三方依賴
231
+
232
+ | 套件 | 用途 |
233
+ |-----|------|
234
+ | `@react-native-masked-view/masked-view` | 提供 MaskedView 元件 |
235
+ | `react-native-linear-gradient` | 提供 LinearGradient 元件 |
236
+ | `react-native-reanimated` | 提供 SharedValue 和 worklet 動畫 |
237
+
238
+ ---
239
+
240
+ ## 六、rn-gradient-mask 目標
241
+
242
+ 將以下結構:
243
+ ```
244
+ MaskedView (JS)
245
+ ├── maskElement: AnimatedMaskGradient (JS + Reanimated)
246
+ │ ├── LinearGradient
247
+ │ └── Animated.View (白色覆蓋層)
248
+ └── children: FlashList
249
+ ```
250
+
251
+ 替換為:
252
+ ```
253
+ RNGradientMask (純原生)
254
+ ├── 原生漸層 mask 渲染
255
+ ├── 原生 maskOpacity 控制
256
+ └── children: FlashList
257
+ ```
258
+
259
+ ### 設計原則
260
+
261
+ 1. **原生層處理所有 mask 渲染** - 不再依賴 MaskedView + LinearGradient
262
+ 2. **maskOpacity 透過 Reanimated 驅動原生 prop** - 使用 `useAnimatedProps`
263
+ 3. **保持相同的 API 介面** - 讓遷移成本最低
264
+ 4. **保持相同的動畫時序** - 600ms 顯示, 400ms 隱藏
265
+
266
+ ---
267
+
268
+ ## 七、rn-gradient-mask 功能需求
269
+
270
+ ### 目標用法
271
+
272
+ ```tsx
273
+ // 取代 MaskedView + AnimatedMaskGradient
274
+ <RNGradientMask
275
+ style={{ flex: 1 }}
276
+ maskOpacity={maskOpacity} // SharedValue<number> 或 number
277
+ colors={GRADIENT_COLORS}
278
+ locations={GRADIENT_LOCATIONS}
279
+ direction="top"
280
+ >
281
+ <FlashList ... />
282
+ </RNGradientMask>
283
+ ```
284
+
285
+ ### Props 規格
286
+
287
+ | Prop | 類型 | 預設值 | 說明 |
288
+ |-----|------|-------|------|
289
+ | `colors` | `string[]` | 必填 | 漸層顏色陣列 (black alpha 格式) |
290
+ | `locations` | `number[]` | 均勻分布 | 顏色位置 (0-1) |
291
+ | `direction` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | 漸層方向 |
292
+ | `maskOpacity` | `SharedValue<number>` 或 `number` | `0` | 漸層效果強度 (0-1) |
293
+ | `style` | `ViewStyle` | - | 容器樣式 |
294
+ | `children` | `ReactNode` | - | 子元件 |
295
+
296
+ ### maskOpacity 行為定義
297
+
298
+ ```
299
+ maskOpacity = 0
300
+ ├── 無漸層效果
301
+ ├── 子元件完全可見(不透明)
302
+ └── 使用情境:一般瀏覽狀態
303
+
304
+ maskOpacity = 1
305
+ ├── 完整漸層效果
306
+ ├── 頂部透明,露出背景圖
307
+ └── 使用情境:滾動時顯示背景
308
+ ```
309
+
310
+ ### 漸層配置(對應 AnimatedMaskGradient)
311
+
312
+ ```typescript
313
+ // 原有配置 (白色格式,用於 MaskedView)
314
+ const MASK_GRADIENT_ALPHA_STOPS = [0, 0, 0.2, 0.6, 0.9, 1];
315
+ const MASK_GRADIENT_LOCATIONS = [0, 0.42, 0.45, 0.48, 0.5, 1];
316
+ const BASE_MASK_COLORS = MASK_GRADIENT_ALPHA_STOPS.map(
317
+ stop => `rgba(255,255,255,${stop})`
318
+ );
319
+
320
+ // 轉換為原生格式 (黑色格式,用於 RNGradientMask)
321
+ const NATIVE_GRADIENT_COLORS = [
322
+ "rgba(0,0,0,0)", // 0% - 透明
323
+ "rgba(0,0,0,0)", // 42% - 透明
324
+ "rgba(0,0,0,0.2)", // 45% - 20% 不透明
325
+ "rgba(0,0,0,0.6)", // 48% - 60% 不透明
326
+ "rgba(0,0,0,0.9)", // 50% - 90% 不透明
327
+ "rgba(0,0,0,1)", // 100% - 不透明
328
+ ];
329
+ const NATIVE_GRADIENT_LOCATIONS = [0, 0.42, 0.45, 0.48, 0.5, 1];
330
+ ```
331
+
332
+ ---
333
+
334
+ ## 八、動畫驅動方案比較
335
+
336
+ ### 方案 A:JS 層驅動(useAnimatedProps)✅ 採用(主要方案)
337
+
338
+ ```
339
+ JS 層 (useMaskedView)
340
+ ├── maskOpacity = useSharedValue(0)
341
+ ├── setMaskTargetVisible(true/false) 觸發動畫
342
+ └── withTiming() 控制動畫時序
343
+
344
+
345
+ useAnimatedProps
346
+
347
+
348
+ 原生層接收 prop 變化,只做渲染
349
+ ```
350
+
351
+ **優點**:
352
+ - JS 完全控制動畫時序、easing
353
+ - 可以隨時 cancelAnimation
354
+ - 可以根據業務邏輯動態調整
355
+
356
+ **缺點**:
357
+ - 需要 JS ↔ Native 通訊
358
+ - Reanimated worklet 仍在執行
359
+
360
+ ---
361
+
362
+ ### 方案 B:原生層驅動(Native Animation)🔄 備用方案
363
+
364
+ ```
365
+ JS 層
366
+ ├── 呼叫原生方法:showMask() / hideMask()
367
+ └── 不管動畫細節
368
+
369
+
370
+ 原生層
371
+ ├── iOS: CABasicAnimation
372
+ ├── Android: ValueAnimator
373
+ └── 完全在原生層執行動畫
374
+ ```
375
+
376
+ **優點**:
377
+ - 動畫完全在原生層,效能最佳
378
+ - 不需要 Reanimated
379
+
380
+ **缺點**:
381
+ - JS 無法即時控制動畫進度
382
+ - 難以實現 cancelAnimation
383
+ - 動畫時序寫死在原生層,修改需要重新編譯
384
+ - 無法根據業務邏輯動態調整
385
+
386
+ **備用原因**:
387
+ - 如果方案 A 的效能無法滿足需求,可考慮切換到此方案
388
+ - 需要實作原生方法:`showMask(duration: number)` 和 `hideMask(duration: number)`
389
+
390
+ ---
391
+
392
+ ### 方案 C:混合模式(記錄但不採用)
393
+
394
+ ```
395
+ JS 層
396
+ ├── maskOpacity prop (靜態值或 SharedValue)
397
+ ├── 簡單情況:直接傳數值
398
+ └── 動畫情況:用 useAnimatedProps
399
+
400
+
401
+ 原生層
402
+ ├── 接收 maskOpacity prop
403
+ ├── 如果需要,可選擇性內建簡單動畫
404
+ └── 但主要依賴 JS 驅動
405
+ ```
406
+
407
+ **說明**:
408
+ - 此方案結合了方案 A 和 B 的優點
409
+ - 目前不採用,但保留作為未來可能的優化方向
410
+
411
+ ---
412
+
413
+ ### 決定:採用方案 A(JS 層驅動 + useAnimatedProps)
414
+
415
+ **狀態**:✅ 已決定採用,待實作
416
+
417
+ **考量因素**:
418
+ 1. **效能**:useAnimatedProps 在 UI thread 執行,效能已經很好
419
+ 2. **靈活性**:JS 完全控制動畫,可隨時調整
420
+ 3. **維護性**:改動在 JS 層,不需重新編譯原生
421
+ 4. **現有架構**:`useMaskedView` 已經用 Reanimated,遷移成本最低
422
+ 5. **必要功能**:`cancelAnimation` 是必要的(離開畫面時需要立即停止)
423
+
424
+ **備用方案**:方案 B(原生層驅動)
425
+ - 如果方案 A 的效能無法滿足需求,可考慮切換到方案 B
426
+ - 方案 B 的實作細節已記錄,可作為未來優化方向
427
+
428
+ ---
429
+
430
+ ## 九、實作設計
431
+
432
+ ### 架構圖
433
+
434
+ ```
435
+ ┌─────────────────────────────────────────────────────────────┐
436
+ │ JS 層 │
437
+ │ ┌─────────────────────────────────────────────────────┐ │
438
+ │ │ useMaskedView (保留不變) │ │
439
+ │ │ maskOpacity = useSharedValue(0) │ │
440
+ │ │ setMaskTargetVisible(true/false) │ │
441
+ │ │ withTiming() 控制動畫 │ │
442
+ │ └─────────────────────────────────────────────────────┘ │
443
+ │ │ │
444
+ │ ▼ │
445
+ │ ┌─────────────────────────────────────────────────────┐ │
446
+ │ │ AnimatedGradientMask (新元件) │ │
447
+ │ │ useAnimatedProps(() => ({ │ │
448
+ │ │ maskOpacity: maskOpacity.value │ │
449
+ │ │ })) │ │
450
+ │ └─────────────────────────────────────────────────────┘ │
451
+ └─────────────────────────────────────────────────────────────┘
452
+
453
+ ▼ (prop 變化直接更新原生層)
454
+ ┌─────────────────────────────────────────────────────────────┐
455
+ │ 原生層 (RNGradientMask) │
456
+ │ ┌─────────────────────────────────────────────────────┐ │
457
+ │ │ iOS: CAGradientLayer + solidMaskLayer │ │
458
+ │ │ Android: LinearGradient + PorterDuff.Mode.DST_IN │ │
459
+ │ │ │ │
460
+ │ │ maskOpacity prop 變化 → 更新 solidMaskLayer opacity │ │
461
+ │ │ (不觸發 JS re-render) │ │
462
+ │ └─────────────────────────────────────────────────────┘ │
463
+ │ │ │
464
+ │ ▼ │
465
+ │ ┌─────────────────────────────────────────────────────┐ │
466
+ │ │ children: FlashList (不受動畫影響) │ │
467
+ │ └─────────────────────────────────────────────────────┘ │
468
+ └─────────────────────────────────────────────────────────────┘
469
+ ```
470
+
471
+ ### 元件 API
472
+
473
+ ```tsx
474
+ // 靜態用法
475
+ <GradientMask
476
+ colors={GRADIENT_COLORS}
477
+ locations={GRADIENT_LOCATIONS}
478
+ direction="top"
479
+ maskOpacity={1} // 固定值
480
+ >
481
+ <FlashList ... />
482
+ </GradientMask>
483
+
484
+ // 動畫用法 (搭配 useMaskedView)
485
+ const { maskOpacity } = useMaskedView();
486
+
487
+ <AnimatedGradientMask
488
+ colors={GRADIENT_COLORS}
489
+ locations={GRADIENT_LOCATIONS}
490
+ direction="top"
491
+ maskOpacity={maskOpacity} // SharedValue<number>
492
+ >
493
+ <FlashList ... />
494
+ </AnimatedGradientMask>
495
+ ```
496
+
497
+ ---
498
+
499
+ ## 十、檔案修改清單
500
+
501
+ ### 需要修改的檔案
502
+
503
+ | 檔案 | 修改內容 |
504
+ |-----|---------|
505
+ | `ios/RNGradientMask/RNGradientMask.swift` | 確認 maskOpacity prop 正確運作 |
506
+ | `ios/RNGradientMask/RNGradientMaskManager.m` | 確認導出 maskOpacity |
507
+ | `android/.../RNGradientMaskView.kt` | 確認 maskOpacity prop 正確運作 |
508
+ | `android/.../RNGradientMaskManager.kt` | 確認導出 maskOpacity |
509
+ | `src/native-modules/rn-gradient-mask/index.tsx` | 簡化,移除多餘的 hook |
510
+
511
+ ### 需要刪除的檔案
512
+
513
+ | 檔案 | 原因 |
514
+ |-----|------|
515
+ | `src/native-modules/rn-gradient-mask/hooks/index.ts` | 多餘,直接使用 useMaskedView |
516
+
517
+ ### CharacterChatScreen 遷移
518
+
519
+ ```tsx
520
+ // 移除
521
+ import MaskedView from "@react-native-masked-view/masked-view";
522
+ import AnimatedMaskGradient from "./components/AnimatedMaskGradient";
523
+
524
+ // 新增
525
+ import { AnimatedGradientMask, GRADIENT_PRESETS } from "@/native-modules/rn-gradient-mask";
526
+
527
+ // 移除 maskElement
528
+ const maskElement = useMemo(() => (
529
+ <AnimatedMaskGradient maskOpacity={maskOpacity} style={styles.gradientMask} />
530
+ ), [maskOpacity]);
531
+
532
+ // 替換 MaskedView
533
+ <MaskedView style={{ flex: 1 }} maskElement={maskElement}>
534
+ <FlashList ... />
535
+ </MaskedView>
536
+
537
+ // 改為
538
+ <AnimatedGradientMask
539
+ style={{ flex: 1 }}
540
+ colors={GRADIENT_PRESETS.CHAT_MASK.colors}
541
+ locations={GRADIENT_PRESETS.CHAT_MASK.locations}
542
+ direction="top"
543
+ maskOpacity={maskOpacity}
544
+ >
545
+ <FlashList ... />
546
+ </AnimatedGradientMask>
547
+ ```
548
+
549
+ ---
550
+
551
+ ## 十一、實作步驟
552
+
553
+ ### 階段 1:驗證現有原生實作
554
+
555
+ 1. 確認 iOS `RNGradientMask.swift` 的 maskOpacity 行為正確
556
+ 2. 確認 Android `RNGradientMaskView.kt` 的 maskOpacity 行為正確
557
+ 3. 使用 `CharacterChatScreenV3DTestNative` 測試頁面驗證
558
+
559
+ ### 階段 2:簡化 JS 層
560
+
561
+ 1. 移除 `hooks/index.ts`(useGradientMaskAnimation)
562
+ 2. 更新 `index.tsx`:
563
+ - `GradientMask`: 接受 `maskOpacity: number`
564
+ - `AnimatedGradientMask`: 接受 `maskOpacity: SharedValue<number>`
565
+ 3. 確認 `useAnimatedProps` 正確驅動原生 prop
566
+
567
+ ### 階段 3:整合測試
568
+
569
+ 1. 更新 `CharacterChatScreenV3DTestNative` 使用 `useMaskedView`
570
+ 2. 比較效能與原有 MaskedView 方案
571
+ 3. 確認所有使用情境正常:
572
+ - 滾動顯示/隱藏 mask
573
+ - 離開畫面時 cancelAnimation
574
+ - 點擊背景重置 mask
575
+
576
+ ### 階段 4:正式遷移(如果測試通過)
577
+
578
+ 1. 在 `CharacterChatScreen` 替換 MaskedView
579
+ 2. 移除 `AnimatedMaskGradient.tsx` 元件
580
+ 3. 更新相關 import
581
+
582
+ ---
583
+
584
+ ## 十二、預設漸層配置
585
+
586
+ ```typescript
587
+ // src/native-modules/rn-gradient-mask/index.tsx
588
+
589
+ export const GRADIENT_PRESETS = {
590
+ /**
591
+ * 聊天畫面遮罩 - 對應 AnimatedMaskGradient 的配置
592
+ * 頂部 42% 透明,漸層過渡到不透明
593
+ */
594
+ CHAT_MASK: {
595
+ colors: [
596
+ "rgba(0,0,0,0)", // 0% - 透明
597
+ "rgba(0,0,0,0)", // 42% - 透明
598
+ "rgba(0,0,0,0.2)", // 45% - 20% 不透明
599
+ "rgba(0,0,0,0.6)", // 48% - 60% 不透明
600
+ "rgba(0,0,0,0.9)", // 50% - 90% 不透明
601
+ "rgba(0,0,0,1)", // 100% - 不透明
602
+ ],
603
+ locations: [0, 0.42, 0.45, 0.48, 0.5, 1],
604
+ },
605
+ } as const;
606
+ ```
607
+
608
+ ---
609
+
610
+ ## 十三、實作狀態與待辦事項
611
+
612
+ ### 當前實作狀態
613
+
614
+ | 功能 | 狀態 | 說明 |
615
+ |-----|------|------|
616
+ | 基本漸層遮罩 | ✅ 已完成 | iOS 和 Android 原生實作完成 |
617
+ | maskOpacity 靜態控制 | ✅ 已完成 | 支援 0-1 數值控制 |
618
+ | 漸層方向支援 | ✅ 已完成 | top/bottom/left/right |
619
+ | 顏色和位置配置 | ✅ 已完成 | colors 和 locations props |
620
+ | **maskOpacity 動畫** | ⏳ **待實作** | 使用方案 A(JS 層驅動) |
621
+ | AnimatedGradientMask 元件 | ⏳ **待實作** | 支援 SharedValue 動畫 |
622
+ | useAnimatedProps 整合 | ⏳ **待實作** | Reanimated 動畫驅動 |
623
+
624
+ ### 動畫方案決定
625
+
626
+ - **主要方案**:方案 A(JS 層驅動 + useAnimatedProps)
627
+ - 狀態:✅ 已決定採用,待實作
628
+ - 實作方式:使用 `useAnimatedProps` 將 Reanimated `SharedValue` 傳遞給原生層
629
+ - 動畫時序:顯示 600ms (ease-in-quad),隱藏 400ms (ease-out-quad)
630
+
631
+ - **備用方案**:方案 B(原生層驅動)
632
+ - 狀態:🔄 備用方案,暫不實作
633
+ - 使用時機:如果方案 A 效能無法滿足需求時考慮
634
+ - 實作方式:原生層內建 CABasicAnimation (iOS) / ValueAnimator (Android)
635
+
636
+ ### 待實作項目
637
+
638
+ 1. **AnimatedGradientMask 元件**
639
+ - 創建支援 `SharedValue<number>` 的動畫版本元件
640
+ - 使用 `useAnimatedProps` 將動畫值傳遞給原生層
641
+ - 保持與靜態 `GradientMask` 元件相同的 API
642
+
643
+ 2. **動畫時序配置**
644
+ - 顯示動畫:600ms,ease-in-quad
645
+ - 隱藏動畫:400ms,ease-out-quad
646
+ - 支援 `cancelAnimation` 功能
647
+
648
+ 3. **測試與驗證**
649
+ - 驗證動畫流暢度
650
+ - 測試與 FlashList 的相容性
651
+ - 效能對比測試(與原有 MaskedView 方案)
652
+
653
+ ### 備註
654
+
655
+ - 當前版本已符合基本需求(靜態 maskOpacity 控制)
656
+ - 動畫功能待後續實作,不影響基本使用
657
+ - 方案 B 的實作細節已記錄,可作為未來優化方向