@uiw/react-native
Version:
UIW for React Native
207 lines (206 loc) • 6.18 kB
JavaScript
import React, { useState, useEffect } from 'react';
import invoke from 'lodash/invoke';
import { StyleSheet, Animated, Easing, LayoutAnimation, View, TouchableOpacity, Platform, Dimensions } from 'react-native';
import { colors } from '../utils';
const PEEP = 8;
const DURATION = 300;
const MARGIN_BOTTOM = 24;
const buttonStartValue = 0.8;
export default function CardCollapse(props) {
const {
isCollapsed,
children,
contentContainerStyle,
itemBorderRadius,
disablePresses,
containerStyle,
onCollapseChanged
} = props;
const getAnimatedScales = () => {
return React.Children.map(children, (_item, index) => {
return new Animated.Value(getItemScale(index));
});
};
const [collapsed, setCollapsed] = useState(isCollapsed);
const [firstItemHeight, setFirstItemHeight] = useState(undefined);
const animatedScale = new Animated.Value(collapsed ? buttonStartValue : 1);
const animatedOpacity = new Animated.Value(collapsed ? buttonStartValue : 1);
const animatedContentOpacity = new Animated.Value(collapsed ? 0 : 1);
const animatedScaleArray = getAnimatedScales();
const itemsCount = React.Children.count(children);
const easeOut = Easing.bezier(0, 0, 0.58, 1);
const Container = !disablePresses ? TouchableOpacity : View;
useEffect(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
}, [collapsed]);
// 动画
const animate = async () => {
return Promise.all([animateValues(), animateCards()]);
};
const animateValues = () => {
const newValue = collapsed ? buttonStartValue : 1;
return new Promise(resolve => {
Animated.parallel([Animated.timing(animatedOpacity, {
duration: DURATION,
toValue: Number(newValue),
useNativeDriver: true
}), Animated.timing(animatedScale, {
toValue: Number(newValue),
easing: easeOut,
duration: DURATION,
useNativeDriver: true
}), Animated.timing(animatedContentOpacity, {
toValue: Number(collapsed ? 0 : 1),
easing: easeOut,
duration: DURATION,
useNativeDriver: true
})]).start(resolve);
});
};
function getItemScale(index) {
if (collapsed) {
return 0.9;
}
return 1;
}
const animateCards = () => {
const promises = [];
for (let index = 0; index < itemsCount; index++) {
const newScale = getItemScale(index);
promises.push(new Promise(resolve => {
Animated.timing(animatedScaleArray[index], {
toValue: Number(newScale),
easing: easeOut,
duration: DURATION,
useNativeDriver: true
}).start(resolve);
}));
}
return Promise.all(promises);
};
// 关闭折叠
const close = async () => {
setCollapsed(true);
invoke(props, 'onCollapseWillChange', true);
if (onCollapseChanged) {
await animate();
onCollapseChanged(true);
} else {
animate();
}
};
// 打开折叠
const open = async () => {
setCollapsed(false);
invoke(props, 'onCollapseWillChange', false);
if (onCollapseChanged) {
await animate();
onCollapseChanged(false);
} else {
animate();
}
};
const getTop = index => {
let start = 0;
if (index === itemsCount - 2) {
start += PEEP;
}
if (index === itemsCount - 1) {
start += PEEP * 2;
}
return start;
};
const getStyle = index => {
const top = getTop(index);
if (collapsed) {
return {
position: index !== 0 ? 'absolute' : undefined,
top
};
}
return {
marginBottom: MARGIN_BOTTOM,
marginTop: index === 0 ? 40 : undefined
};
};
const onLayout = event => {
const height = event.nativeEvent.layout.height;
if (height) {
setFirstItemHeight(height);
}
};
const onItemPress = index => {
invoke(props, 'onItemPress', index);
};
const renderItem = (item, index) => {
return <Animated.View key={index} onLayout={index === 0 ? onLayout : undefined} style={[Platform.OS === 'ios' && styles.containerShadow, getStyle(index), {
borderRadius: Platform.OS === 'ios' ? itemBorderRadius : undefined,
alignSelf: 'center',
zIndex: itemsCount - index,
transform: [{
scaleX: animatedScaleArray[index]
}],
width: Dimensions.get('screen').width - 40,
height: collapsed ? firstItemHeight : undefined
}]} collapsable={false}>
<Container style={[contentContainerStyle, styles.card, {
borderRadius: itemBorderRadius
}]} onPress={() => disablePresses && onItemPress(index)}>
<TouchableOpacity onPress={close}>
<Animated.View style={index !== 0 ? {
opacity: animatedContentOpacity
} : undefined} collapsable={false}>
{item}
</Animated.View>
</TouchableOpacity>
</Container>
</Animated.View>;
};
return <View style={containerStyle}>
<View style={{
marginBottom: PEEP * 3
}}>
{/* <Animated.View
style={{
position: 'absolute',
right: 0,
opacity: animatedOpacity,
transform: [{ scale: animatedScale }],
}}
>
{collapsed ? null : (
<TouchableOpacity onPress={close}>
<Icon xml={down} size={30} />
</TouchableOpacity>
)}
</Animated.View> */}
{React.Children.map(children, (item, index) => {
return renderItem(item, index);
})}
{collapsed && <TouchableOpacity onPress={open} activeOpacity={1} style={[styles.touchable, {
height: firstItemHeight ? firstItemHeight + PEEP * 2 : undefined,
zIndex: itemsCount
}]} />}
</View>
</View>;
}
const styles = StyleSheet.create({
touchable: {
position: 'absolute',
width: '100%'
},
containerShadow: {
backgroundColor: colors.white,
shadowColor: colors.colorsPalette.dark40,
shadowOpacity: 0.25,
shadowRadius: 12,
shadowOffset: {
height: 5,
width: 0
}
},
card: {
overflow: 'hidden',
flexShrink: 1
}
});