使用react-native-reanimated实现的跑马灯效果

实现跑马灯的效果,代码如下:

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。