Skip to content

Instantly share code, notes, and snippets.

@Grohden
Last active April 9, 2020 21:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Grohden/11640fdca517e218d629626d2917731a to your computer and use it in GitHub Desktop.
Save Grohden/11640fdca517e218d629626d2917731a to your computer and use it in GitHub Desktop.
Slider prototype for react native
/* eslint-disable no-magic-numbers */
import React from 'react'
import { StyleSheet, Text, View, ViewStyle } from 'react-native'
import { BaseColors } from '@constants/colors'
import fonts from '@constants/fonts'
type Props = {
style: ViewStyle
}
const LABEL_DIMEN = 32
// hypotenuse value (and floor | 0)
export const LABEL_HEIGHT = Math.sqrt((LABEL_DIMEN ** 2) * 2) | 0
const styles = StyleSheet.create({
container: {
transform: [
{ rotate: '-45deg' }
],
alignItems: 'center',
justifyContent: 'center',
backgroundColor: BaseColors.primary,
width: LABEL_DIMEN,
height: LABEL_DIMEN,
borderTopLeftRadius: LABEL_DIMEN,
borderTopRightRadius: LABEL_DIMEN,
borderBottomRightRadius: LABEL_DIMEN,
borderBottomLeftRadius: 0
},
font: {
...fonts.white.caption,
transform: [
{ rotate: '45deg' }
]
}
})
export const Label = (props: Props) => (
<View style={ [
styles.container,
props.style
] }>
<Text style={ styles.font }>0%</Text>
</View>
)
export default Label
import React from 'react'
import {
Platform,
StyleSheet,
TouchableHighlight,
View,
ViewStyle
} from 'react-native'
export const MARKER_DIMENSION = Platform.select({
default: 30,
android: 12
})
const styles = StyleSheet.create({
markerStyle: {
...Platform.select({
default: {
height: MARKER_DIMENSION,
width: MARKER_DIMENSION,
borderRadius: MARKER_DIMENSION,
borderWidth: 1,
borderColor: '#DDDDDD',
backgroundColor: '#FFFFFF',
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 3
},
shadowRadius: 1,
shadowOpacity: 0.2
},
android: {
height: MARKER_DIMENSION,
width: MARKER_DIMENSION,
borderRadius: MARKER_DIMENSION,
backgroundColor: '#0D8675'
}
})
},
pressedMarkerStyle: {
...Platform.select({
default: {},
android: {
height: 20,
width: 20,
borderRadius: 20
}
})
},
disabled: {
backgroundColor: '#d3d3d3'
}
})
type Props = {
enabled?: boolean
pressed?: boolean
markerStyle?: ViewStyle
pressedMarkerStyle?: ViewStyle
disabledMarkerStyle?: ViewStyle
}
const Marker = (props: Props) => {
const style = props.enabled
? [
styles.markerStyle,
props.markerStyle,
props.pressed && styles.pressedMarkerStyle,
props.pressed && props.pressedMarkerStyle
]
: [
styles.markerStyle,
styles.disabled,
props.disabledMarkerStyle
]
return (
<TouchableHighlight>
<View style={ style } />
</TouchableHighlight>
)
}
export default Marker
/* eslint-disable no-magic-numbers */
import React, { useCallback, useState } from 'react'
import {
LayoutChangeEvent,
Platform,
StyleSheet, View
} from 'react-native'
import Marker, { MARKER_DIMENSION } from './Marker'
import XDraggable from '@components/filter/slider/XDraggable'
import Animated, {
call,
Extrapolate,
interpolate,
max,
min,
round,
sub,
useCode,
Value
} from 'react-native-reanimated'
import Label, { LABEL_HEIGHT } from '@components/filter/slider/Label'
type Values = [number, number]
type Props = {
values: Values
min: number
max: number
step: number
onValuesChange: (values: Values) => void
onValuesChangeFinish?: (values: Values) => void
}
const styles = StyleSheet.create({
container: {
width: '100%',
height: MARKER_DIMENSION + 14
},
fullTrack: {
width: '100%',
height: 30,
paddingHorizontal: 24,
position: 'relative',
flexDirection: 'row',
alignContent: 'center'
},
draggable: {
position: 'absolute'
},
track: {
position: 'absolute',
// FIXME: need to fix position
// needs to be 50% - (height/2)
top: '50%',
...Platform.select({
ios: {
height: 2,
borderRadius: 2
},
android: {
height: 2
}
})
},
mainTrack: {
left: 0,
right: 0,
...Platform.select({
default: {
backgroundColor: '#A7A7A7'
},
android: {
backgroundColor: '#CECECE'
}
})
},
selectedTrack: {
...Platform.select({
default: {
backgroundColor: '#095FFF'
},
android: {
backgroundColor: '#0D8675'
}
})
}
})
const HALF_MARKER = (MARKER_DIMENSION / 2)
const Slider = (props: Props) => {
const [firstValue] = useState(() => new Value(0 as number))
const [secondValue] = useState(() => new Value(0 as number))
const [measure, setMeasure] = useState<
{ width: number, height: number } | null
>(null)
useCode(() => {
if(!measure) {
return false
}
const interpolateConfig = ({
inputRange: [0, measure.width - HALF_MARKER],
outputRange: [0, 100],
extrapolate: Extrapolate.CLAMP
})
const notifyDeps = [firstValue, secondValue]
.map(it => round(
interpolate(it, interpolateConfig)
))
return call(notifyDeps, ([first, second]) => {
console.log(first, second)
})
}, [firstValue, secondValue, measure])
const renderMarkers = () => {
if(!measure) {
return
}
const max = measure.width - HALF_MARKER
// eslint-disable-next-line no-magic-numbers
return (
<>
<XDraggable
value={ firstValue }
style={ styles.draggable }
axisMax={ max }
axisMin={ 0 }>
<Label
style={ {
position: 'absolute',
right: -1,
bottom: LABEL_HEIGHT
} }
/>
<Marker
enabled
pressed={ false }
/>
</XDraggable>
<XDraggable
value={ secondValue }
style={ styles.draggable }
axisMax={ max }
axisMin={ 0 }>
<Marker
enabled
pressed={ false }
/>
</XDraggable>
</>
)
}
const handleLayout = useCallback((event: LayoutChangeEvent) => {
const { height, width } = event.nativeEvent.layout
setMeasure({ width, height })
}, [])
return (
<View style={ styles.container }>
<View
style={ styles.fullTrack }>
<View
onLayout={ handleLayout }
style={ StyleSheet.absoluteFill }>
<View style={ [styles.track, styles.mainTrack] } />
</View>
<Animated.View
style={ [
styles.track,
styles.selectedTrack,
{
left: min(firstValue, secondValue),
width: sub(
max(firstValue, secondValue),
min(firstValue, secondValue),
)
}
] }
/>
{ renderMarkers() }
</View>
</View>
)
}
export default Slider
/* eslint-disable no-magic-numbers */
import React, { useState } from 'react'
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
State
} from 'react-native-gesture-handler'
import Animated, {
add,
block,
cond,
eq,
event,
greaterOrEq,
lessOrEq,
proc,
set,
Value
} from 'react-native-reanimated'
import { ViewStyle } from 'react-native'
type Props = {
style?: ViewStyle
children: React.ReactNode
value: Animated.Value<number>
axisMax: number
axisMin: number
}
const clamp = proc((
value: Animated.Adaptable<number>,
min: Animated.Adaptable<number>,
max: Animated.Adaptable<number>
) => cond(
lessOrEq(value, min),
min,
cond(greaterOrEq(value, max), max, value)),
)
const XDraggable = (props: Props) => {
const [offsetX] = useState(() => new Value(0))
const handlePan = event([{
nativeEvent: ({
translationX: x,
state
}: PanGestureHandlerGestureEvent['nativeEvent']) => {
return block([
set(props.value, clamp(
add(x, offsetX),
props.axisMin,
props.axisMax
)),
cond(eq(state, State.END), [
set(offsetX, add(offsetX, x))
])
])
}
}])
return (
<PanGestureHandler
maxPointers={ 1 }
onGestureEvent={ handlePan }
onHandlerStateChange={ handlePan }>
<Animated.View
style={ [{
transform: [{
translateX: props.value
}]
}, props.style] }>
{ props.children }
</Animated.View>
</PanGestureHandler>
)
}
export default XDraggable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment