react-native-gradient-mask 0.0.1-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 (59) hide show
  1. package/.eslintrc.js +5 -0
  2. package/README.ja.md +335 -0
  3. package/README.md +335 -0
  4. package/README.zh-TW.md +335 -0
  5. package/android/build.gradle +43 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/expo/modules/gradientmask/GradientMaskModule.kt +33 -0
  8. package/android/src/main/java/expo/modules/gradientmask/GradientMaskView.kt +198 -0
  9. package/build/AnimatedGradientMaskView.d.ts +36 -0
  10. package/build/AnimatedGradientMaskView.d.ts.map +1 -0
  11. package/build/AnimatedGradientMaskView.js +37 -0
  12. package/build/AnimatedGradientMaskView.js.map +1 -0
  13. package/build/AnimatedGradientMaskView.web.d.ts +17 -0
  14. package/build/AnimatedGradientMaskView.web.d.ts.map +1 -0
  15. package/build/AnimatedGradientMaskView.web.js +100 -0
  16. package/build/AnimatedGradientMaskView.web.js.map +1 -0
  17. package/build/GradientMask.types.d.ts +38 -0
  18. package/build/GradientMask.types.d.ts.map +1 -0
  19. package/build/GradientMask.types.js +2 -0
  20. package/build/GradientMask.types.js.map +1 -0
  21. package/build/GradientMaskModule.d.ts +3 -0
  22. package/build/GradientMaskModule.d.ts.map +1 -0
  23. package/build/GradientMaskModule.js +4 -0
  24. package/build/GradientMaskModule.js.map +1 -0
  25. package/build/GradientMaskModule.web.d.ts +3 -0
  26. package/build/GradientMaskModule.web.d.ts.map +1 -0
  27. package/build/GradientMaskModule.web.js +3 -0
  28. package/build/GradientMaskModule.web.js.map +1 -0
  29. package/build/GradientMaskView.d.ts +4 -0
  30. package/build/GradientMaskView.d.ts.map +1 -0
  31. package/build/GradientMaskView.js +7 -0
  32. package/build/GradientMaskView.js.map +1 -0
  33. package/build/GradientMaskView.web.d.ts +8 -0
  34. package/build/GradientMaskView.web.d.ts.map +1 -0
  35. package/build/GradientMaskView.web.js +99 -0
  36. package/build/GradientMaskView.web.js.map +1 -0
  37. package/build/index.d.ts +6 -0
  38. package/build/index.d.ts.map +1 -0
  39. package/build/index.js +7 -0
  40. package/build/index.js.map +1 -0
  41. package/expo-module.config.json +9 -0
  42. package/images/android.mp4 +0 -0
  43. package/images/android.png +0 -0
  44. package/images/demo.mov +0 -0
  45. package/images/ios Demo Video.webm +0 -0
  46. package/images/ios.png +0 -0
  47. package/ios/GradientMask.podspec +29 -0
  48. package/ios/GradientMaskModule.swift +29 -0
  49. package/ios/GradientMaskView.swift +133 -0
  50. package/package.json +83 -0
  51. package/src/AnimatedGradientMaskView.tsx +60 -0
  52. package/src/AnimatedGradientMaskView.web.tsx +149 -0
  53. package/src/GradientMask.types.ts +43 -0
  54. package/src/GradientMaskModule.ts +4 -0
  55. package/src/GradientMaskModule.web.ts +2 -0
  56. package/src/GradientMaskView.tsx +11 -0
  57. package/src/GradientMaskView.web.tsx +129 -0
  58. package/src/index.ts +7 -0
  59. package/tsconfig.json +9 -0
@@ -0,0 +1,335 @@
1
+ <p align="center">
2
+ <h1 align="center">react-native-gradient-mask</h1>
3
+ </p>
4
+
5
+ <p align="center">
6
+ <b>React Native 原生漸層遮罩元件</b>
7
+ </p>
8
+
9
+ <p align="center">
10
+ 輕鬆建立精美的淡出效果、列表遮罩與流暢的漸層過渡動畫,具備原生效能與 Reanimated 動畫支援。
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://www.npmjs.com/package/react-native-gradient-mask">
15
+ <img src="https://img.shields.io/npm/v/react-native-gradient-mask.svg" alt="npm version" />
16
+ </a>
17
+ <a href="https://www.npmjs.com/package/react-native-gradient-mask">
18
+ <img src="https://img.shields.io/npm/dm/react-native-gradient-mask.svg" alt="npm downloads" />
19
+ </a>
20
+ <img src="https://img.shields.io/badge/platforms-iOS%20%7C%20Android%20%7C%20Web-brightgreen.svg" alt="platforms" />
21
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license" />
22
+ </p>
23
+
24
+ <p align="center">
25
+ <a href="./README.md">English</a> •
26
+ <a href="./README.ja.md">日本語</a>
27
+ </p>
28
+
29
+ ---
30
+
31
+ ## 展示
32
+
33
+ <p align="center">
34
+ <table>
35
+ <tr>
36
+ <td align="center"><b>iOS</b></td>
37
+ <td align="center"><b>Android</b></td>
38
+ </tr>
39
+ <tr>
40
+ <td><a href="./images/ios Demo Video.webm"><img src="./images/ios.png" alt="iOS 展示" width="280" /></a></td>
41
+ <td><a href="./images/android.mp4"><img src="./images/android.png" alt="Android 展示" width="280" /></a></td>
42
+ </tr>
43
+ </table>
44
+ </p>
45
+
46
+ ## 特色
47
+
48
+ | 特色 | 說明 |
49
+ |------|------|
50
+ | **跨平台** | 支援 iOS、Android 和 Web |
51
+ | **原生效能** | iOS: `CAGradientLayer` • Android: `Bitmap` + `PorterDuff` • Web: CSS `mask-image` |
52
+ | **Reanimated 支援** | 透過 `AnimatedGradientMaskView` 實現 60fps 流暢遮罩動畫 |
53
+ | **彈性設定** | 自訂顏色、位置、方向與遮罩強度 |
54
+ | **TypeScript** | 完整型別定義 |
55
+
56
+ ## 安裝
57
+
58
+ ```bash
59
+ npm install react-native-gradient-mask
60
+ ```
61
+
62
+ ```bash
63
+ yarn add react-native-gradient-mask
64
+ ```
65
+
66
+ ### 需求
67
+
68
+ | 相依套件 | 版本 |
69
+ |----------|------|
70
+ | Expo SDK | 50+ |
71
+ | React Native | 0.73+ |
72
+ | react-native-reanimated | >= 3.0.0 *(選用)* |
73
+
74
+ ### 設定
75
+
76
+ <details>
77
+ <summary><b>iOS</b></summary>
78
+
79
+ ```bash
80
+ cd ios && pod install
81
+ ```
82
+ </details>
83
+
84
+ <details>
85
+ <summary><b>Android</b></summary>
86
+
87
+ 無需額外設定,自動連結已啟用。
88
+ </details>
89
+
90
+ ---
91
+
92
+ ## 快速開始
93
+
94
+ ```tsx
95
+ import { processColor } from 'react-native';
96
+ import { GradientMaskView } from 'react-native-gradient-mask';
97
+
98
+ const colors = [
99
+ processColor('rgba(0,0,0,0)'),
100
+ processColor('rgba(0,0,0,1)'),
101
+ ];
102
+
103
+ export default function App() {
104
+ return (
105
+ <GradientMaskView
106
+ colors={colors}
107
+ locations={[0, 1]}
108
+ direction="top"
109
+ style={{ flex: 1 }}
110
+ >
111
+ <YourContent />
112
+ </GradientMaskView>
113
+ );
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## API 參考
120
+
121
+ ### 元件
122
+
123
+ | 元件 | 說明 |
124
+ |------|------|
125
+ | `GradientMaskView` | 基礎漸層遮罩元件 |
126
+ | `AnimatedGradientMaskView` | 支援 Reanimated 動畫的漸層遮罩元件 |
127
+
128
+ ### Props
129
+
130
+ #### GradientMaskView
131
+
132
+ | 屬性 | 型別 | 必填 | 預設值 | 說明 |
133
+ |------|------|:----:|--------|------|
134
+ | `colors` | `(number \| null)[]` | 是 | - | 漸層顏色(需使用 `processColor()`) |
135
+ | `locations` | `number[]` | 是 | - | 顏色位置 (0-1) |
136
+ | `direction` | `'top' \| 'bottom' \| 'left' \| 'right'` | 否 | `'top'` | 漸層方向 |
137
+ | `maskOpacity` | `number` | 否 | `1` | 遮罩強度 (0-1) |
138
+ | `style` | `ViewStyle` | 否 | - | 容器樣式 |
139
+ | `children` | `ReactNode` | 否 | - | 要套用遮罩的內容 |
140
+
141
+ #### AnimatedGradientMaskView
142
+
143
+ 與 `GradientMaskView` 相同,但 `maskOpacity` 接受 `SharedValue<number>` 用於動畫控制。
144
+
145
+ ### 方向說明
146
+
147
+ | 方向 | 效果 |
148
+ |------|------|
149
+ | `top` | 頂部透明 → 底部不透明 |
150
+ | `bottom` | 底部透明 → 頂部不透明 |
151
+ | `left` | 左側透明 → 右側不透明 |
152
+ | `right` | 右側透明 → 左側不透明 |
153
+
154
+ ---
155
+
156
+ ## 範例
157
+
158
+ ### 基本淡出效果
159
+
160
+ ```tsx
161
+ import { processColor } from 'react-native';
162
+ import { GradientMaskView } from 'react-native-gradient-mask';
163
+
164
+ const colors = [
165
+ processColor('rgba(0,0,0,0)'),
166
+ processColor('rgba(0,0,0,0.5)'),
167
+ processColor('rgba(0,0,0,1)'),
168
+ ];
169
+
170
+ function FadeExample() {
171
+ return (
172
+ <GradientMaskView
173
+ colors={colors}
174
+ locations={[0, 0.3, 1]}
175
+ direction="top"
176
+ style={{ flex: 1 }}
177
+ >
178
+ <ScrollView>
179
+ <Text>套用淡出效果的內容</Text>
180
+ </ScrollView>
181
+ </GradientMaskView>
182
+ );
183
+ }
184
+ ```
185
+
186
+ ### 搭配 Reanimated 動畫
187
+
188
+ ```tsx
189
+ import { processColor } from 'react-native';
190
+ import { AnimatedGradientMaskView } from 'react-native-gradient-mask';
191
+ import { useSharedValue, withTiming } from 'react-native-reanimated';
192
+
193
+ function AnimatedExample() {
194
+ const maskOpacity = useSharedValue(0);
195
+
196
+ const showMask = () => {
197
+ maskOpacity.value = withTiming(1, { duration: 600 });
198
+ };
199
+
200
+ const hideMask = () => {
201
+ maskOpacity.value = withTiming(0, { duration: 400 });
202
+ };
203
+
204
+ return (
205
+ <AnimatedGradientMaskView
206
+ colors={[
207
+ processColor('rgba(0,0,0,0)'),
208
+ processColor('rgba(0,0,0,1)'),
209
+ ]}
210
+ locations={[0, 1]}
211
+ maskOpacity={maskOpacity}
212
+ style={{ flex: 1 }}
213
+ >
214
+ <YourContent />
215
+ </AnimatedGradientMaskView>
216
+ );
217
+ }
218
+ ```
219
+
220
+ ### 聊天列表動態遮罩
221
+
222
+ ```tsx
223
+ import { useMemo, useCallback, useRef } from 'react';
224
+ import { processColor } from 'react-native';
225
+ import { FlashList } from '@shopify/flash-list';
226
+ import { AnimatedGradientMaskView } from 'react-native-gradient-mask';
227
+ import { useSharedValue, withTiming, cancelAnimation, Easing } from 'react-native-reanimated';
228
+
229
+ function ChatList({ messages }) {
230
+ const maskOpacity = useSharedValue(0);
231
+ const isAtBottomRef = useRef(false);
232
+
233
+ const maskColors = useMemo(() => [
234
+ processColor('rgba(0,0,0,0)'),
235
+ processColor('rgba(0,0,0,0)'),
236
+ processColor('rgba(0,0,0,0.2)'),
237
+ processColor('rgba(0,0,0,0.6)'),
238
+ processColor('rgba(0,0,0,0.9)'),
239
+ processColor('rgba(0,0,0,1)'),
240
+ ], []);
241
+
242
+ const handleScroll = useCallback((e) => {
243
+ const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
244
+ const distanceFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height;
245
+ const isAtBottom = distanceFromBottom <= 30;
246
+
247
+ if (isAtBottom !== isAtBottomRef.current) {
248
+ isAtBottomRef.current = isAtBottom;
249
+ cancelAnimation(maskOpacity);
250
+ maskOpacity.value = withTiming(isAtBottom ? 1 : 0, {
251
+ duration: isAtBottom ? 600 : 400,
252
+ easing: isAtBottom ? Easing.in(Easing.quad) : Easing.out(Easing.quad),
253
+ });
254
+ }
255
+ }, []);
256
+
257
+ return (
258
+ <AnimatedGradientMaskView
259
+ colors={maskColors}
260
+ locations={[0, 0.42, 0.45, 0.48, 0.5, 1]}
261
+ direction="top"
262
+ maskOpacity={maskOpacity}
263
+ style={{ flex: 1 }}
264
+ >
265
+ <FlashList
266
+ data={messages}
267
+ renderItem={({ item }) => <MessageItem item={item} />}
268
+ onScroll={handleScroll}
269
+ scrollEventThrottle={16}
270
+ />
271
+ </AnimatedGradientMaskView>
272
+ );
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## 技巧與最佳實踐
279
+
280
+ ### 務必使用 `processColor()`
281
+
282
+ ```tsx
283
+ // ✅ 正確
284
+ const colors = [
285
+ processColor('rgba(0,0,0,0)'),
286
+ processColor('rgba(0,0,0,1)'),
287
+ ];
288
+
289
+ // ❌ 錯誤 - 無法運作
290
+ const colors = [
291
+ 'rgba(0,0,0,0)',
292
+ 'rgba(0,0,0,1)',
293
+ ];
294
+ ```
295
+
296
+ ### 使用 `useMemo` 優化效能
297
+
298
+ ```tsx
299
+ const maskColors = useMemo(() => [
300
+ processColor('rgba(0,0,0,0)'),
301
+ processColor('rgba(0,0,0,1)'),
302
+ ], []);
303
+ ```
304
+
305
+ ### 避免閃爍
306
+
307
+ ```tsx
308
+ import { cancelAnimation } from 'react-native-reanimated';
309
+
310
+ // 在開始新動畫前取消前一個動畫
311
+ cancelAnimation(maskOpacity);
312
+ maskOpacity.value = withTiming(newValue, { duration: 300 });
313
+ ```
314
+
315
+ ---
316
+
317
+ ## 平台支援
318
+
319
+ | 平台 | 實作方式 | 狀態 |
320
+ |------|----------|:----:|
321
+ | iOS | `CAGradientLayer` | ✅ |
322
+ | Android | `Bitmap` + `LinearGradient` + `PorterDuff` | ✅ |
323
+ | Web | CSS `mask-image` + `linear-gradient` | ✅ |
324
+
325
+ ---
326
+
327
+ ## 授權
328
+
329
+ MIT © [DaYuan Lin (CS6)](https://github.com/CS6)
330
+
331
+ ---
332
+
333
+ <p align="center">
334
+ <sub>為 React Native 社群用心打造 ❤️</sub>
335
+ </p>
@@ -0,0 +1,43 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'expo.modules.gradientmask'
4
+ version = '0.1.0'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
+ // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
+ // Most of the time, you may like to manage the Android SDK versions yourself.
15
+ def useManagedAndroidSdkVersions = false
16
+ if (useManagedAndroidSdkVersions) {
17
+ useDefaultAndroidSdkVersions()
18
+ } else {
19
+ buildscript {
20
+ // Simple helper that allows the root project to override versions declared by this library.
21
+ ext.safeExtGet = { prop, fallback ->
22
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23
+ }
24
+ }
25
+ project.android {
26
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
27
+ defaultConfig {
28
+ minSdkVersion safeExtGet("minSdkVersion", 24)
29
+ targetSdkVersion safeExtGet("targetSdkVersion", 36)
30
+ }
31
+ }
32
+ }
33
+
34
+ android {
35
+ namespace "expo.modules.gradientmask"
36
+ defaultConfig {
37
+ versionCode 1
38
+ versionName "0.1.0"
39
+ }
40
+ lintOptions {
41
+ abortOnError false
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,33 @@
1
+ package expo.modules.gradientmask
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class GradientMaskModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("GradientMask")
9
+
10
+ // View definition
11
+ View(GradientMaskView::class) {
12
+ // colors: List of processed colors (from processColor in JS)
13
+ Prop("colors") { view: GradientMaskView, colors: List<Int>? ->
14
+ view.setColors(colors)
15
+ }
16
+
17
+ // locations: Array of floats (0-1) for gradient stops
18
+ Prop("locations") { view: GradientMaskView, locations: List<Double>? ->
19
+ view.setLocations(locations)
20
+ }
21
+
22
+ // direction: "top" | "bottom" | "left" | "right"
23
+ Prop("direction") { view: GradientMaskView, direction: String? ->
24
+ view.setDirection(direction ?: "top")
25
+ }
26
+
27
+ // maskOpacity: 0 = no mask effect, 1 = full gradient mask
28
+ Prop("maskOpacity") { view: GradientMaskView, opacity: Double? ->
29
+ view.setMaskOpacity(opacity ?: 1.0)
30
+ }
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,198 @@
1
+ package expo.modules.gradientmask
2
+
3
+ import android.content.Context
4
+ import android.graphics.*
5
+ import android.view.View
6
+ import expo.modules.kotlin.AppContext
7
+ import expo.modules.kotlin.views.ExpoView
8
+
9
+ /**
10
+ * GradientMaskView - 原生漸層透明遮罩
11
+ *
12
+ * 使用 Bitmap 作為 mask,配合 setLayerType + PorterDuff.Mode.DST_IN
13
+ *
14
+ * 顏色語意(與 iOS CAGradientLayer mask 一致):
15
+ * - 顏色的 alpha 值決定該區域內容的可見度
16
+ * - alpha = 0 → 內容透明(看到背景)
17
+ * - alpha = 255 → 內容不透明(看到內容)
18
+ *
19
+ * maskOpacity 控制漸層 mask 效果的顯示程度:
20
+ * - maskOpacity = 0 → 無漸層效果,內容完全可見(全部 alpha=255)
21
+ * - maskOpacity = 1 → 完整漸層效果(使用原始 alpha)
22
+ */
23
+ class GradientMaskView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
24
+
25
+ // Gradient mask 的參數
26
+ private var colors: IntArray? = null
27
+ private var locations: FloatArray? = null
28
+ private var direction: String = "top"
29
+
30
+ // maskOpacity: 0 = 無漸層效果, 1 = 完整漸層效果
31
+ private var maskOpacity: Float = 1f
32
+
33
+ // Mask bitmap 和相關的 Paint
34
+ private var maskBitmap: Bitmap? = null
35
+ private var maskBitmapInvalidated = true
36
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
37
+ private val porterDuffXferMode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
38
+
39
+ init {
40
+ // 確保背景是透明的
41
+ setBackgroundColor(Color.TRANSPARENT)
42
+ // 始終使用 SOFTWARE 模式,避免動態切換層模式造成的黑色閃爍
43
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null)
44
+
45
+ android.util.Log.d("GradientMask", "=== GradientMaskView init ===")
46
+ }
47
+
48
+ // MARK: - Props setters
49
+
50
+ fun setColors(colorArray: List<Int>?) {
51
+ colors = colorArray?.toIntArray()
52
+ maskBitmapInvalidated = true
53
+ invalidate()
54
+ }
55
+
56
+ fun setLocations(locationArray: List<Double>?) {
57
+ locations = locationArray?.map { it.toFloat() }?.toFloatArray()
58
+ maskBitmapInvalidated = true
59
+ invalidate()
60
+ }
61
+
62
+ fun setDirection(dir: String) {
63
+ direction = dir
64
+ maskBitmapInvalidated = true
65
+ invalidate()
66
+ }
67
+
68
+ fun setMaskOpacity(opacity: Double) {
69
+ maskOpacity = opacity.toFloat().coerceIn(0f, 1f)
70
+ maskBitmapInvalidated = true
71
+ invalidate()
72
+ }
73
+
74
+ // MARK: - Layout
75
+
76
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
77
+ super.onSizeChanged(w, h, oldw, oldh)
78
+ if (w > 0 && h > 0) {
79
+ updateMaskBitmap()
80
+ maskBitmapInvalidated = false
81
+ }
82
+ }
83
+
84
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
85
+ super.onLayout(changed, l, t, r, b)
86
+ if (changed) {
87
+ maskBitmapInvalidated = true
88
+ }
89
+ }
90
+
91
+ // MARK: - Drawing
92
+
93
+ override fun dispatchDraw(canvas: Canvas) {
94
+ // 檢查尺寸是否有效
95
+ if (width <= 0 || height <= 0) {
96
+ super.dispatchDraw(canvas)
97
+ return
98
+ }
99
+
100
+ // 如果 mask bitmap 需要更新,重新創建
101
+ if (maskBitmapInvalidated) {
102
+ updateMaskBitmap()
103
+ maskBitmapInvalidated = false
104
+ }
105
+
106
+ val bitmap = maskBitmap
107
+ // 如果沒有 mask bitmap 或 maskOpacity=0,直接繪製子元件(無 mask 效果)
108
+ if (bitmap == null || maskOpacity <= 0f) {
109
+ super.dispatchDraw(canvas)
110
+ return
111
+ }
112
+
113
+ // 使用 saveLayer 創建離屏緩衝區
114
+ val saveCount = canvas.saveLayer(
115
+ 0f, 0f,
116
+ width.toFloat(), height.toFloat(),
117
+ null
118
+ )
119
+
120
+ try {
121
+ // 先繪製所有子元件到離屏緩衝區
122
+ super.dispatchDraw(canvas)
123
+
124
+ // 應用 mask(使用 DST_IN 模式)
125
+ paint.xfermode = porterDuffXferMode
126
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
127
+ paint.xfermode = null
128
+ } finally {
129
+ canvas.restoreToCount(saveCount)
130
+ }
131
+ }
132
+
133
+ private fun updateMaskBitmap() {
134
+ if (width <= 0 || height <= 0) return
135
+
136
+ // 回收舊的 bitmap
137
+ maskBitmap?.recycle()
138
+
139
+ // 創建新的 mask bitmap
140
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
141
+ val bitmapCanvas = Canvas(bitmap)
142
+
143
+ val currentColors = colors
144
+ val currentLocations = locations
145
+
146
+ // 如果沒有 colors/locations,創建全白 mask(內容完全可見)
147
+ if (currentColors == null || currentLocations == null ||
148
+ currentColors.size != currentLocations.size ||
149
+ currentColors.isEmpty()) {
150
+ bitmapCanvas.drawColor(Color.WHITE)
151
+ maskBitmap = bitmap
152
+ return
153
+ }
154
+
155
+ // 計算 effective colors(根據 maskOpacity 混合)
156
+ val effectiveColors = IntArray(currentColors.size) { i ->
157
+ val originalColor = currentColors[i]
158
+ val originalAlpha = Color.alpha(originalColor)
159
+ // 當 maskOpacity = 0,alpha = 255(完全不透明,內容完全可見)
160
+ // 當 maskOpacity = 1,alpha = originalAlpha
161
+ val blendedAlpha = (255 + (originalAlpha - 255) * maskOpacity).toInt()
162
+ Color.argb(blendedAlpha, 255, 255, 255)
163
+ }
164
+
165
+ // 建立 gradient shader
166
+ val (startX, startY, endX, endY) = getGradientCoordinates()
167
+ val shader = LinearGradient(
168
+ startX, startY, endX, endY,
169
+ effectiveColors,
170
+ currentLocations,
171
+ Shader.TileMode.CLAMP
172
+ )
173
+
174
+ // 繪製漸層到 bitmap
175
+ val gradientPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
176
+ this.shader = shader
177
+ }
178
+ bitmapCanvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint)
179
+
180
+ maskBitmap = bitmap
181
+ }
182
+
183
+ override fun onDetachedFromWindow() {
184
+ super.onDetachedFromWindow()
185
+ maskBitmap?.recycle()
186
+ maskBitmap = null
187
+ }
188
+
189
+ private fun getGradientCoordinates(): List<Float> {
190
+ return when (direction) {
191
+ "top" -> listOf(0f, 0f, 0f, height.toFloat())
192
+ "bottom" -> listOf(0f, height.toFloat(), 0f, 0f)
193
+ "left" -> listOf(0f, 0f, width.toFloat(), 0f)
194
+ "right" -> listOf(width.toFloat(), 0f, 0f, 0f)
195
+ else -> listOf(0f, 0f, 0f, height.toFloat())
196
+ }
197
+ }
198
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ import { GradientMaskViewProps } from './GradientMask.types';
4
+ export type AnimatedGradientMaskViewProps = Omit<GradientMaskViewProps, 'maskOpacity'> & {
5
+ /**
6
+ * Mask effect intensity (0-1) as a Reanimated SharedValue
7
+ * 0 = no gradient effect (content fully visible)
8
+ * 1 = full gradient effect
9
+ */
10
+ maskOpacity: SharedValue<number>;
11
+ };
12
+ /**
13
+ * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件
14
+ *
15
+ * 使用方式:
16
+ * ```tsx
17
+ * const maskOpacity = useSharedValue(0);
18
+ *
19
+ * // 動畫顯示
20
+ * maskOpacity.value = withTiming(1, {
21
+ * duration: 600,
22
+ * easing: Easing.in(Easing.quad),
23
+ * });
24
+ *
25
+ * <AnimatedGradientMaskView
26
+ * colors={colors}
27
+ * locations={locations}
28
+ * direction="top"
29
+ * maskOpacity={maskOpacity}
30
+ * >
31
+ * <FlashList ... />
32
+ * </AnimatedGradientMaskView>
33
+ * ```
34
+ */
35
+ export default function AnimatedGradientMaskView(props: AnimatedGradientMaskViewProps): React.JSX.Element;
36
+ //# sourceMappingURL=AnimatedGradientMaskView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedGradientMaskView.d.ts","sourceRoot":"","sources":["../src/AnimatedGradientMaskView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAO7D,MAAM,MAAM,6BAA6B,GAAG,IAAI,CAC9C,qBAAqB,EACrB,aAAa,CACd,GAAG;IACF;;;;OAIG;IACH,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,KAAK,EAAE,6BAA6B,qBASrC"}
@@ -0,0 +1,37 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+ import { useAnimatedProps } from 'react-native-reanimated';
4
+ import Animated from 'react-native-reanimated';
5
+ const NativeView = requireNativeView('GradientMask');
6
+ const AnimatedNativeView = Animated.createAnimatedComponent(NativeView);
7
+ /**
8
+ * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件
9
+ *
10
+ * 使用方式:
11
+ * ```tsx
12
+ * const maskOpacity = useSharedValue(0);
13
+ *
14
+ * // 動畫顯示
15
+ * maskOpacity.value = withTiming(1, {
16
+ * duration: 600,
17
+ * easing: Easing.in(Easing.quad),
18
+ * });
19
+ *
20
+ * <AnimatedGradientMaskView
21
+ * colors={colors}
22
+ * locations={locations}
23
+ * direction="top"
24
+ * maskOpacity={maskOpacity}
25
+ * >
26
+ * <FlashList ... />
27
+ * </AnimatedGradientMaskView>
28
+ * ```
29
+ */
30
+ export default function AnimatedGradientMaskView(props) {
31
+ const { maskOpacity, ...restProps } = props;
32
+ const animatedProps = useAnimatedProps(() => ({
33
+ maskOpacity: maskOpacity.value,
34
+ }));
35
+ return <AnimatedNativeView {...restProps} animatedProps={animatedProps}/>;
36
+ }
37
+ //# sourceMappingURL=AnimatedGradientMaskView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedGradientMaskView.js","sourceRoot":"","sources":["../src/AnimatedGradientMaskView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,QAAQ,MAAM,yBAAyB,CAAC;AAI/C,MAAM,UAAU,GACd,iBAAiB,CAAC,cAAc,CAAC,CAAC;AAEpC,MAAM,kBAAkB,GAAG,QAAQ,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC;AAcxE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,KAAoC;IAEpC,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE,GAAG,KAAK,CAAC;IAE5C,MAAM,aAAa,GAAG,gBAAgB,CAA6C,GAAG,EAAE,CAAC,CAAC;QACxF,WAAW,EAAE,WAAW,CAAC,KAAK;KAC/B,CAAC,CAAC,CAAC;IAEJ,OAAO,CAAC,kBAAkB,CAAC,IAAI,SAAS,CAAC,CAAC,aAAa,CAAC,CAAC,aAAa,CAAC,EAAG,CAAC;AAC7E,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\nimport { useAnimatedProps } from 'react-native-reanimated';\nimport type { SharedValue } from 'react-native-reanimated';\nimport Animated from 'react-native-reanimated';\n\nimport { GradientMaskViewProps } from './GradientMask.types';\n\nconst NativeView: React.ComponentType<GradientMaskViewProps> =\n requireNativeView('GradientMask');\n\nconst AnimatedNativeView = Animated.createAnimatedComponent(NativeView);\n\nexport type AnimatedGradientMaskViewProps = Omit<\n GradientMaskViewProps,\n 'maskOpacity'\n> & {\n /**\n * Mask effect intensity (0-1) as a Reanimated SharedValue\n * 0 = no gradient effect (content fully visible)\n * 1 = full gradient effect\n */\n maskOpacity: SharedValue<number>;\n};\n\n/**\n * AnimatedGradientMaskView - 支援 Reanimated SharedValue 動畫的漸層遮罩元件\n *\n * 使用方式:\n * ```tsx\n * const maskOpacity = useSharedValue(0);\n *\n * // 動畫顯示\n * maskOpacity.value = withTiming(1, {\n * duration: 600,\n * easing: Easing.in(Easing.quad),\n * });\n *\n * <AnimatedGradientMaskView\n * colors={colors}\n * locations={locations}\n * direction=\"top\"\n * maskOpacity={maskOpacity}\n * >\n * <FlashList ... />\n * </AnimatedGradientMaskView>\n * ```\n */\nexport default function AnimatedGradientMaskView(\n props: AnimatedGradientMaskViewProps\n) {\n const { maskOpacity, ...restProps } = props;\n\n const animatedProps = useAnimatedProps<Pick<GradientMaskViewProps, 'maskOpacity'>>(() => ({\n maskOpacity: maskOpacity.value,\n }));\n\n return <AnimatedNativeView {...restProps} animatedProps={animatedProps} />;\n}\n\n"]}