React Native Reanimated2 TimePicker Typescript 
Published: 2021-03-09 18:43:31

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c4g2llm1n1qhg97nefwk.png)
 

Reanimated2 TimePicker Typescript React Native

```javascript
const screenHeight = 280
const textHeight = 56

interface TimePickerPropsInterface {
  backgroundColor: string
  startHourValue: number
  startMinuteValue: number
  hourValueChanged: (hour: number) => void
  minuteValueChanged: (minute: number) => void
}

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
    height: screenHeight,
    marginTop: 150,
  },
})

const animatedContainerLeftStyle: ViewStyle = {
  position: "absolute",
  left: 0,
  right: Dimensions.get("screen").width / 2,
  top: 0,
  opacity: 0.5,
  height: screenHeight,
}

const animatedContainerRightStyle: ViewStyle = {
  position: "absolute",
  left: Dimensions.get("screen").width / 2,
  right: 0,
  top: 0,
  height: screenHeight,
  opacity: 0.5,
}

const visualContainerRightStyle: ViewStyle = {
  alignItems: "center",
  marginRight: 20,
  flex: 1,
}

const visualContainerLeftStyle: ViewStyle = {
  alignItems: "center",
  marginLeft: 20,
  flex: 1,
}

const basicTextStyle: TextStyle = {
  fontSize: 52,
  height: textHeight,
  color: palette.darkGreen,
  lineHeight: 59,
}

const backgroundAbove: ViewStyle = {
  backgroundColor: "#fff",
  opacity: 1,
  top: -textHeight - 20,
  height: textHeight + 20,
  width: "100%",
  position: "absolute",
}

const backgroundBelow: ViewStyle = {
  backgroundColor: "#fff",
  opacity: 1,
  bottom: -textHeight - 25,
  height: textHeight + 25,
  width: "100%",
  position: "absolute",
}

const gradientBottom: ViewStyle = {
  height: textHeight - 10,
  width: "100%",
  bottom: 0,
  position: "absolute",
}

const gradientTop: ViewStyle = {
  height: textHeight - 10,
  width: "100%",
  top: 0,
  position: "absolute",
}

const centerView: ViewStyle = {
  height: textHeight,
  borderTopWidth: 0.5,
  borderBottomWidth: 0.5,
  flex: 1,
  justifyContent: "center",
  alignItems: "center",
  position: "absolute",
  top: screenHeight / 2 - textHeight / 2,
  right: -20,
  left: -20,
}

const BOX1: ViewStyle = {
  flex: 1,
  justifyContent: "center",
  alignItems: "center",
}
const TEXTSTYLE: TextStyle = {
  color: "black",
  fontSize: 52,
  alignSelf: "center",
  lineHeight: 52,
  textAlign: "center",
}

export const TimePicker: React.FC<TimePickerPropsInterface> = (props) => {
  const hourOffset = props.startHourValue * textHeight
  const minuteOffset = props.startMinuteValue * textHeight
  const hourSavedValue = useSharedValue(-1)
  const minuteSavedValue = useSharedValue(-1)
  const yL = useSharedValue(hourOffset)
  const yR = useSharedValue(minuteOffset)

  const textNumberOffset = (screenHeight - textHeight) / 2

  const translateYHour = useAnimatedStyle(() => {
    return {
      transform: [{ translateY: textNumberOffset - yL.value }],
    }
  })

  const translateYMinute = useAnimatedStyle(() => {
    return {
      transform: [{ translateY: textNumberOffset - yR.value }],
    }
  })

  const scrollL = useAnimatedScrollHandler({
    onScroll: (e) => {
      yL.value = e.contentOffset.y
    },
  })

  const scrollR = useAnimatedScrollHandler({
    onScroll: (e) => {
      yR.value = e.contentOffset.y
    },
  })

  const indexTextLeft = useDerivedValue(() => {
    const i = Math.round(yL.value / textHeight)
    hourSavedValue.value = i
    if (i !== hourSavedValue.value) {
      hourSavedValue.value = i
      runOnJS(ReactNativeHaptic.generate)("notification")
    }
    return i
  })

  const indexTextRight = useDerivedValue(() => {
    const i = Math.round(yR.value / textHeight)

    if (i !== minuteSavedValue.value) {
      minuteSavedValue.value = i
      runOnJS(ReactNativeHaptic.generate)("notification")
    }
    return i
  })

  return (
    <View style={[styles.container, { backgroundColor: props.backgroundColor }]}>
      {/* HOUR */}
      <Animated.View style={[visualContainerLeftStyle, translateYHour]}>
        {TimeHourTexts.map((c, i) => (
          <OpacityNumber
            i={i}
            y={yL}
            index={indexTextLeft}
            text={c.text}
            key={c.text}
          ></OpacityNumber>
        ))}
      </Animated.View>
      <Animated.ScrollView
        onScroll={scrollL}
        contentOffset={{ y: hourOffset, x: 0 }}
        style={animatedContainerLeftStyle}
        snapToInterval={textHeight}
        scrollEventThrottle={1}
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{
          height: screenHeight - textHeight + textHeight * TimeHourTexts.length,
        }}
        decelerationRate="normal"
        onMomentumScrollEnd={() => {
          props.hourValueChanged(indexTextLeft.value)
        }}
      ></Animated.ScrollView>

      {/* MINUTE */}
      <Animated.View style={[visualContainerRightStyle, translateYMinute]}>
        {TimeMinutesTexts.map((c, i) => (
          <OpacityNumber
            i={i}
            y={yR}
            index={indexTextRight}
            text={c.text}
            key={c.text}
          ></OpacityNumber>
        ))}
      </Animated.View>

      <Animated.ScrollView
        onScroll={scrollR}
        contentOffset={{ y: minuteOffset, x: 0 }}
        style={animatedContainerRightStyle}
        snapToInterval={textHeight}
        scrollEventThrottle={1}
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{
          height: screenHeight - textHeight + textHeight * TimeMinutesTexts.length,
        }}
        decelerationRate="normal"
        onMomentumScrollEnd={() => {
          props.minuteValueChanged(indexTextRight.value)
        }}
      ></Animated.ScrollView>
      <View
        style={[
          backgroundAbove,
          {
            backgroundColor: props.backgroundColor,
          },
        ]}
      ></View>
      <View
        style={[
          backgroundBelow,
          {
            backgroundColor: props.backgroundColor,
          },
        ]}
      ></View>
      <LinearGradient
        pointerEvents="none"
        colors={[rgba(props.backgroundColor, 0), props.backgroundColor]}
        style={gradientBottom}
      ></LinearGradient>
      <LinearGradient
        pointerEvents="none"
        colors={[props.backgroundColor, rgba(props.backgroundColor, 0)]}
        style={gradientTop}
      ></LinearGradient>
      <View
        style={[
          centerView,
          {
            borderTopColor: rgba("black", 0.3),
            borderBottomColor: rgba("black", 0.3),
          },
        ]}
        pointerEvents="none"
      >
        <View style={BOX1}>
          <Text preset="h1" style={TEXTSTYLE}>
            :
          </Text>
        </View>
      </View>
    </View>
  )
}

interface Props {
  y: Animated.SharedValue<number>
  // y: number
  index: Animated.SharedValue<number>
  i: number
  text: string
}

export const OpacityNumber: React.FC<Props> = ({ y, index, i, text }) => {
  const previousPosition = (i - 1) * textHeight
  const currentPosition = i * textHeight
  const nextPosition = (i + 1) * textHeight

  const style = useAnimatedStyle(() => {
    const op = interpolate(
      y.value,
      [previousPosition, currentPosition, nextPosition],
      [0.3, 1, 0.3],
      Extrapolate.CLAMP,
    )
    const calc = index.value > i + 2 ? 0 : index.value < i - 2 ? 0 : op

    return {
      opacity: calc,
    }
  })
  return (
    <Animated.View style={style} key={text}>
      <Text preset="h1" style={basicTextStyle} key={text}>
        {text}
      </Text>
    </Animated.View>
  )
}
```