配图来自法国画家Martin Jarrie
一.需求背景
使用手势上滑、下拉,实现类似抽屉的弹出、收回效果。
二.设计思路
2.1 手势
一个基于手势的动画效果,必然要用到React Native官方组件PanResponder,并且使用onPanResponderRelease监听手势在y轴的滑动方向,根据不同的滑动方向控制列表向上拉出或者向下收回。
2.2 动画
既然涉及弹出、收回动画,那必然要用到react-native-reanimated,我所用到的interface如下:
import Animated, {
useAnimatedStyle,
withTiming,
Easing,
useSharedValue
} from "react-native-reanimated";
react-native-reanimated目前可以为<View /> <Text /> <Image /> <ScrollView /> <FlatList />5种组件添加动画效果,例如,你想为<View />添加动画效果,你需要将React Native的官方组件<View />替换成<Animated.View />
其中
useAnimatedStyle用来定义style,可以提供平移、旋转等动画。
useSharedValue用来定义一个当前实例内全局可用的单例值,例如y轴位移值。
Easing用于定义动画运行效果,例如线性、弹跳等
export declare const Easing: {
linear: typeof linear;
ease: typeof ease;
quad: typeof quad;
cubic: typeof cubic;
poly: typeof poly;
sin: typeof sin;
circle: typeof circle;
exp: typeof exp;
elastic: typeof elastic;
back: typeof back;
bounce: typeof bounce;
bezier: typeof bezier;
bezierFn: typeof bezierFn;
steps: typeof steps;
in: typeof in_;
out: typeof out;
inOut: typeof inOut;
};
withTiming让你创建一个能够设置动画周期和运行效果的动画,该方法包含三个参数,toValue用来传入动画运行的位移,config用于传入动画效果配置,callback为你需要处理的回调
/**
* Lets you create an animation based on duration and easing.
*
* @param toValue - The value on which the animation will come at rest - {@link AnimatableValue}.
* @param config - The timing animation configuration - {@link TimingConfig}.
* @param callback - A function called on animation complete - {@link AnimationCallback}.
* @returns An [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withTiming
*/
export declare const withTiming: withTimingType;
2.3 避免手势冲突
因为我当前需要添加上滑、下滑手势的组件中已经包含了一个具有滑动手势的FlashList,因此为了避免冲突,我选择在图中红色部分添加手势,去控制绿色部分的FlashList的父View整体动画位移,这样可以避免添加的手势和FlashList冲突。
三.核心代码
const bottomViewHeight = 100
const bottomViewAtTopPosition = 170
const bottomTanslateY = windowHeight - bottomViewHeight - bottomViewAtTopPosition
const y = useSharedValue(0);
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (e, gestureState) => {
return Math.abs(gestureState.dy) > Math.abs(gestureState.dx);
},
onPanResponderMove: (e, gestureState) => {
},
onPanResponderRelease: (e, gestureState) => {
//向下滑动
if (gestureState.dy > 0) {
//bottomView已经位于页面下方了,不位移,反之才向下位移
if(bottomViewAtTop == false) {
y.value = withTiming(0, { easing: Easing.linear });
} else {
y.value = withTiming(bottomTanslateY, { easing: Easing.linear });
}
setBottomViewAtTop(false)
} else {
//向上滑动
if(bottomViewAtTop == true) {
//bottomView已经位于页面上方了,不位移
y.value = withTiming(0, { easing: Easing.linear });
} else {
y.value = withTiming(-bottomTanslateY, { easing: Easing.linear });
}
setBottomViewAtTop(true)
}
},
})
).current;
const animatedJobListStyle = useAnimatedStyle(() => ({
transform: [
{
translateY: withTiming(y.value, {
duration: 50,
easing: Easing.linear,
}),
},
],
}));
return (
<Animated.View style={[styles.jobListView, animatedJobListStyle]}>
<Animated.View {...panResponder.panHandlers}>
<View style={styles.filterRoot}>
<Text style={[textStyles.semibold18]}>
职位
</Text>
<View style={styles.shortLineView}>
<Image style={styles.shortLine} source={require('../../../assets/images/ShortLine.png')}></Image>
</View>
<Image style={styles.search} source={require('../../../assets/images/search_big.png')}></Image>
</View>
<View style={styles.filterBack}>
<Pressable style={styles.filterButton} onPress={() => { }}>
<Text style={styles.filterText}>职位</Text>
<Image style={styles.downArrow} source={downArrowIcon}></Image>
</Pressable>
<Pressable style={styles.filterButton} onPress={() => { }}>
<Text style={styles.filterText}>经验</Text>
<Image style={styles.downArrow} source={downArrowIcon}></Image>
</Pressable>
<Pressable style={styles.filterButton} onPress={() => { }}>
<Text style={styles.filterText}>城市</Text>
<Image style={styles.downArrow} source={downArrowIcon}></Image>
</Pressable>
<Pressable style={styles.filterButton} onPress={() => { }}>
<Text style={styles.filterText}>薪资</Text>
<Image style={styles.downArrow} source={downArrowIcon}></Image>
</Pressable>
</View>
</Animated.View>
<View style={{ width: windowWidth, height: bottomTanslateY+30 }}>
<FlashList
showsVerticalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
onEndReachedThreshold={0.3}
onEndReached={onLoadMore}
keyExtractor={item => `${item.id}`}
data={jobs}
renderItem={renderItem}
contentContainerStyle={{ backgroundColor: 'rgb(243,245,250)' }}
ListFooterComponent={renderFooter}
extraData={jobData}
estimatedItemSize={30}
nestedScrollEnabled
/>
</View>
</Animated.View>
)
四.实现效果
最后,看看最终的实现效果