前言
最近的有个需求是要实现一个下拉面板 dropdown
效果,但技术栈是 react-native
, 去网上搜了一圈发现并没有相关的开源组件,于是只有自己实现,于是参考了下 vant
的实现效果,一个简单的demo
如下:
分析
要实现下拉面板的效果核心元素就三个,一个触发器 trigger
, 一个白色面板pannel
, 一个遮罩mask
还有一个重要的点是:下拉面板的位置并不是视窗顶部,而是在触发器的位置。所以我们需要计算tigger
的 top
值,然后将下拉面板的top
值设置为 top
,bottom
设置为0
,并且设置overflow: hidden
就可以了,整体思路是一样的,只是实现的代码不一样,比如计算top
, 和动画这些。
react-native
的动画和 html
的动画实现方式不一样,相对复杂点。
实现
最终代码如下:
js
import styled from '@emotion/native';
import React, {memo} from 'react';
import {useRef, useState} from 'react';
import {
TouchableOpacity,
TouchableWithoutFeedback,
Animated,
Easing,
} from 'react-native';
const Wrapper = styled.View`
height: 36px;
width: 100%;
border-bottom-width: 0.5px;
border-bottom-style: solid;
border-bottom-color: ${({theme}) => theme.colorV2.cover8};
position: relative;
background-color: ${({theme}) => theme.colorV2.background};
flex-direction: row;
align-items: center;
`;
const Trigger = styled.View`
align-items: center;
justify-content: center;
padding: 0 12px;
flex-direction: row;
&:not(:last-of-type) {
border-right-width: 0.5px;
border-right-style: solid;
border-right-color: ${({theme}) => theme.colorV2.cover8};
}
`;
const TextWrapper = styled.Text`
font-size: 12px;
color: ${props =>
props.active ? props.theme.colorV2.primary : props.theme.colorV2.text40};
font-weight: 500;
`;
const DropdownWrapper = styled.View`
position: absolute;
bottom: 0;
width: 100%;
z-index: 1000;
overflow: hidden;
`;
const Mask = styled(Animated.View)`
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
width: 100%;
height: 100%;
`;
const DropdownContianer = styled(Animated.View)`
width: 100%;
min-height: 100px;
background: ${props => props.theme.colorV2.background};
border-radius: 0 0 8px 8px;
`;
const VerticalItem = styled.View`
height: 48px;
padding: 10px 16px;
justify-content: center;
`;
const VerticalItemText = styled(Animated.Text)`
font-size: 14px;
font-weight: ${props => (props.active ? 500 : 400)};
color: ${props =>
props.active ? props.theme.colorV2.primary : props.theme.colorV2.text};
`;
const data = [
{
title: '产品',
field: 'coin',
options: [
{label: 'ALL', value: 'ALL'},
{label: '牛肉', value: 'BTC'},
{label: '猪肉', value: 'ETH'},
{label: '鸡肉', value: 'ETCCH'},
],
},
{
title: '方向',
field: 'side',
options: [
{label: 'ALL', value: 'ALL'},
{label: '买入', value: 'BUY'},
{label: '卖出', value: 'SELL'},
],
},
{
title: '时间',
field: 'time',
options: [
{label: 'ALL', value: 'ALL'},
{label: '1周', value: '1week'},
{label: '3周', value: '3week'},
{label: '1月', value: '1moth'},
{label: '3月', value: '3moth'},
],
},
];
const DURATION = 200;
/**
* Dropdown
*/
const Dropdown = memo(props => {
const {...restProps} = props;
const [visible, setVisible] = useState(false);
const [activeDropItem, setActiveDropItem] = useState({});
const [values, setValues] = useState({});
const layoutRef = useRef({});
const containerLayoutRef = useRef({});
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(-100)).current;
const handleLayout = e => {
layoutRef.current = e.nativeEvent.layout;
};
const handleContainerLayout = e => {
containerLayoutRef.current = e.nativeEvent.layout;
};
const {height, y} = layoutRef.current;
const wrapperTop = height + y;
const handlePress = (item, i) => {
setVisible(true);
setActiveDropItem(item);
Animated.timing(fadeAnim, {
toValue: 1,
duration: DURATION,
easing: Easing.linear,
useNativeDriver: false,
}).start();
Animated.timing(slideAnim, {
toValue: 0,
duration: DURATION,
easing: Easing.linear,
useNativeDriver: false,
}).start();
};
const handleMaskPress = () => {
Animated.timing(fadeAnim, {
toValue: 0,
duration: DURATION,
easing: Easing.linear,
useNativeDriver: false,
}).start();
Animated.timing(slideAnim, {
toValue: -containerLayoutRef.current.height,
duration: DURATION,
easing: Easing.linear,
useNativeDriver: false,
}).start(() => setVisible(false));
};
const handleOptionPress = (item, _activeDropItem, i) => {
const {field} = _activeDropItem;
setValues(prev => ({...prev, [field]: item}));
};
return (
<>
<Wrapper {...restProps} onLayout={handleLayout}>
{data.map((item, i) => {
return (
<TouchableOpacity
key={i}
activeOpacity={1}
onPress={() => handlePress(item, i)}>
<Trigger>
<TextWrapper active={values[item.field]?.label}>
{values[item.field]?.label || item.title}
</TextWrapper>
</Trigger>
</TouchableOpacity>
);
})}
</Wrapper>
{visible && (
<DropdownWrapper
style={{
top: wrapperTop,
}}>
<TouchableWithoutFeedback onPress={handleMaskPress}>
<Mask
style={{
opacity: fadeAnim,
}}
/>
</TouchableWithoutFeedback>
<DropdownContianer
onLayout={handleContainerLayout}
style={{
transform: [{translateY: slideAnim}],
}}>
{activeDropItem.options.map((item, i) => (
<TouchableOpacity
key={i}
activeOpacity={0.8}
onPress={() => handleOptionPress(item, activeDropItem, i)}>
<VerticalItem key={item.value}>
<VerticalItemText
active={item.value === values[activeDropItem.field]?.value}>
{item.label}
</VerticalItemText>
</VerticalItem>
</TouchableOpacity>
))}
</DropdownContianer>
</DropdownWrapper>
)}
</>
);
});
export default Dropdown;
总结
一个简单的 reacti-native-dropdown
的下拉面板就实现了。 因为我的需求是这样所以定的上面的数据格式。并且还有很多的细节没有去优化。这个实现只是一个简单的思路,更复杂的可以基于这个思路做扩展