实现跑马灯的效果,代码如下:
import React, { useState } from 'react';
import { Button, StyleSheet, View, Image, Text } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useFrameCallback,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
const MeasureElement = ({ onLayout, children }) => (
<Animated.ScrollView
horizontal
style={marqueeStyles.hidden}
pointerEvents="box-none">
<View onLayout={(ev) => onLayout(ev.nativeEvent.layout.width)}>
{children}
</View>
</Animated.ScrollView>
);
const TranslatedElement = ({ index, children, offset, childrenWidth }) => {
//设计方向:有三组相同的View,左中右,永远是中间组的view在移动
const animatedStyle = useAnimatedStyle(() => {
return {
left: (index - 1) * childrenWidth,
transform: [
{
translateX: -offset.value,
},
],
};
});
return (
<Animated.View style={[styles.animatedStyle, animatedStyle]}>
{children}
</Animated.View>
);
};
const getIndicesArray = (length) => Array.from({ length }, (_, i) => i);
const Cloner = ({ count, renderChild }) => (
<>{getIndicesArray(count).map(renderChild)}</>
);
const ChildrenScroller = ({
duration,
childrenWidth,
parentWidth,
reverse,
children,
}) => {
const offset = useSharedValue(0);
const coeff = useSharedValue(reverse ? 1 : -1);
React.useEffect(() => {
coeff.value = reverse ? 1 : -1;
//循环动画方案一
const endOver = withTiming(childrenWidth, { duration, easing: Easing.linear });
offset.value = withRepeat(endOver, -1, false)
}, [reverse]);
//循环动画方案二
// useFrameCallback((i) => {
// // prettier-ignore (计算在总时间内滑动一组的平均一个item时间)
// offset.value += (coeff.value * ((i.timeSincePreviousFrame ?? 10) * childrenWidth)) / duration;
// offset.value = offset.value % childrenWidth;
// //这里注意,当移动的距离刚好是childrenWidth,位移又重新从0开始,实现可以循环滚动效果
// }, true);
//屏幕左右两边添加一组view,形成无限循环的现象
const count = Math.round(parentWidth / childrenWidth) + 2;
const renderChild = (index) => (
<TranslatedElement
key={`clone-${index}`}
index={index}
offset={offset}
childrenWidth={childrenWidth}>
{children}
</TranslatedElement>
);
return <Cloner count={count} renderChild={renderChild} />;
};
const Marquee = ({ duration = 2000, reverse = false, children, style }) => {
const [parentWidth, setParentWidth] = React.useState(0);
const [childrenWidth, setChildrenWidth] = React.useState(0);
return (
<View
style={style}
onLayout={(ev) => {
setParentWidth(ev.nativeEvent.layout.width);
}}
pointerEvents="box-none">
<View style={marqueeStyles.row} pointerEvents="box-none">
<MeasureElement onLayout={setChildrenWidth}>{children}</MeasureElement>
{childrenWidth > 0 && parentWidth > 0 && (
<ChildrenScroller
duration={duration}
parentWidth={parentWidth}
childrenWidth={childrenWidth}
reverse={reverse}>
{children}
</ChildrenScroller>
)}
</View>
</View>
);
};
const marqueeStyles = StyleSheet.create({
hidden: { opacity: 0, zIndex: -1 },
row: { flexDirection: 'row', overflow: 'hidden' },
});
//示例
function MarqueeScreen() {
const [reverse, setReverse] = useState(false);
const list = [{ backgroundColor: 'red', index: 0 },
{ backgroundColor: 'green', index: 1 },
{ backgroundColor: 'blue', index: 2 },
{ backgroundColor: 'yellow', index: 3 },
{ backgroundColor: 'brown', index: 4 },
]
return (
<View style={styles.container}>
<View style={styles.safeArea}>
<Marquee reverse={reverse} duration={10000} reverse={true}>
<View style={{ flexDirection: 'row' }}>
{list.map((item, index) => {
return <View key={index} style={[styles.horseImage, { backgroundColor: item.backgroundColor }]}>
<Text style={{ color: '#000', fontSize: 15 }}>{item.index}</Text>
</View>
})}
</View>
</Marquee>
</View>
</View>
);
}
const styles = StyleSheet.create({
horseImage: {
width: 140,
height: 80,
marginRight: 10,
alignItems: 'center',
justifyContent: 'center'
},
container: {
flex: 1,
},
safeArea: {
display: 'flex',
gap: '1rem',
// alignItems: 'center',
// justifyContent: 'center',
flex: 1,
},
animatedStyle: {
position: 'absolute',
},
circle: {
marginTop: 4,
borderRadius: 100,
height: 120,
width: 160,
backgroundColor: '#b58df1',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
});
export default MarqueeScreen;
运行效果,在js页面添加:
<MarqueeScreen/>
image.png