react-native-dropdown

前言

最近的有个需求是要实现一个下拉面板 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 的下拉面板就实现了。 因为我的需求是这样所以定的上面的数据格式。并且还有很多的细节没有去优化。这个实现只是一个简单的思路,更复杂的可以基于这个思路做扩展

相关推荐
deli0071 小时前
手机20%突然关机?电量百分比是估的,SOC曲线4组实测
前端
深蓝AI12 小时前
Meta 开源用了八年的设计系统 astryx:连坐两天 GitHub 总榜第一,组件库开始写给 agent 的说明书
前端·人工智能
IT_陈寒13 小时前
Vue的watch把我坑惨了,原来要这样用才靠谱
前端·人工智能·后端
1-1=013 小时前
Vue 3 模板语
前端·javascript·vue.js
用户76103044737113 小时前
【转战智能眼镜开发】ROKID AIUI 小游戏 亚马逊标枪项目实战讲解
前端
风卿13 小时前
看着AI写代码太孤独了,来欣赏星云吧:一个DSH 装饰插件
前端·javascript
牧艺13 小时前
弹层不用再手算坐标了:CSS Anchor Positioning 从原理到可上线
前端·javascript·css
用户老李13 小时前
YOLO 目标检测自学教程 07:YOLOv3——多尺度预测
前端
moshuying14 小时前
ESP32 墨水屏金价牌(上):从点亮到把图片推过去
前端
李剑一14 小时前
别再抬杠,前端岗已经没了!无论前后端,我们最终都要成“全干”工程师
前端·面试