react-native 封装城市选择组件(二)

326 阅读4分钟

今天我进一步进行完善,主要是把暴露的接口写完了,我还会进一步完善,如果在项目中跑起来没什么问题的话,我就会上传到 npm ,以供大家学习和使用,当然如果能封装更好的,大家可以一块学习。

一、props

下面看 props

  1. data 这个参数必传。跟 SectionList 相似,只不过我的数组属性是 items , 而 SectionList 的是 data
[
	{
    	...
        items: []
    }
]
  1. selectedIndexes 初始化已选中的下标,如:
// 这里面的元素个数取决于你的总列数
[0, 0, 0] // 代表选中第一列的第一个,第二列的第一个,第三列的第一个
[2, 1, 0] // 代表选中第一列的第三个,第二列的第二个,第三个的第一个
// 加入说你传入的是 [1] 那么就会选中第一列的第二个,第二列的第二个,第三列的第二个,也就相当于 [1, 1, 1]
// 如果传入的是 [1, 2] 那么就会选中第一列的第二个,第二列的第三个,第三列的第三个,也就相当于 [1, 2, 2]
// 如果不传,就默认: [0, 0, 0]
  1. renderItems, renderItem 这个参数必传。这个就是 FlatListrenderItem ,这两个的优先级 renderItems > renderItem
// 如果传入的 renderItem 而没有传入 renderItems ,
// 那么所有列都使用相同的 renderItem ,否则按照 renderItems 数组里面的渲染各自的
// 先假设总的列数是 n
// 渲染规则跟上面的 selectedIndexes 相同
// 比如 [()=>null] ,相当于 [()=>,()=>null,()=>null]
// 比如 [()=>null,()=><Text>test</Text>] ,相当于 [()=>null,()=><Text>test</Text>, ()=><Text>test</Text>]
// 值得说的是回调的参数
// ({item, index, isSelected}) => null
// 其中 item 代表每一列的每一项数据
// index 代表下标
// isSelected 代表当前行是否被选中
  1. n 这个就是代表总列数,这个参数必传
  2. style 最外面容器的样式。
  3. widthRadio 这个是每一列的宽度占比。
// 比如: ['30%', '30%', '30%'],那么就代表三列的宽度都是屏幕宽度的一半
// 比如: [240, '40%'],那么代表第一列 240 ,第二列占屏幕的宽度百分之四十

// 如果有缺省值,也就是传入 n > 数组的长度,比如 n 我传入的是 5 ,而数组 ['30%', '30%', '30%'] ,
// 那么相当于 ['30%', '30%', '30%', '30%', '30%'] 这样的话有些列就不会在屏幕上显示,只需要滑动就能看到
  1. flatListContainerStyle 这个代表所有列的容器的样式。
  2. ListHeaderComponent 跟 FlatList 相似,只不过不同的是这个不会随着列表的上下滑动而滑动。
  3. ListFooterComponent 同上。
  4. styles 这个代表每一个 FlatListstyle ,目前的功能是一一对应的,以后也会像上面的 selectedIndexes 一样。
  5. moreFlatListProps 这个是 FlatListprops ,也是一一对应的,同样以后会实现像上面的效果。
  6. scrollViewProps 这个就是 ScrollViewprops
  7. itemPress 这个就是每一项的点击事件。
// itemPress 回调的参数就是每一列的数据,除 items 外的数据,像下面实现:
const itemPress = (item, i) => {}
// 这里面的 i 是代表第几列,第一列是 0 以此类推。

二、效果图

三、示例代码

const DefaultPicker = (props) => {
  const renderItemTitle = useCallback(({item, index, isSelected}) => {
    const style = {
      borderLeftWidth: 4,
      borderLeftColor: isSelected ? colors.primary : '#fff',
      backgroundColor: isSelected ? '#F8F8F8' : '#fff',
    };
    return (
      <View style={style}>
        <Text key={index} style={styles.itemTitleStyle}>
          {item?.name || ''}
        </Text>
      </View>
    );
  }, []);
  const renderItemOther = useCallback(({item, index, isSelected}) => {
    const style = {
      color: isSelected ? colors.primary : '#505050',
    };
    return (
      <Text key={index} style={[styles.itemOtherStyle, style]}>
        {item?.name || ''}
      </Text>
    );
  }, []);
  return (
    <View style={styles.fullscreen}>
      <View style={styles.fullscreen} />
      <Picker
        {...props}
        style={styles.pickerStyle}
        styles={[styles.f1BgColor, styles.f23BgColor, styles.f23BgColor]}
        ListHeaderComponent={() => ListHeaderComponent(props.disappear)}
        renderItems={[renderItemTitle, renderItemOther]}
        n={3}
        moreFlatListProps={[
          {keyExtractor: (item, index) => item + index},
          {keyExtractor: (item, index) => item + index},
          {keyExtractor: (item, index) => item + index},
        ]}
      />
    </View>
  );
};

四、源码

import React, {useCallback, useState} from 'react';
import {
  ScrollView,
  View,
  StyleSheet,
  FlatList,
  Dimensions,
  Pressable,
} from 'react-native';

const selectData = (index, indexes, data) => {
  let tempData = data;
  for (let i = 0; i < index; i++) {
    tempData = tempData?.[indexes[i]]?.items || [];
  }
  return tempData;
};

/**
 * 初始化已经选择的数组
 * @param {Array<number>} selectedIndexes 传入的已经选择的下标
 * @param {number} n 列的个数
 */
const initSelectedIndexes = (selectedIndexes = [], n) => {
  const tempIndexes = [...selectedIndexes];
  for (let i = selectedIndexes?.length || 0; i < n; i++) {
    tempIndexes.push(0);
  }
  return tempIndexes;
};

/**
 * 得到模拟数组
 * @param {number} n 所有数据
 */
const getSimulateArray = (n) => {
  return new Array(n).fill(1);
};

/**
 *
 * @param {Array<number|string>} widthRadio 各宽度比例
 */
const getWidths = (widthRadio, n) => {
  const width = Dimensions.get('screen').width;
  const len = widthRadio?.length || 0;
  const tempWidths = [];
  for (let i = 0; i < len; i++) {
    if (typeof widthRadio[i] === 'string') {
      const radio = parseFloat(widthRadio[i]) / 100;
      tempWidths.push(width * radio);
    } else {
      tempWidths.push(widthRadio[i]);
    }
  }

  // 如果还有没有添加到数组中的就默认使用最后一个宽度,如果前面都没有值
  // ,那么就定义宽度为总宽度的 1/3
  const equalWidth =
    tempWidths[tempWidths.length - 1] || parseFloat((width / 3).toFixed(2));
  for (let j = len; j < n; j++) {
    tempWidths.push(equalWidth);
  }
  return tempWidths;
};

const getComponent = (ele = null) => {
  let temp;
  if (typeof ele === 'function') {
    temp = ele;
  } else {
    temp = () => ele;
  }
  return temp;
};

const getRenderItem = (renderItems = [], n) => {
  if (!renderItems) {
    throw new Error('您至少传入一个 renderItem');
  }
  const tempRenderItems = [];
  if (Array.isArray(renderItems)) {
    tempRenderItems.push(...renderItems);
  } else {
    tempRenderItems.push(renderItems);
  }
  const len = tempRenderItems.length;
  const sameRenderItem = tempRenderItems[len - 1];
  for (let i = len; i < n; i++) {
    tempRenderItems.push(sameRenderItem);
  }
  return tempRenderItems;
};

/**
 * @typedef {{
 *  data: Array,
 *  selectedIndexes?: Array<number>,
 *  renderItems: Array<({item: any, index: number, isSelected: boolean}) => React.ReactElement>,
 *  renderItem: {item: any, index: number}) => React.ReactElement,
 *  styles?: Array<import('react-native').ViewStyle>,
 *  style?: import('react-native').ViewStyle,
 *  flatListContainerStyle?: import('react-native').ViewStyle,
 *  widthRadio?: Array<number|string>,
 *  ListHeaderComponent?: () => React.ReactElement | import('react').ReactElement | null,
 *  ListFooterComponent?: () => React.ReactElement | import('react').ReactElement | null,
 *  moreFlatListProps?: Array<import('react-native').FlatListProps>,
 *  scrollViewProps?: import('react-native').ScrollViewProps,
 *  n: number,
 *  itemPress?: (item: any, n: number) => void
 * }} PickerType
 * @param {PickerType} param0
 */
const Picker = ({
  data,
  selectedIndexes,
  renderItems,
  renderItem,
  style,
  flatListContainerStyle,
  widthRadio,
  ListHeaderComponent,
  ListFooterComponent,
  styles,
  moreFlatListProps,
  scrollViewProps,
  n,
  itemPress,
}) => {
  const [indexes, setIndexes] = useState(
    initSelectedIndexes(selectedIndexes, n),
  );

  const onPress = useCallback((i, index) => {
    setIndexes((oldIndexes) => {
      oldIndexes[i] = index;
      for (let j = i + 1; j < 3; j++) {
        oldIndexes[j] = 0;
      }
      return [...oldIndexes];
    });
  }, []);
  const toRenderItem = useCallback(
    ({item, index}, i) => {
      const setItem = {...item};
      delete setItem.items;
      return (
        <Pressable
          onPress={() => {
            onPress(i, index);
            itemPress?.(setItem, i);
          }}>
          {getRenderItem(renderItems || renderItem, n)[i]({
            item,
            index,
            isSelected: indexes[i] === index,
          })}
        </Pressable>
      );
    },
    [renderItems, renderItem, n, indexes, onPress, itemPress],
  );
  return (
    <View style={[pickerStyles.container, style]}>
      {getComponent(ListHeaderComponent)()}
      <ScrollView {...scrollViewProps} horizontal={true}>
        <View style={[pickerStyles.listContainer, flatListContainerStyle]}>
          {getSimulateArray(n).map((_, index) => {
            return (
              <FlatList
                {...(moreFlatListProps?.[index] || {})}
                style={[
                  pickerStyles.flatListStyle,
                  styles?.[index] || {},
                  {width: getWidths(widthRadio, n)[index]},
                ]}
                data={selectData(index, indexes, data)}
                renderItem={({item, index: i}) =>
                  toRenderItem({item, index: i}, index)
                }
              />
            );
          })}
        </View>
      </ScrollView>
      {getComponent(ListFooterComponent)()}
    </View>
  );
};

const pickerStyles = StyleSheet.create({
  container: {
    flex: 1,
  },
  listContainer: {
    flex: 1,
    flexDirection: 'row',
    backgroundColor: '#fff',
  },
  flatListStyle: {
    height: '100%',
  },
});

export default React.memo(Picker);

这篇主要是介绍 props 的和我实战的实例。以后还会完善,最终要达到上传到 npm 的目的。如果以后在使用过程中遇到性能的问题我也会进行优化。