react-native-dropdown

前言

最近的有个需求是要实现一个下拉面板 dropdown 效果,但技术栈是 react-native, 去网上搜了一圈发现并没有相关的开源组件,于是只有自己实现,于是参考了下 vant 的实现效果,一个简单的demo如下:

分析

要实现下拉面板的效果核心元素就三个,一个触发器 trigger, 一个白色面板pannel, 一个遮罩mask 还有一个重要的点是:下拉面板的位置并不是视窗顶部,而是在触发器的位置。所以我们需要计算tiggertop值,然后将下拉面板的top值设置为 topbottom设置为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 的下拉面板就实现了。 因为我的需求是这样所以定的上面的数据格式。并且还有很多的细节没有去优化。这个实现只是一个简单的思路,更复杂的可以基于这个思路做扩展

相关推荐
MiyueFE4 分钟前
14 个逻辑驱动的 UI 设计技巧,助您改善任何界面
前端·设计
啃火龙果的兔子8 分钟前
前端单元测试覆盖率工具有哪些,分别有什么优缺点
前端·单元测试
「、皓子~36 分钟前
后台管理系统的诞生 - 利用AI 1天完成整个后台管理系统的微服务后端+前端
前端·人工智能·微服务·小程序·go·ai编程·ai写作
就改了38 分钟前
Ajax——在OA系统提升性能的局部刷新
前端·javascript·ajax
凌冰_40 分钟前
Ajax 入门
前端·javascript·ajax
京东零售技术1 小时前
京东小程序JS API仓颉改造实践
前端
老A技术联盟1 小时前
从小白入门,基于Cursor开发一个前端小程序之Cursor 编程实践与案例分析
前端·小程序
风铃喵游1 小时前
构建引擎: 打造小程序编译器
前端·小程序·架构
sunbyte1 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ThemeClock(主题时钟)
前端·javascript·css·vue.js·前端框架·tailwindcss
小飞悟1 小时前
🎯 什么是模块化?CommonJS 和 ES6 Modules 到底有什么区别?小白也能看懂
前端·javascript·设计