|
| 1 | +import React, { FC, useState, useRef } from 'react'; |
| 2 | +import { StyleSheet, View, TouchableOpacity, StyleProp, ViewStyle, LayoutAnimation, Animated } from 'react-native'; |
| 3 | +import { Theme } from '../theme'; |
| 4 | +import { useTheme } from '@shopify/restyle'; |
| 5 | +import Icon from '../Icon'; |
| 6 | + |
| 7 | +interface AccordionProps { |
| 8 | + /** 自定义手风琴列表*/ |
| 9 | + sections: { |
| 10 | + /** 列表标题内容*/ |
| 11 | + title: JSX.Element; |
| 12 | + /** 展开内容*/ |
| 13 | + content: JSX.Element; |
| 14 | + /** 是否可以点击,默认可以点击*/ |
| 15 | + isOnPress?: boolean; |
| 16 | + }[]; |
| 17 | + /** 是否展示多个,默认展示多个*/ |
| 18 | + isMultiple?: boolean; |
| 19 | + /** 手风琴每行列表样式 */ |
| 20 | + accordionStyle?: StyleProp<ViewStyle>; |
| 21 | + /** 点击展开内容样式 */ |
| 22 | + contentStyle?: StyleProp<ViewStyle>; |
| 23 | + /** 是否展示图标 */ |
| 24 | + iconShow?: boolean; |
| 25 | + /** 图标源 */ |
| 26 | + customIcon?: string | JSX.Element; |
| 27 | + /** 图标尺寸 */ |
| 28 | + iconSize?: number; |
| 29 | +} |
| 30 | + |
| 31 | +const Accordion: FC<AccordionProps> = (props) => { |
| 32 | + const { sections, isMultiple = true, iconShow = true, iconSize = 18, accordionStyle, contentStyle } = props; |
| 33 | + const [activeIndex, setActiveIndex] = useState<number[] | number>(isMultiple ? [] : -1); |
| 34 | + const theme = useTheme<Theme>(); |
| 35 | + const styles = createStyles({ |
| 36 | + bgColor: theme.colors.mask, |
| 37 | + headerColor: theme.colors.background, |
| 38 | + borderColor: theme.colors.border, |
| 39 | + }); |
| 40 | + const animatedController = useRef(new Animated.Value(0)).current; |
| 41 | + |
| 42 | + const onPress = (index: number | never) => { |
| 43 | + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); |
| 44 | + if (isMultiple) { |
| 45 | + const currentIndex = Array.isArray(activeIndex) ? activeIndex.indexOf(index) : -1; |
| 46 | + if (currentIndex > -1) { |
| 47 | + const newActiveIndex = Array.isArray(activeIndex) ? [...activeIndex] : []; |
| 48 | + if (currentIndex > -1) { |
| 49 | + newActiveIndex.splice(currentIndex, 1); |
| 50 | + } |
| 51 | + setActiveIndex(newActiveIndex); |
| 52 | + } else { |
| 53 | + setActiveIndex(Array.isArray(activeIndex) ? [...activeIndex, index] : [index]); |
| 54 | + } |
| 55 | + } else { |
| 56 | + setActiveIndex(activeIndex === index ? -1 : index); |
| 57 | + } |
| 58 | + Animated.timing(animatedController, { |
| 59 | + toValue: activeIndex === index ? 0 : 1, |
| 60 | + duration: 500, |
| 61 | + useNativeDriver: true, |
| 62 | + }).start(); |
| 63 | + }; |
| 64 | + |
| 65 | + const rotateZ = animatedController.interpolate({ |
| 66 | + inputRange: [0, 1], |
| 67 | + outputRange: ['0deg', '90deg'], |
| 68 | + }); |
| 69 | + |
| 70 | + return ( |
| 71 | + <View> |
| 72 | + {sections.map((item, index) => ( |
| 73 | + <View key={index}> |
| 74 | + <TouchableOpacity |
| 75 | + disabled={item?.isOnPress || false} |
| 76 | + activeOpacity={0.8} |
| 77 | + onPress={() => onPress(index)} |
| 78 | + style={[styles.header, accordionStyle]} |
| 79 | + > |
| 80 | + <View style={styles.titleBy} key={index}> |
| 81 | + {item.title} |
| 82 | + {iconShow && ( |
| 83 | + <Animated.View |
| 84 | + style={{ |
| 85 | + transform: [ |
| 86 | + { |
| 87 | + rotateZ: |
| 88 | + activeIndex === index || (Array.isArray(activeIndex) && activeIndex.indexOf(index) > -1) |
| 89 | + ? rotateZ |
| 90 | + : '0deg', |
| 91 | + }, |
| 92 | + ], |
| 93 | + }} |
| 94 | + > |
| 95 | + <Icon name="right" size={iconSize} color={theme.colors.border} /> |
| 96 | + </Animated.View> |
| 97 | + )} |
| 98 | + </View> |
| 99 | + </TouchableOpacity> |
| 100 | + {((isMultiple && Array.isArray(activeIndex) && activeIndex.indexOf(index) > -1) || |
| 101 | + (!isMultiple && activeIndex === index)) && ( |
| 102 | + <View style={[styles.content, contentStyle]}>{item.content}</View> |
| 103 | + )} |
| 104 | + </View> |
| 105 | + ))} |
| 106 | + </View> |
| 107 | + ); |
| 108 | +}; |
| 109 | + |
| 110 | +type CreStyle = { |
| 111 | + bgColor: string; |
| 112 | + headerColor: string; |
| 113 | + borderColor: string; |
| 114 | +}; |
| 115 | + |
| 116 | +function createStyles({ bgColor, borderColor, headerColor }: CreStyle) { |
| 117 | + return StyleSheet.create({ |
| 118 | + titleBy: { |
| 119 | + flexDirection: 'row', |
| 120 | + justifyContent: 'space-between', |
| 121 | + }, |
| 122 | + header: { |
| 123 | + borderBottomWidth: 1, |
| 124 | + borderBottomColor: borderColor, |
| 125 | + padding: 15, |
| 126 | + backgroundColor: headerColor, |
| 127 | + }, |
| 128 | + content: { |
| 129 | + padding: 15, |
| 130 | + backgroundColor: bgColor, |
| 131 | + }, |
| 132 | + }); |
| 133 | +} |
| 134 | + |
| 135 | +export default Accordion; |
0 commit comments