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

相关推荐
m0_748251728 分钟前
DataOps驱动数据集成创新:Apache DolphinScheduler & SeaTunnel on Amazon Web Services
前端·apache
珊珊来吃9 分钟前
EXCEL中给某一列数据加上双引号
java·前端·excel
胡西风_foxww36 分钟前
【ES6复习笔记】Spread 扩展运算符(8)
前端·笔记·es6·扩展·运算符·spread
小林爱1 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
跨境商城搭建开发1 小时前
一个服务器可以搭建几个网站?搭建一个网站的流程介绍
运维·服务器·前端·vue.js·mysql·npm·php
hhzz1 小时前
vue前端项目中实现电子签名功能(附完整源码)
前端·javascript·vue.js
秋雨凉人心1 小时前
上传npm包加强
开发语言·前端·javascript·webpack·npm·node.js
时清云2 小时前
【算法】 课程表
前端·算法·面试
NoneCoder2 小时前
CSS系列(37)-- Overscroll Behavior详解
前端·css
Nejosi_念旧2 小时前
使用Webpack构建NPM Library
前端·webpack·npm