react-native-gradient-mask
v0.2.5
Published
A native gradient mask component for React Native with Reanimated animation support. Supports iOS, Android, and Web platforms.
Readme
展示
新版 Example 實錄:螢幕 50% 上緣、背景切換、75% 下緣、獨立羽化與遮罩開關。點 GIF 可下載 MP4;GIF 為 12 fps 預覽,不代表效能量測。
特色
| 特色 | 說明 |
|------|------|
| 跨平台 | 支援 iOS、Android 和 Web |
| 原生效能 | iOS: CAGradientLayer • Android: Canvas.saveLayer + PorterDuff • Web: CSS mask-image |
| Reanimated 支援 | 透過 AnimatedGradientMaskView 在 UI thread 驅動遮罩動畫 |
| 彈性設定 | 自訂顏色、位置、方向與遮罩強度 |
| TypeScript | 完整型別定義 |
安裝
npm install react-native-gradient-maskyarn add react-native-gradient-mask需求
| 相依套件 | 版本 | |----------|------| | Expo SDK | 50+ | | React Native | 0.73+ | | react-native-reanimated | >= 3.0.0 (選用) |
設定
cd ios && pod install無需額外設定,自動連結已啟用。
快速開始
import { processColor } from 'react-native';
import { GradientMaskView } from 'react-native-gradient-mask';
const colors = [
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,1)'),
];
export default function App() {
return (
<GradientMaskView
colors={colors}
locations={[0, 1]}
direction="top"
style={{ flex: 1 }}
>
<YourContent />
</GradientMaskView>
);
}API 參考
元件
| 元件 | 說明 |
|------|------|
| GradientMaskView | 基礎漸層遮罩元件 |
| AnimatedGradientMaskView | 支援 Reanimated 動畫的漸層遮罩元件 |
Props
GradientMaskView
| 屬性 | 型別 | 必填 | 預設值 | 說明 |
|------|------|:----:|--------|------|
| colors | (number \| null)[] | 是 | - | 漸層顏色(需使用 processColor()) |
| locations | number[] | 是 | - | 顏色位置 (0-1) |
| direction | 'top' \| 'bottom' \| 'left' \| 'right' | 否 | 'top' | 漸層方向 |
| maskOpacity | number | 否 | 1 | 遮罩強度 (0-1) |
| style | ViewStyle | 否 | - | 容器樣式 |
| children | ReactNode | 否 | - | 要套用遮罩的內容 |
AnimatedGradientMaskView
與 GradientMaskView 相同,但 maskOpacity 接受 SharedValue<number> 用於動畫控制。
方向說明
| 方向 | 效果 |
|------|------|
| top | 頂部透明 → 底部不透明 |
| bottom | 底部透明 → 頂部不透明 |
| left | 左側透明 → 右側不透明 |
| right | 右側透明 → 左側不透明 |
範例
基本淡出效果
import { processColor } from 'react-native';
import { GradientMaskView } from 'react-native-gradient-mask';
const colors = [
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,0.5)'),
processColor('rgba(0,0,0,1)'),
];
function FadeExample() {
return (
<GradientMaskView
colors={colors}
locations={[0, 0.3, 1]}
direction="top"
style={{ flex: 1 }}
>
<ScrollView>
<Text>套用淡出效果的內容</Text>
</ScrollView>
</GradientMaskView>
);
}搭配 Reanimated 動畫
import { processColor } from 'react-native';
import { AnimatedGradientMaskView } from 'react-native-gradient-mask';
import { useSharedValue, withTiming } from 'react-native-reanimated';
function AnimatedExample() {
const maskOpacity = useSharedValue(0);
const showMask = () => {
maskOpacity.value = withTiming(1, { duration: 600 });
};
const hideMask = () => {
maskOpacity.value = withTiming(0, { duration: 400 });
};
return (
<AnimatedGradientMaskView
colors={[
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,1)'),
]}
locations={[0, 1]}
maskOpacity={maskOpacity}
style={{ flex: 1 }}
>
<YourContent />
</AnimatedGradientMaskView>
);
}聊天列表動態遮罩
import { useMemo, useCallback, useRef } from 'react';
import { processColor } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import { AnimatedGradientMaskView } from 'react-native-gradient-mask';
import { useSharedValue, withTiming, cancelAnimation, Easing } from 'react-native-reanimated';
function ChatList({ messages }) {
const maskOpacity = useSharedValue(0);
const isAtBottomRef = useRef(false);
const maskColors = useMemo(() => [
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,0.2)'),
processColor('rgba(0,0,0,0.6)'),
processColor('rgba(0,0,0,0.9)'),
processColor('rgba(0,0,0,1)'),
], []);
const handleScroll = useCallback((e) => {
const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
const distanceFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height;
const isAtBottom = distanceFromBottom <= 30;
if (isAtBottom !== isAtBottomRef.current) {
isAtBottomRef.current = isAtBottom;
cancelAnimation(maskOpacity);
maskOpacity.value = withTiming(isAtBottom ? 1 : 0, {
duration: isAtBottom ? 600 : 400,
easing: isAtBottom ? Easing.in(Easing.quad) : Easing.out(Easing.quad),
});
}
}, []);
return (
<AnimatedGradientMaskView
colors={maskColors}
locations={[0, 0.42, 0.45, 0.48, 0.5, 1]}
direction="top"
maskOpacity={maskOpacity}
style={{ flex: 1 }}
>
<FlashList
data={messages}
renderItem={({ item }) => <MessageItem item={item} />}
onScroll={handleScroll}
scrollEventThrottle={16}
/>
</AnimatedGradientMaskView>
);
}技巧與最佳實踐
務必使用 processColor()
// ✅ 正確
const colors = [
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,1)'),
];
// ❌ 錯誤 - 無法運作
const colors = [
'rgba(0,0,0,0)',
'rgba(0,0,0,1)',
];使用 useMemo 優化效能
const maskColors = useMemo(() => [
processColor('rgba(0,0,0,0)'),
processColor('rgba(0,0,0,1)'),
], []);避免閃爍
import { cancelAnimation } from 'react-native-reanimated';
// 在開始新動畫前取消前一個動畫
cancelAnimation(maskOpacity);
maskOpacity.value = withTiming(newValue, { duration: 300 });平台支援
| 平台 | 實作方式 | 狀態 |
|------|----------|:----:|
| iOS | CAGradientLayer | ✅ |
| Android | Canvas.saveLayer + LinearGradient + PorterDuff | ✅ |
| Web | CSS mask-image + linear-gradient | ✅ |
為 Anini 打造
這個套件最初是為 Anini 開發的 — 一款提供流暢、原生品質使用者體驗的 AI 聊天夥伴應用程式。
Example 驗證環境
主要 Example 已對齊 App:Expo 56.0.17 / React Native 0.85.3。 重現步驟見升級與驗證紀錄, 原版結果見 Expo 54 建置基準。環境升級與功能修改分開記錄。
贊助
本專案由 GAZAI 贊助 — 打造創新的 AI 驅動應用程式。
授權
MIT © DaYuan Lin (CS6)
Native chat viewport mask
See opt-in viewport API, coordinate rules and Example and validation checklist.
0.2.0:可選觸控範圍與螢幕百分比
restrictTouchesToVisibleArea 預設 false;開啟後只允許在可視範圍內開始新觸控,已開始的拖曳移出範圍仍會繼續,羽化區可操作。
percentageReference 預設 "container",可設為 "screen" 讓百分比/ratio 邊界與羽化使用 RN 螢幕高度。top/bottom 也接受 "50%",原點仍是容器頂端;數值/px 不變。可選 boundaryPercentageReference 僅覆寫邊界基準,不填就共用 percentageReference。
螢幕中線需扣掉容器位置;Example 已完成換算。容器背景設為透明後,隱藏與羽化區域會透出後方頁面。詳見 座標換算與透明背景。
背景點擊穿透(0.2.4)
啟用 restrictTouchesToVisibleArea={true},並將不需接收觸控的祖先容器設為 pointerEvents="box-none",可讓可視範圍外的點擊傳至後方兄弟元件。Chat viewport 先按 Top 50% screen,再將「背景穿透:關」切為「背景穿透:開」,即可點擊後方按鈕。詳見整合與雙平台驗證紀錄。
Navigation stack 與 view recycling(0.2.5)
遮罩不會擋掉 iOS 左緣返回手勢 —— 該手勢的 recognizer 掛在 navigation controller 的 view 上,是遮罩的祖先,而拒收觸控是穿透而非吞掉。真正會弄壞畫面的是 Fabric 在 viewport 遮罩與 gradient 遮罩之間回收原生 view:每個 wrapper 只送自己的 prop,回收後的 view 便保留前一個元素的邊界、觸控限制與漸層狀態。現在每個 wrapper 都會送出完整的原生 prop 面。詳見native stack 與回收驗證紀錄。
