实现效果
需要实现的效果主要有两个,一个是上下翻页效果,还有就是点赞的动画。
效果 (简书好像不支持 webp 格式图片 只能将就一下)
列表页上下翻页实现
播放视频使用 react-native-video 库。列表使用 FlatList,每个 item 占用一整屏,配合 pagingEnabled 属性可以翻页效果。
通过 onViewableItemsChanged
来控制只有当前的页面才播放视频。
const ShortVideoPage = () => {
const [currentItem, setCurrentItem] = useState(0);
const [data, setData] = useState<ItemData[]>([]);
const _onViewableItemsChanged = useCallback(({ viewableItems }) => {
// 这个方法为了让state对应当前呈现在页面上的item的播放器的state
// 也就是只会有一个播放器播放,而不会每个item都播放
// 可以理解为,只要不是当前再页面上的item 它的状态就应该暂停
// 只有100%呈现再页面上的item(只会有一个)它的播放器是播放状态
if (viewableItems.length === 1) {
setCurrentItem(viewableItems[0].index);
}
}, []);
useEffect(() => {
const mockData = [];
for (let i = 0; i < 100; i++) {
mockData.push({ id: i, pause: false });
}
setData(mockData);
}, []);
return (
<View style={{ flex: 1 }}>
<StatusBar
backgroundColor="transparent"
translucent
/>
<FlatList<ItemData>
onMoveShouldSetResponder={() => true}
data={data}
renderItem={({ item, index }) => (
<ShortVideoItem
paused={index !== currentItem}
id={item.id}
/>
)}
pagingEnabled={true}
getItemLayout={(item, index) => {
return { length: HEIGHT, offset: HEIGHT * index, index };
}}
onViewableItemsChanged={_onViewableItemsChanged}
keyExtractor={(item, index) => index.toString()}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 80, // item滑动80%部分才会到下一个
}}
/>
</View>
);
};
点赞效果
单次点击的时候切换暂停/播放状态,连续多次点击每次在点击位置出现一个爱心,随机旋转一个角度,爱心先放大再变透明消失。
爱心动画实现
const AnimatedHeartView = React.memo(
(props: AnimatedHeartProps) => {
// [-25, 25]随机一个角度
const rotateAngle = `${Math.round(Math.random() * 50 - 25)}deg`;
const animValue = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.sequence([
Animated.spring(animValue, {
toValue: 1,
useNativeDriver: true,
bounciness: 5,
}),
Animated.timing(animValue, {
toValue: 2,
useNativeDriver: true,
}),
]).start(() => {
props.onAnimFinished();
});
}, [animValue, props]);
return (
<Animated.Image
style={{
position: 'absolute',
width: 108,
height: 126,
top: props.y - 100,
left: props.x - 54,
opacity: animValue.interpolate({
inputRange: [0, 1, 2],
outputRange: [1, 1, 0],
}),
transform: [
{
scale: animValue.interpolate({
inputRange: [0, 1, 2],
outputRange: [1.5, 1.0, 2],
}),
},
{
rotate: rotateAngle,
},
],
}}
source={require('./img/heart.webp')}
/>
);
},
() => true,
);
连续点赞判定
监听手势,记录每次点击时间 lastClickTime,设置 CLICK_THRESHOLD 连续两次点击事件间隔小于 CLICK_THRESHOLD 视为连续点击,在点击位置创建爱心,添加到 heartList,否则视为单次点击,暂停播放。
const ShortVideoItem = React.memo((props: ShortVideoItemProps) => {
const [paused, setPaused] = React.useState(props.paused);
const [data, setData] = React.useState<VideoData>();
const [heartList, setHeartList] = React.useState<HeartData[]>([]);
const lastClickTime = React.useRef(0); // 记录上次点击时间
const pauseHandler = React.useRef<number>();
useEffect(() => {
setTimeout(() => {
setData({
video: TEST_VIDEO,
hasFavor: false,
});
});
}, []);
useEffect(() => {
setPaused(props.paused);
}, [props.paused]);
const _addHeartView = React.useCallback(heartViewData => {
setHeartList(list => [...list, heartViewData]);
}, []);
const _removeHeartView = React.useCallback(index => {
setHeartList(list => list.filter((item, i) => index !== i));
}, []);
const _favor = React.useCallback(
(hasFavor, canCancelFavor = true) => {
if (!hasFavor || canCancelFavor) {
setData(preValue => (preValue ? { ...preValue, hasFavor: !hasFavor } : preValue));
}
}, [],
);
const _handlerClick = React.useCallback(
(event: GestureResponderEvent) => {
const { pageX, pageY } = event.nativeEvent;
const heartViewData = {
x: pageX,
y: pageY - 60,
key: new Date().getTime().toString(),
};
const currentTime = new Date().getTime();
// 连续点击
if (currentTime - lastClickTime.current < CLICK_THRESHOLD) {
pauseHandler.current && clearTimeout(pauseHandler.current);
_addHeartView(heartViewData);
if (data && !data.hasFavor) {
_favor(false, false);
}
} else {
pauseHandler.current = setTimeout(() => {
setPaused(preValue => !preValue);
}, CLICK_THRESHOLD);
}
lastClickTime.current = currentTime;
}, [_addHeartView, _favor, data],
);
return <View
onStartShouldSetResponder={() => true}
onResponderGrant={_handlerClick}
style={{ height: HEIGHT }}
>
{
data
? <Video source={{ uri: data?.video }}
style={styles.backgroundVideo}
paused={paused}
resizeMode={'contain'}
repeat
/>
: null
}
{
heartList.map(({ x, y, key }, index) => {
return (
<AnimatedHeartView
x={x}
y={y}
key={key}
onAnimFinished={() => _removeHeartView(index)}
/>
);
})
}
<View style={{ justifyContent: 'flex-end', paddingHorizontal: 22, flex: 1 }}>
<View style={{
backgroundColor: '#000',
opacity: 0.8,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
marginRight: 'auto',
paddingHorizontal: 8,
}}>
<Text
style={{ fontSize: 14, color: '#FFF' }}
>
短视频招募了
</Text>
</View>
<View
style={{ height: 1, marginTop: 12, backgroundColor: '#FFF' }}
/>
<Text
style={{
marginTop: 12,
color: '#FFF',
fontSize: 16,
fontWeight: 'bold',
}}
numberOfLines={1}
>
5㎡长条形卫生间如何设计干湿分离?
</Text>
<Text
style={{
marginTop: 8,
color: '#FFF',
opacity: 0.6,
fontSize: 12,
}}
numberOfLines={2}
>
家里只有一个卫生间,一定要这样装!颜值比五星酒店卫生间还高级,卫生间,一定要这样装!颜值比卫生间,一定要这样装!
</Text>
<View style={{
flexDirection: 'row',
marginTop: 18,
marginBottom: 20,
alignItems: 'center',
}}>
<View
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: '#FFF' }}
/>
<Text style={{ color: '#FFF', fontSize: 14, marginLeft: 4 }}>
造作设计工作坊
</Text>
</View>
</View>
<View style={{
position: 'absolute',
right: 20,
bottom: 165,
}}>
<Image
style={styles.icon}
source={data?.hasFavor ? require('./img/love-f.png') : require('./img/love.png')}
/>
<Text style={styles.countNumber}>1.2w</Text>
<Image
style={styles.icon}
source={require('./img/collect.png')}
/>
<Text style={styles.countNumber}>1.2w</Text>
<Image
style={styles.icon}
source={require('./img/comment.png')}
/>
<Text style={styles.countNumber}>1.2w</Text>
</View>
{
paused
? <Image
style={{
position: 'absolute',
top: '50%',
left: '50%',
width: 40,
height: 40,
marginLeft: -20,
marginTop: -20,
}}
source={require('./img/play.webp')}
/>
: null
}
</View>;
}, (preValue, nextValue) => preValue.id === nextValue.id && preValue.paused === nextValue.paused);
手势冲突
通过 GestureResponder 拦截点击事件之后会造成 FlatList 滚动事件失效,所以需要将滚动事件交给 FlatList。通过 onResponderTerminationRequest 属性可以让 View 放弃处理事件的权利,将滚动事件交给 FlatList来处理。
<View
onStartShouldSetResponder={() => true}
onResponderTerminationRequest={() => true} <---- here
onResponderGrant={_handlerClick}>
{/* some code */}
</View>
代码
参考文献
https://blog.csdn.net/qq_38356174/article/details/96439456
https://juejin.im/post/5b504823e51d451953125799