@uiw/react-native
Version:
UIW for React Native
216 lines (212 loc) • 6.26 kB
JavaScript
import keys from 'lodash/keys';
import map from 'lodash/map';
import React, { useMemo, useRef, useEffect } from 'react';
import { StyleSheet, View, ScrollView, TouchableOpacity, SafeAreaView, Platform, Animated, I18nManager, Image } from 'react-native';
import { useSetState } from 'ahooks';
import RnText from '../Typography/RnText';
import { last } from '../utils/utils';
import { useTheme } from '@shopify/restyle';
function ActionBar(props) {
const ios = Platform.OS === 'ios';
const baseRef = useRef();
const theme = useTheme();
const {
actions = [],
style,
keepAbsoulte = true,
height = 48,
backgroundColor = theme.colors.primary_background,
scroll = false,
useSafeArea = true,
focusIndex = 0
} = props;
const [state, setState] = useSetState({
itemsLayouts: {},
// items/layout
contentOffset: 0,
// 内容距离
scrollContentWidth: 0,
// 滚动距离
containerWidth: 0,
gradientOpacity: new Animated.Value(0),
gradientOpacityLeft: new Animated.Value(0)
});
const {
itemsLayouts,
contentOffset,
scrollContentWidth,
containerWidth,
gradientOpacity,
gradientOpacityLeft
} = state;
const Component = scroll ? ScrollView : useSafeArea && ios ? SafeAreaView : View;
const styles = createStyles({
height: height,
backgroundColor: backgroundColor,
shadowColor: theme.colors.gray300
});
useEffect(() => {
scroll && onFocusIndex(focusIndex);
}, [focusIndex]);
// 跳转
const onFocusIndex = (index = 0) => {
const focusedItemLayout = itemsLayouts[index];
if (focusedItemLayout && scroll) {
const {
x,
width
} = focusedItemLayout;
if (x < contentOffset) {
baseRef?.current?.scrollTo({
x: x - width
});
} else if (x + width > contentOffset + containerWidth) {
const offsetChange = Math.max(0, x - (contentOffset + containerWidth));
baseRef?.current?.scrollTo({
x: contentOffset + offsetChange + width
});
}
}
};
// 计算 continer/width
const onLayout = event => {
setState({
containerWidth: event.nativeEvent.layout.width,
gradientOpacity: new Animated.Value(scrollContentWidth > containerWidth ? 1 : 0)
});
};
// 计算 items/layout
const onItemsLayout = (event, index) => {
const layout = event.nativeEvent.layout || {};
itemsLayouts[index] = layout;
if (actions && keys(itemsLayouts).length === keys(actions).length) {
onFocusIndex(focusIndex);
}
};
const animateGradientOpacity = (offsetX, contentWidth, containerWidth) => {
const overflow = contentWidth - containerWidth;
const newValue = offsetX > 0 && offsetX >= overflow - 1 ? 0 : 1;
const newValueLeft = offsetX > 0 ? 1 : 0;
Animated.parallel([Animated.spring(gradientOpacity, {
toValue: newValue,
speed: 20,
useNativeDriver: true
}), Animated.spring(gradientOpacityLeft, {
toValue: newValueLeft,
speed: 20,
useNativeDriver: true
})]).start();
};
const onContentSizeChange = contentWidth => {
if (scrollContentWidth !== contentWidth) {
setState({
scrollContentWidth: contentWidth
});
if (contentWidth > containerWidth) {
setState({
gradientOpacity: new Animated.Value(1)
});
}
}
};
const onScroll = event => {
const {
layoutMeasurement,
contentOffset,
contentSize
} = event.nativeEvent;
setState({
contentOffset: contentOffset.x
});
const offsetX = contentOffset.x;
const contentWidth = contentSize.width;
const containerWidth = layoutMeasurement.width;
animateGradientOpacity(offsetX, contentWidth, containerWidth);
};
/**
* 下面为dom部分
* renderGradient 左侧 或 右侧 阴影判断
* Items
*/
const renderGradient = left => {
const imageTransform = I18nManager.isRTL ? left ? undefined : [{
scaleX: -1
}] : left ? [{
scaleX: -1
}] : undefined;
const heightToUse = 48 || height || '100%';
return <Animated.View pointerEvents="none" style={{
opacity: left ? gradientOpacityLeft : gradientOpacity,
width: 76,
height: heightToUse,
position: 'absolute',
right: !left ? 0 : undefined,
left: left ? 0 : undefined
}}>
<Image source={require('./assets/gradientOverlay.png')} style={{
width: 76,
height: heightToUse,
tintColor: theme.colors.white,
transform: imageTransform
}} resizeMode="stretch" />
</Animated.View>;
};
const Items = useMemo(() => {
return <View style={[styles.container, {
justifyContent: 'space-between',
paddingHorizontal: scroll ? 0 : 20,
...style
}]}>
{map(actions, ({
label = '',
onPress,
render,
fontStyle
}, i) => {
const prop = {
onLayout: event => onItemsLayout(event, i),
key: i,
style: {
marginRight: scroll && !last(actions.length, i) ? 20 : 0
}
};
// 自定义
if (render) {
return <View {...prop}>{render}</View>;
}
return <TouchableOpacity {...prop} onPress={onPress && onPress}>
<RnText style={{
...fontStyle
}} label={label} />
</TouchableOpacity>;
})}
</View>;
}, [actions]);
return <Component ref={baseRef} horizontal scrollEventThrottle={100} showsHorizontalScrollIndicator={false} onScroll={onScroll} onContentSizeChange={onContentSizeChange} style={[keepAbsoulte && styles.absoluteContainer]} onLayout={onLayout}>
{Items}
{scroll && renderGradient(true)}
{scroll && renderGradient(false)}
</Component>;
}
function createStyles({
height,
backgroundColor,
shadowColor
}) {
return StyleSheet.create({
container: {
height,
flexDirection: 'row',
alignItems: 'center'
},
absoluteContainer: {
...StyleSheet.absoluteFillObject,
top: undefined,
backgroundColor,
shadowColor: shadowColor,
shadowOpacity: 0.06,
shadowRadius: 18.5
}
});
}
export default ActionBar;