React Native 渐变骨架屏组件 SkeletonElement 使用指南
一、前言
在移动端开发中,数据加载过程中的空白等待会显著降低用户体验。骨架屏(Skeleton Screen) 是一种广受推崇的加载策略:它通过渲染与真实内容结构一致的灰色占位块,在数据返回前给予用户明确的视觉反馈。
本文提供一个轻量、高性能、支持自定义样式的 SkeletonElement 组件,适用于任何需要占位展示的 UI 元素。
二、组件特性
- 完全自定义样式 :通过
style属性控制尺寸、方向、间距等 - 流畅渐变动画 :使用
Animated实现原生驱动的高帧率闪烁效果 - 轻量无依赖:仅基于 React Native 内置模块,零第三方依赖
- 灵活配色:支持自定义背景色和高亮色
- 自动清理:组件卸载时自动停止动画,防止内存泄漏
三、快速上手
tsx
<View>
{/* 标题骨架 */}
<SkeletonElement style={{ height: 24, width: '80%', marginBottom: 16 }} />
{/* 多行文本骨架 */}
<SkeletonElement style={{ height: 16, width: '100%', marginBottom: 8 }} />
<SkeletonElement style={{ height: 16, width: '90%', marginBottom: 8 }} />
<SkeletonElement style={{ height: 16, width: '95%' }} />
{/* 按钮骨架 */}
<SkeletonElement
style={{
height: 48,
width: '50%',
borderRadius: 24,
alignSelf: 'center',
marginTop: 20
}}
/>
</View>
四、API 说明
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
style |
StyleProp<ViewStyle> |
undefined |
自定义骨架样式(宽高、方向、圆角等) |
backgroundColor |
string |
#E4E4E4 |
骨架背景色 |
highlightColor |
string |
#D0D0D0 |
高亮闪烁色(较背景色稍亮) |
speed |
number |
1600 |
完整闪烁周期(毫秒),值越小动画越快 |
五、源码解析
核心逻辑
- 动画驱动 :使用
useRef(new Animated.Value(0.4))创建不触发重渲染的动画值 - 循环闪烁 :通过
Animated.loop + Animated.sequence实现"暗→亮→暗"的循环 - 性能优化 :启用
useNativeDriver: true,动画在原生线程运行 - 资源清理 :
useEffect返回清理函数,确保组件卸载时停止动画
关键代码片段
ts
const opacityValue = useRef(new Animated.Value(0.4)).current;
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(opacityValue, { toValue: 1, ... }),
Animated.timing(opacityValue, { toValue: 0.4, ... })
])
).start();
return () => opacityValue.stopAnimation();
}, [opacityValue, speed]);
六、完整源码
tsx
import React, {useEffect, useRef} from 'react';
import {Animated, Easing, StyleProp, View, ViewStyle} from 'react-native';
interface SkeletonElementProps {
style?: StyleProp<ViewStyle>;
backgroundColor?: string;
highlightColor?: string;
speed?: number;
}
export const SkeletonElement: React.FC<SkeletonElementProps> = ({
style,
backgroundColor = '#E4E4E4',
highlightColor = '#D0D0D0',
speed = 1600,
}) => {
const useSkeletonAnimation = (speed: number) => {
const opacityValue = useRef(new Animated.Value(0.4)).current;
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(opacityValue, {
toValue: 1,
duration: speed / 2,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(opacityValue, {
toValue: 0.4,
duration: speed / 2,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
]),
).start();
// 返回清理函数
return () => {
opacityValue.stopAnimation();
};
}, [opacityValue, speed]);
return opacityValue;
};
const opacityValue = useSkeletonAnimation(speed);
// 合并样式
const skeletonStyle: ViewStyle = {
backgroundColor,
overflow: 'hidden',
position: 'relative',
...(style as ViewStyle),
};
// 添加渐变动画效果
const highlightOverlay = (
<Animated.View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: highlightColor,
opacity: opacityValue,
}}
/>
);
return <View style={skeletonStyle}>{highlightOverlay}</View>;
};
💡 提示:将此组件与条件渲染结合,即可实现优雅的数据加载过渡:
tsx{loading ? <SkeletonElement style={{ height: 200 }} /> : <RealContent />}