import React from 'react' ; import { View , StyleSheet , PanResponder , PanResponderInstance , LayoutChangeEvent , Animated , GestureResponderEvent , PanResponderGestureState , ViewStyle , StyleProp , Easing , } from 'react-native' ; export const numberSliderDefaultProps = { /**
     * 当前值
     */ value : 0 , /**
     * 允许滑动的最小值
     * 默认值 0
     */ min : 0 , /**
     * 允许滑动的最大值
     * 默认值 1000
     */ max : 1000 , /**
     * 滑动时的步长
     */ step : 1 , /**
     * 是否禁用
     * 默认 false
     */ disabled : false , /**
     * 轨道颜色
     * 默认 rgba(0,0,0,.4)
     */ trackColor : 'rgba(0,0,0,.4)' , /**
     * 激活的轨道颜色
     * 默认 #fff
     */ tintColor : '#fff' , /**
     * 滑动生效的开始距离
     * 默认为 7 个像素距离
     */ invalidSwipeDistance : 7 , /**
     * 是否可以点击选择
     * 默认为 true
     */ clickEnabled : true , /**
     * 是否可以能过轨道直接滑动
     */ trackSlideEnabled : false , /**
     * 是否显示激活轨道
     * 默认为显示
     */ showTint : true , /**
     * 滑动方向
     */ direction : 'horizontal' as 'horizontal' | 'vertical' , /**
     * 是否反向滑动
     * 当为水平滑动条时，默认滑条的最小值在左边，设置reverse = true后，最小值在右边
     * 当为垂直滑动条时，默认滑条的最小值在下边，设置reverse = true后，最小值在上边
     */ reverse : false , /**
     * 使用动画显示
     */ showAnimation : true , /**
     * 拖动时，滑动的有效触控区大小
     */ thumbTouchSize : { width : 40 , height : 40 } , /**
     * 测试标位
     */ accessibilityLabel : 'numberSlider' , /**
     * 滑块的可滑动范围类型
     * inner 为滑动范围为滑块内，滑动不会超出轨道的范围
     * outer 为滑动范围可超出轨道范围，滑动时滑块中心位置可与轨道的边重叠
     */ thumbLimitType : 'inner' as 'inner' | 'outer' , param value onGrant ( value ) { } , param value onMove ( value ) { } , param value onRelease ( value ) { } , param value onPress ( value ) { } , param value onChange ( value ) { } , param offset param value onThumbChange ( offset , value ? ) { } , } ; type DefaultProps = Readonly < typeof numberSliderDefaultProps > ; export type NumberSliderProps = { /**
     * 组件最外层容器样式
     */ style ? : StyleProp < ViewStyle > ; /**
     * 轨道的样式
     */ trackStyle ? : StyleProp < ViewStyle > ; /**
     * 激活部分轨道样式
     */ tintStyle ? : StyleProp < ViewStyle > ; /**
     * 滑块的样式
     */ thumbStyle ? : StyleProp < ViewStyle > ; /**
     * 显示的最小值
     * 值此必须小于等于允许滑动的最小值 min 的值，如果不设置则默认等于允许滑动的最小值 min 的值
     *
     */ showMin ? : number ; /**
     * 显示的最大值
     * 值此必须大于等于允许滑动的最大值 max 的值，如果不设置则默认等于允许滑动的最大值 max 的值
     */ showMax ? : number ; /**
     * 渲染轨道
     */ renderTrack ? : ( ) => React . ReactNode ; /**
     * 渲染激活部分轨道
     */ renderTint ? : ( ) => React . ReactNode ; /**
     * 渲染滑块
     */ renderThumb ? : ( ) => React . ReactNode ; /**
     * 组件的layout事件
     */ onLayout ? : ( e : LayoutChangeEvent ) => void ; /**
     * 组件的轨道layout事件
     */ onTrackLayout ? : ( e : LayoutChangeEvent ) => void ; } & DefaultProps ; interface NumberSliderState { value : number ; showTint : boolean ; } /**
 * 基础滑动条 slider
 * 此滑动条仅保证整数值情况下完整的
 */ export default class NumberSlider extends React . Component < NumberSliderProps , NumberSliderState > { static defaultProps : DefaultProps = numberSliderDefaultProps ; constructor ( props ) { super ( props ) ; this . _panResponder = PanResponder . create ( { onStartShouldSetPanResponder : this . handleStartPanResponder , onMoveShouldSetPanResponder : this . handleSetPanResponder , onPanResponderTerminationRequest : ( ) => ! this . moving , onPanResponderMove : this . onMove , onPanResponderRelease : this . onRelease , onPanResponderReject : this . handleTerminate , onPanResponderTerminate : this . handleTerminate , } ) ; this . animate . addListener ( this . fireThumbChange ) ; this . state = { value : this . props . value , showTint : false } ; this . tempValue = this . props . value ; } componentWillReceiveProps ( nextProps ) { if ( ! this . locked ) { this . handleLength ( nextProps ) ; if ( nextProps . value !== this . props . value || nextProps . direction !== this . props . direction ) { const valueLength = this . valueToCoor ( nextProps . value ) ; this . setAnimationValue ( valueLength ) ; this . valueLength = valueLength ; this . setState ( { value : nextProps . value } ) ; this . tempValue = nextProps . value ; } } } shouldComponentUpdate ( ) { return ! this . locked ; } componentWillUnmount ( ) { this . animate . removeAllListeners ( ) ; } onMove = ( e , gesture ) => { if ( this . moving || this . handleSetPanResponder ( e , gesture ) ) { this . handleMove ( gesture , this . props . onMove , false ) ; } } ; onRelease = ( e , gesture ) => { this . isTouchStart = false ; if ( this . moving ) { this . moving = false ; this . locked = false ; this . handleMove ( gesture , this . props . onRelease , true ) ; } else if ( this . props . clickEnabled ) { const { direction } = this . props ; const { centerX , centerY } = this . getThumbBound ( ) ; const now = + new Date ( ) ; if ( Math . abs ( gesture . dx ) < 4 && Math . abs ( gesture . dy ) < 4 && now - this . grantTime < 300 ) { const { locationX , locationY } = e . nativeEvent ; let diff = 0 ; if ( direction === 'horizontal' ) { diff = locationX - centerX ; } else { diff = locationY - centerY ; } this . handleMove ( { dx : diff , dy : diff } as PanResponderGestureState , this . props . onPress , true ) ; } } } ; setAnimationValue ( value , isSliding = false , noAnimation = false ) { if ( ! noAnimation && this . props . showAnimation ) { if ( value !== this . valueLength ) { this . animate . stopAnimation ( ) ; Animated . timing ( this . animate , { toValue : value , duration : isSliding ? 32 : Math . round ( ( 300 * Math . abs ( value - this . valueLength ) ) / this . maxLength ) , easing : Easing . linear , } ) . start ( ) ; } } else { this . animate . setValue ( value ) ; } } /**
     * 返回滑动位置
     */ getThumbBound ( ) { const { direction , reverse , thumbTouchSize : { width , height } , } = this . props ; const isHorizontal = direction === 'horizontal' ; let x = 0 ; let y = 0 ; if ( isHorizontal ) { y = this . trackWidth / 2 ; if ( reverse ) { x = this . maxLength + this . offsetMaxLength - this . valueLength ; } else { x = this . offsetMinLength + this . valueLength ; } } else { x = this . trackWidth / 2 ; if ( reverse ) { y = this . offsetMinLength + this . valueLength ; } else { y = this . maxLength + this . offsetMaxLength - this . valueLength ; } } return { x : x - width / 2 , y : y - height / 2 , width , height , centerX : x , centerY : y } ; } handleMove ( gesture , callback , isEnd = false ) { const { valueLength , maxLength } = this ; const { direction , reverse , onChange } = this . props ; const isHorizontal = direction === 'horizontal' ; const { dx , dy } = gesture ; let diff = 0 ; if ( isHorizontal ) { diff = dx * ( reverse ? - 1 : 1 ) ; } else { diff = dy * ( reverse ? 1 : - 1 ) ; } let currentLength = valueLength + diff ; if ( currentLength < 0 ) { currentLength = 0 ; } else if ( currentLength > maxLength ) { currentLength = maxLength ; } const value = this . coorToValue ( currentLength ) ; currentLength = this . valueToCoor ( value ) ; this . setAnimationValue ( currentLength , ! isEnd , ! isEnd ) ; if ( Math . abs ( value - this . tempValue ) >= 0.000001 ) { onChange ( value ) ; } this . tempValue = value ; if ( isEnd ) { this . valueLength = currentLength ; this . setState ( { value } ) ; } callback ( value ) ; } _panResponder : PanResponderInstance ; locked = false ; trackLength = 0 ; trackSize : number [ ] = [ 0 , 0 ] ; trackWidth = 0 ; valueLength = 0 ; offsetMinLength = 0 ; offsetMaxLength = 0 ; maxLength = 0 ; thumbWidth = 0 ; thumbHeight = 0 ; animate : Animated . Value = new Animated . Value ( 0 ) ; moving = false ; grantTime = 0 ; isTouchStart = false ; needUpdate = false ; trackX = 0 ; trackY = 0 ; tempValue = 0 ; fireThumbChange = ( { value } ) => { this . props . onThumbChange ( value + this . offsetMinLength , this . coorToValue ( value ) ) ; } ; handleStartPanResponder = ( ) => { if ( this . props . disabled ) { return false ; } this . grantTime = + new Date ( ) ; return this . props . clickEnabled ; } ; handleSetPanResponder = ( e , gesture ) => { const { direction , invalidSwipeDistance , onGrant , disabled , trackSlideEnabled } = this . props ; if ( disabled ) { return false ; } const isHorizontal = direction === 'horizontal' ; if ( ! this . moving ) { if ( ( isHorizontal && Math . abs ( gesture . dx ) < invalidSwipeDistance ) || ( ! isHorizontal && Math . abs ( gesture . dy ) < invalidSwipeDistance ) ) { return false ; } if ( ! trackSlideEnabled ) { const { locationX , locationY } = e . nativeEvent ; const currentX = locationX - this . trackX ; const currentY = locationY - this . trackY ; const { x , y , width , height } = this . getThumbBound ( ) ; if ( currentX < x || currentX > width + x || currentY < y || currentY > height + y ) { return false ; } } onGrant ( this . state . value ) ; this . moving = true ; this . locked = true ; } return trackSlideEnabled ; } ; handleTerminate = ( ) => { this . moving = false ; this . locked = false ; this . isTouchStart = false ; } ; handleLength ( props ) { const { showMax , showMin , min , max , thumbStyle , thumbLimitType , direction } = props ; const { trackSize } = this ; const trackLength = direction === 'horizontal' ? trackSize [ 0 ] : trackSize [ 1 ] ; this . trackLength = trackLength ; this . trackWidth = direction === 'horizontal' ? trackSize [ 1 ] : trackSize [ 0 ] ; const thumbStyles = [ styles . thumb , thumbStyle ] ; const { height : thumbHeight = 24 , width : thumbWidth = 24 } = StyleSheet . flatten ( thumbStyles ) ; this . thumbWidth = thumbWidth as number ; this . thumbHeight = thumbHeight as number ; const isThumbInner = thumbLimitType === 'inner' ; const isHorizontal = direction === 'horizontal' ; let offset = 0 ; if ( isThumbInner ) { offset = isHorizontal ? this . thumbWidth / 2 : this . thumbHeight / 2 ; } const realMax = showMax === undefined ? max : showMax ; const realMin = showMin === undefined ? min : showMin ; this . offsetMinLength = ( trackLength * ( min - realMin ) ) / ( realMax - realMin ) + offset ; this . offsetMaxLength = ( trackLength * ( realMax - max ) ) / ( realMax - realMin ) + offset ; this . maxLength = Math . max ( 1 , trackLength - this . offsetMaxLength - this . offsetMinLength ) ; } handleLayout = ( e ) => { const { width , height , x , y } = e . nativeEvent . layout ; const { trackLength , trackWidth } = this ; this . trackSize = [ width , height ] ; this . trackX = x ; this . trackY = y ; this . handleLength ( this . props ) ; if ( ! this . locked && ( trackLength !== this . trackLength || trackWidth !== this . trackWidth ) ) { const valueLength = this . valueToCoor ( this . state . value ) ; this . setAnimationValue ( valueLength , false , true ) ; this . valueLength = valueLength ; } if ( ! this . state . showTint ) { this . setState ( { showTint : true } ) ; } this . props . onTrackLayout && this . props . onTrackLayout ( e ) ; } ; valueToCoor ( value ) { const { min , max } = this . props ; const percent = ( Math . min ( max , Math . max ( min , value ) ) - min ) / ( max - min ) ; return percent * this . maxLength ; } coorToValue ( length ) { const { min , max , step } = this . props ; let value = ( length / this . maxLength ) * ( max - min ) + min ; if ( step ) { value = Math . round ( ( value - min ) / step ) * step + min ; } return Math . min ( max , Math . max ( min , value ) ) ; } render ( ) { const { trackColor , tintColor , trackStyle , tintStyle , thumbStyle , style , direction , reverse , accessibilityLabel , thumbLimitType , showTint : visibleTint , renderTrack , renderTint , renderThumb , onLayout , children , } = this . props ; const { showTint } = this . state ; const isHorizontal = direction === 'horizontal' ; const containerStyle = [ isHorizontal ? styles . container : styles . containerVertical , style ] ; const thumbStyles = [ styles . thumb , thumbStyle ] ; const isThumbInner = thumbLimitType === 'inner' ; const trackStyles = [ isHorizontal ? styles . track : styles . trackVertical , trackStyle ] ; const { height : thumbHeight = 24 , width : thumbWidth = 24 } = StyleSheet . flatten ( thumbStyles ) ; const halfThumbWidth = ( thumbWidth as number ) / 2 ; const halfThumbHeight = ( thumbHeight as number ) / 2 ; const { height : trackHeight = 6 , width : trackWidth = 6 } = StyleSheet . flatten ( trackStyles ) ; return ( < View style = { containerStyle } accessibilityLabel = { accessibilityLabel } onLayout = { onLayout } > < View style = { [ styles . body , { flexDirection : isHorizontal ? 'row' : 'column' } ] } pointerEvents = "box-only" { ... this . _panResponder . panHandlers } > { } < View onLayout = { this . handleLayout } style = { [ isHorizontal ? styles . track : styles . trackVertical , { backgroundColor : trackColor , borderRadius : isHorizontal ? ( trackHeight as number ) / 2 : ( trackWidth as number ) / 2 , } , trackStyle , ] } > < View style = { { flex : 1 } } > { ! ! renderTrack && renderTrack ( ) } < /View> { } { showTint && visibleTint && ( < Animated . View style = { [ styles . tintTrack , isHorizontal ? { backgroundColor : tintColor , height : trackHeight , borderRadius : ( trackHeight as number ) / 2 , width : isThumbInner ? this . animate . interpolate ( { inputRange : [ 0 , halfThumbWidth , Math . max ( halfThumbWidth , this . maxLength - halfThumbWidth ) , Math . max ( halfThumbWidth , this . maxLength ) , ] , outputRange : [ 0 , thumbWidth as number , this . maxLength , this . maxLength + ( thumbWidth as number ) , ] , } ) : this . animate , [ reverse ? 'right' : 'left' ] : this . offsetMinLength - ( isThumbInner ? halfThumbWidth : 0 ) , } : { backgroundColor : tintColor , width : trackWidth , borderRadius : ( trackWidth as number ) / 2 , height : isThumbInner ? this . animate . interpolate ( { inputRange : [ 0 , halfThumbHeight , Math . max ( halfThumbHeight , this . maxLength - halfThumbHeight ) , Math . max ( halfThumbHeight , this . maxLength ) , ] , outputRange : [ 0 , thumbHeight as number , this . maxLength , this . maxLength + ( thumbHeight as number ) , ] , } ) : this . animate , [ reverse ? 'top' : 'bottom' ] : this . offsetMinLength - ( isThumbInner ? halfThumbHeight : 0 ) , } , tintStyle , ] } > { ! ! renderTint && renderTint ( ) } < /Animated.View>)} < /View> < View style = { { ... StyleSheet . absoluteFillObject } } pointerEvents = "none" > { children } < /View> { } { showTint && ( < Animated . View style = { [ thumbStyles , isHorizontal ? { [ reverse ? 'right' : 'left' ] : Animated . add ( this . animate , this . offsetMinLength - halfThumbWidth ) , } : { [ reverse ? 'top' : 'bottom' ] : Animated . add ( this . animate , this . offsetMinLength - halfThumbHeight ) , } , ] } > { ! ! renderThumb && renderThumb ( ) } < /Animated.View>)} < /View> < /View>); } } const styles = StyleSheet . create ( { body : { alignItems : 'center' , height : '100%' , justifyContent : 'center' , width : '100%' , } , container : { height : 48 , width : '100%' , } , containerVertical : { height : '100%' , minHeight : 50 , width : 48 , } , thumb : { backgroundColor : '#fff' , borderRadius : 12 , height : 24 , position : 'absolute' , width : 24 , } , tintTrack : { position : 'absolute' , } , track : { borderRadius : 3 , height : 6 , overflow : 'hidden' , width : '100%' , } , trackVertical : { borderRadius : 3 , height : '100%' , overflow : 'hidden' , width : 6 , } , } ) ;