Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: dont scroll if content fits in width #4

Open
MobileMon opened this issue Feb 12, 2024 · 3 comments
Open

Feature request: dont scroll if content fits in width #4

MobileMon opened this issue Feb 12, 2024 · 3 comments

Comments

@MobileMon
Copy link

only scroll if scrolling is neccessary

@deadlinecode
Copy link

deadlinecode commented Apr 25, 2024

A little AutoMarquee Helper:

import { memo, useEffect, useState } from 'react'
import { StyleSheet, Text, TextStyle, type ViewStyle } from 'react-native'
import { View } from 'react-native'
import Animated, {
  type SharedValue,
  runOnJS,
  useAnimatedReaction,
  useAnimatedStyle,
  useFrameCallback,
  useSharedValue,
} from 'react-native-reanimated'

const AnimatedChild = ({
  index,
  children,
  anim,
  textWidth,
  spacing,
}: React.PropsWithChildren<{
  index: number
  anim: SharedValue<number>
  textWidth: SharedValue<number>
  spacing: number
}>) => {
  const stylez = useAnimatedStyle(() => {
    return {
      position: 'absolute',
      width: textWidth.value + spacing,
      alignSelf: 'flex-start',
      left: index * (textWidth.value + spacing),
      transform: [
        {
          translateX: -(anim.value % (textWidth.value + spacing)),
        },
      ],
    }
  }, [index, spacing, textWidth])
  return <Animated.View style={stylez}>{children}</Animated.View>
}

export type MarqueeProps = React.PropsWithChildren<{
  speed?: number
  spacing?: number
  style?: ViewStyle
}>

/**
 * Used to animate the given children in a horizontal manner.
 */
export const Marquee = memo(({ speed = 1, children, spacing = 0, style }: MarqueeProps) => {
  const parentWidth = useSharedValue(0)
  const textWidth = useSharedValue(0)
  const [cloneTimes, setCloneTimes] = useState(0)
  const anim = useSharedValue(0)

  useFrameCallback(() => {
    anim.value += speed
  }, true)

  useAnimatedReaction(
    () => {
      if (textWidth.value === 0 || parentWidth.value === 0) {
        return 0
      }
      return Math.round(parentWidth.value / textWidth.value) + 1
    },
    (v) => {
      if (v === 0) {
        return
      }
      // This is going to cover the case when the text/element size
      // is greater than the actual parent size
      // Double this to cover the entire screen twice, in this way we can
      // reset the position of the first element when its going to move out
      // of the screen without any noticible glitch
      runOnJS(setCloneTimes)(v * 2)
    },
    []
  )
  return (
    <Animated.View
      style={style}
      onLayout={(ev) => {
        parentWidth.value = ev.nativeEvent.layout.width
      }}
      pointerEvents="box-none"
    >
      <Animated.View style={styles.row} pointerEvents="box-none">
        {
          // We are adding the text inside a ScrollView because in this way we
          // ensure that its not going to "wrap".
        }
        <Animated.ScrollView horizontal style={styles.hidden} pointerEvents="box-none">
          <View
            onLayout={(ev) => {
              textWidth.value = ev.nativeEvent.layout.width
            }}
          >
            {children}
          </View>
        </Animated.ScrollView>
        {cloneTimes > 0 &&
          [...Array(cloneTimes).keys()].map((index) => {
            return (
              <AnimatedChild
                key={`clone-${index}`}
                index={index}
                anim={anim}
                textWidth={textWidth}
                spacing={spacing}
              >
                {children}
              </AnimatedChild>
            )
          })}
      </Animated.View>
    </Animated.View>
  )
})

const styles = StyleSheet.create({
  hidden: { opacity: 0, zIndex: -9999 },
  row: { flexDirection: 'row', overflow: 'hidden' },
})

export const AutoMarquee = memo(
  (
    props: Omit<MarqueeProps, 'children' | 'style'> & {
      children: string
      className?: string
      style?: TextStyle
    }
  ) => {
    const [textWidth, setTextWidth] = useState(0)
    const [viewWidth, setViewWidth] = useState(0)

    useEffect(() => {
      console.log('textWidth', textWidth)
      console.log('viewWidth', viewWidth)
    }, [textWidth, viewWidth])

    return (
      <View className="w-full" onLayout={(e) => setViewWidth(e.nativeEvent.layout.width)}>
        <View
          style={{ height: 0, alignSelf: 'flex-start' }}
          onLayout={(e) => {
            setTextWidth(e.nativeEvent.layout.width)
          }}
        >
          <Text style={{ width: '100%' }} className={props.className} children={props.children} />
        </View>
        {textWidth < viewWidth ? (
          <Text style={props.style} className={props.className} children={props.children} />
        ) : (
          <Marquee {...{ ...props, style: undefined, children: undefined, textWidth }}>
            <Text style={props.style} className={props.className} children={props.children} />
          </Marquee>
        )}
      </View>
    )
  }
)

@chunghn
Copy link

chunghn commented May 3, 2024

@deadlinecode Thank you, the AutoMarquee works perfectly in my case. And it is smart.

I just wonder is this a common way to measure layout in prior in RN?

@deadlinecode
Copy link

I mostly do it Like that by using onLayout

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants