likes
comments
collection
share

react-native-dropdown

作者站长头像
站长
· 阅读数 4

前言

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

react-native-dropdown

分析

要实现下拉面板的效果核心元素就三个,一个触发器 trigger, 一个白色面板pannel, 一个遮罩mask 还有一个重要的点是:下拉面板的位置并不是视窗顶部,而是在触发器的位置。所以我们需要计算tiggertop值,然后将下拉面板的top值设置为 topbottom设置为0,并且设置overflow: hidden 就可以了,整体思路是一样的,只是实现的代码不一样,比如计算top, 和动画这些。

react-native的动画和 html 的动画实现方式不一样,相对复杂点。

实现

最终代码如下:

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

转载自:https://juejin.cn/post/7331717626060636223
评论
请登录