rn自定义滑动条,需要动画库react-native-reanimated

import React, { useEffect, useState } from 'react'

import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'

import { GestureDetector, Gesture } from 'react-native-gesture-handler'

import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated'

import { useLayout } from '@react-native-community/hooks'

const values = [30, 35, 40, 45, 50, 55, 60, 65, 70]

const lables = ['30', '|', '40', '|', '50', '|', '60', '|', '70']

interface CutHeightSliderProps {

  value: number

  onValueChanged?: (value: number) => void

  style?: StyleProp<ViewStyle>

}

export default function CutHeightSlider(props: CutHeightSliderProps) {

  const { value, onValueChanged, style } = props

  const [selectedIndex, setSelectedIndex] = useState(values.indexOf(value))

  const { width, onLayout } = useLayout()

  const size = width / values.length

  const offset = useSharedValue(0)

  const w = useSharedValue(0)

  const draggable = useSharedValue(false)

  const lastIndex = useSharedValue(selectedIndex)

  const [showTop, setShowTop] = useState(false)

  useEffect(() => {

    if (width !== 0) {

      w.value = width

    }

  }, [width, w])

  useEffect(() => {

    const index = values.indexOf(value)

    if (index !== -1) {

      offset.value = index * size

      lastIndex.value = index

      setSelectedIndex(index)

    }

  }, [value, size, offset, lastIndex])

  const animatedStyle = useAnimatedStyle(() => {

    return {

      transform: [

        {

          translateX: offset.value,

        },

      ],

    }

  })

  const onIndexChanged = (index: number) => {

    lastIndex.value = index

    setSelectedIndex(index)

    const nextValue = values[index]

    if (nextValue !== value) {

      onValueChanged && onValueChanged(values[index])

    }

  }

  const onIndexChangedStart = () => {

    setShowTop(true)

  }

  const onIndexChangedEnd = () => {

    setTimeout(() => {

      setShowTop(false)

    }, 500);

  }

  const onIndexChangedTap = (index: number) => {

    setShowTop(true)

    lastIndex.value = index

    setSelectedIndex(index)

    const nextValue = values[index]

    if (nextValue !== value) {

      onValueChanged && onValueChanged(values[index])

    }

    setTimeout(() => {

      setShowTop(false)

    }, 500);

  }

  const bubbleStyle = useAnimatedStyle(() => {

    return {

      transform: [

        {

          translateX: size*selectedIndex,

        },

      ],

    }

  })

  const tap = Gesture.Tap().onEnd(e => {

    const index = Math.floor(e.x / size)

    offset.value = withSpring(

      index * size,

      {

        stiffness: 500,

        overshootClamping: true,

      },

      () => runOnJS(onIndexChangedTap)(Math.abs(index)),

    )

  })

  const pan = Gesture.Pan()

    .onChange(e => {

      if (!draggable.value) {

        return

      }

      offset.value = offset.value + e.changeX

      if (offset.value < 0) {

        offset.value = 0

      }

      if (offset.value > w.value - size) {

        offset.value = w.value - size

      }

      const current = Math.round(offset.value / size)

      if (lastIndex.value !== current) {

        lastIndex.value = current

        runOnJS(setSelectedIndex)(current)

      }

    })

    .onStart(e => {

      const target = Math.floor(e.x / size)

      const current = Math.floor(offset.value / size)

      draggable.value = target === current

      runOnJS(onIndexChangedStart)()

    })

    .onEnd(() => {

      draggable.value = false

      const index = Math.round(offset.value / size)

      offset.value = withSpring(

        index * size,

        {

          stiffness: 500,

          overshootClamping: true,

        },

        () => runOnJS(onIndexChanged)(Math.abs(index)),

      )

      runOnJS(onIndexChangedEnd)()

    }

  )

  const composed = Gesture.Race(tap, pan)

  return (

    <GestureDetector gesture={composed}>

      <View style={[styles.root, style]} onLayout={onLayout}>

        <Animated.View style={[styles.thumb, { width: size, height: size }, animatedStyle]} />

        <View style={styles.plate}>

          {lables.map((label, index) => (

            <Mark

              key={values[index]}

              style={{ width: size, height: size }}

              text={index === selectedIndex ? String(values[index]) : label}

              checked={index === selectedIndex}

            />

          ))}

        </View>

           {/* <Animated.View style={[styles.bubble,{width:size*(selectedIndex+1)},]} > */}

        {showTop ?

           <Animated.View style={[styles.bubble,{width:size},animatedStyle]} >

            <View style={[styles.bubbleRect,{width:size}]}>

              <Text style={styles.bubbleText}>{values[selectedIndex]}</Text>

            </View>

            <View style={[{width:size,},styles.bubbleTriangleView]}>

              <View style={[styles.bubbleTriangle,]} />

            </View>

          </Animated.View>

        :null}

      </View>

    </GestureDetector>

  )

}

interface MarkProps {

  text: string

  checked?: boolean

  style?: StyleProp<ViewStyle>

}

function Mark(props: MarkProps) {

  const { text, checked, style } = props

  return (

    <View style={[styles.label, style]}>

      <Text style={[styles.labelText, checked ? styles.labelTextSelected : undefined]}>{text}</Text>

    </View>

  )

}

const styles = StyleSheet.create({

  root: {

    height: 48,

    backgroundColor: '#F7F8FF',

    borderRadius: 12,

    justifyContent: 'center',

  },

  plate: {

    ...StyleSheet.absoluteFillObject,

    flexDirection: 'row',

    alignItems: 'center',

  },

  label: {

    alignItems: 'center',

    justifyContent: 'center',

  },

  labelText: {

    color: '#17174A',

    fontSize: 14,

  },

  labelTextSelected: {

    color: '#4D58FF',

    fontWeight: 'bold',

  },

  thumb: {

    backgroundColor: '#DBDEFF',

    borderRadius: 8,

  },

  bubble: {

    position: 'absolute',

    height: 40,

    // width: 30,

    bottom: 55,

    // paddingTop: 4,

    alignItems: 'flex-end',

    // backgroundColor:'blue',

    // marginHorizontal: 12,

  },

  bubbleRect: {

    height: 40,

    // width: BUBBLE_WIDTH,

    borderRadius: 4,

    backgroundColor: 'rgba(223, 227, 229, 1)',

    justifyContent: 'center',

    alignItems: 'center',

  },

  bubbleTriangleView:{

    position:'absolute',

    bottom:0,

    alignItems:'center',

    backgroundColor:'rgba(223, 227, 229, 1)'

  },

  bubbleTriangle: {

    position: 'absolute',

    bottom: -8,

    width: 0,

    height: 0,

    backgroundColor: 'transparent',

    borderStyle: 'solid',

    borderTopWidth: 4,

    borderRightWidth: 4,

    borderBottomWidth: 4,

    borderLeftWidth: 4,

    borderTopColor: 'rgba(223, 227, 229, 1)',

    borderBottomColor: 'transparent',

    borderRightColor: 'transparent',

    borderLeftColor: 'transparent',

    zIndex: -10,

  },

  bubbleText: {

    color: '#292F33',

    fontSize: 12,

  },

})

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值