實現效果
需要實現的效果主要有兩個,一個是上下翻頁效果,還有就是點讚的動畫。
列表頁上下翻頁實現
播放視頻使用 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。
<FlatList<ItemData>
onMoveShouldSetResponder={() => true}
/>
代碼
參考文獻
https://blog.csdn.net/qq_38356174/article/details/96439456
https://juejin.im/post/5b504823e51d451953125799