實(shí)現(xiàn)效果
需要實(shí)現(xiàn)的效果主要有兩個(gè),一個(gè)是上下翻頁效果,還有就是點(diǎn)贊的動(dòng)畫。
效果 (簡(jiǎn)書好像不支持 webp 格式圖片 只能將就一下)
列表頁上下翻頁實(shí)現(xiàn)
播放視頻使用 react-native-video 庫。列表使用 FlatList,每個(gè) item 占用一整屏,配合 pagingEnabled 屬性可以翻頁效果。
通過 onViewableItemsChanged 來控制只有當(dāng)前的頁面才播放視頻。
const ShortVideoPage = () => {
const [currentItem, setCurrentItem] = useState(0);
const [data, setData] = useState<ItemData[]>([]);
const _onViewableItemsChanged = useCallback(({ viewableItems }) => {
// 這個(gè)方法為了讓state對(duì)應(yīng)當(dāng)前呈現(xiàn)在頁面上的item的播放器的state
// 也就是只會(huì)有一個(gè)播放器播放,而不會(huì)每個(gè)item都播放
// 可以理解為,只要不是當(dāng)前再頁面上的item 它的狀態(tài)就應(yīng)該暫停
// 只有100%呈現(xiàn)再頁面上的item(只會(huì)有一個(gè))它的播放器是播放狀態(tài)
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滑動(dòng)80%部分才會(huì)到下一個(gè)
}}
/>
</View>
);
};
點(diǎn)贊效果
單次點(diǎn)擊的時(shí)候切換暫停/播放狀態(tài),連續(xù)多次點(diǎn)擊每次在點(diǎn)擊位置出現(xiàn)一個(gè)愛心,隨機(jī)旋轉(zhuǎn)一個(gè)角度,愛心先放大再變透明消失。
愛心動(dòng)畫實(shí)現(xiàn)
const AnimatedHeartView = React.memo(
(props: AnimatedHeartProps) => {
// [-25, 25]隨機(jī)一個(gè)角度
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,
);
連續(xù)點(diǎn)贊判定
監(jiān)聽手勢(shì),記錄每次點(diǎn)擊時(shí)間 lastClickTime,設(shè)置 CLICK_THRESHOLD 連續(xù)兩次點(diǎn)擊事件間隔小于 CLICK_THRESHOLD 視為連續(xù)點(diǎn)擊,在點(diǎn)擊位置創(chuàng)建愛心,添加到 heartList,否則視為單次點(diǎn)擊,暫停播放。
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); // 記錄上次點(diǎn)擊時(shí)間
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();
// 連續(xù)點(diǎn)擊
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㎡長(zhǎng)條形衛(wèi)生間如何設(shè)計(jì)干濕分離?
</Text>
<Text
style={{
marginTop: 8,
color: '#FFF',
opacity: 0.6,
fontSize: 12,
}}
numberOfLines={2}
>
家里只有一個(gè)衛(wèi)生間,一定要這樣裝!顏值比五星酒店衛(wèi)生間還高級(jí),衛(wèi)生間,一定要這樣裝!顏值比衛(wèi)生間,一定要這樣裝!
</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 }}>
造作設(shè)計(jì)工作坊
</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);
手勢(shì)沖突
通過 GestureResponder 攔截點(diǎn)擊事件之后會(huì)造成 FlatList 滾動(dòng)事件失效,所以需要將滾動(dòng)事件交給 FlatList。通過 onResponderTerminationRequest 屬性可以讓 View 放棄處理事件的權(quán)利,將滾動(dòng)事件交給 FlatList來處理。
<View
onStartShouldSetResponder={() => true}
onResponderTerminationRequest={() => true} <---- here
onResponderGrant={_handlerClick}>
{/* some code */}
</View>
代碼
參考文獻(xiàn)
https://blog.csdn.net/qq_38356174/article/details/96439456
https://juejin.im/post/5b504823e51d451953125799