UNPKG

react-native-color-wheel

Version:

A react native reusable and color picker wheel

242 lines (219 loc) 6.36 kB
// @flow import React, {Component} from 'react' import { Animated, Image, Dimensions, PanResponder, StyleSheet, View, } from 'react-native' import colorsys from 'colorsys' export class ColorWheel extends Component { static defaultProps = { thumbSize: 50, initialColor: '#ffffff', onColorChange: () => {}, } constructor (props) { super(props) this.state = { offset: {x: 0, y: 0}, currentColor: props.initialColor, pan: new Animated.ValueXY(), } } componentDidMount = () => { this._panResponder = PanResponder.create({ onStartShouldSetPanResponderCapture: ({nativeEvent}) => { if (this.outBounds(nativeEvent)) return this.updateColor({nativeEvent}) this.setState({panHandlerReady: true}) this.state.pan.setValue({ x: -this.state.left + nativeEvent.pageX - this.props.thumbSize / 2, y: -this.state.top + nativeEvent.pageY - this.props.thumbSize / 2, }) return true }, onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponderCapture: () => true, onPanResponderGrant: () => true, onPanResponderMove: (event, gestureState) => { if (this.outBounds(gestureState)) return this.resetPanHandler() return Animated.event( [ null, { dx: this.state.pan.x, dy: this.state.pan.y, }, ], {listener: this.updateColor} )(event, gestureState) }, onMoveShouldSetPanResponder: () => true, onPanResponderRelease: ({nativeEvent}) => { this.setState({panHandlerReady: true}) this.state.pan.flattenOffset() const {radius} = this.calcPolar(nativeEvent) if (radius < 0.1) { this.forceUpdate('#ffffff') } if (this.props.onColorChangeComplete) { this.props.onColorChangeComplete(this.state.hsv); } }, }) } onLayout () { this.measureOffset() } measureOffset () { /* * const {x, y, width, height} = nativeEvent.layout * onlayout values are different than measureInWindow * x and y are the distances to its previous element * but in measureInWindow they are relative to the window */ this.self.measureInWindow((x, y, width, height) => { const window = Dimensions.get('window') const absX = x % width const radius = Math.min(width, height) / 2 const offset = { x: absX + width / 2, y: y % window.height + height / 2, } this.setState({ offset, radius, height, width, top: y % window.height, left: absX, }) this.forceUpdate(this.state.currentColor) }) } calcPolar (gestureState) { const { pageX, pageY, moveX, moveY, } = gestureState const [x, y] = [pageX || moveX, pageY || moveY] const [dx, dy] = [x - this.state.offset.x, y - this.state.offset.y] return { deg: Math.atan2(dy, dx) * (-180 / Math.PI), // pitagoras r^2 = x^2 + y^2 normalized radius: Math.sqrt(dy * dy + dx * dx) / this.state.radius, } } outBounds (gestureState) { const {radius} = this.calcPolar(gestureState) return radius > 1 } resetPanHandler () { if (!this.state.panHandlerReady) { return } this.setState({panHandlerReady: false}) this.state.pan.setOffset({ x: this.state.pan.x._value, y: this.state.pan.y._value, }) this.state.pan.setValue({x: 0, y: 0}) } calcCartesian (deg, radius) { const r = radius * this.state.radius // was normalized const rad = Math.PI * deg / 180 const x = r * Math.cos(rad) const y = r * Math.sin(rad) return { left: this.state.width / 2 + x, top: this.state.height / 2 - y, } } updateColor = ({nativeEvent}) => { const {deg, radius} = this.calcPolar(nativeEvent) const hsv = {h: deg, s: 100 * radius, v: 100}; const currentColor = colorsys.hsv2Hex(hsv) this.setState({hsv, currentColor}) this.props.onColorChange(hsv); } forceUpdate = color => { const {h, s, v} = colorsys.hex2Hsv(color) const {left, top} = this.calcCartesian(h, s / 100) this.setState({currentColor: color}) this.props.onColorChange({h, s, v}) this.state.pan.setValue({ x: left - this.props.thumbSize / 2, y: top - this.props.thumbSize / 2, }) } animatedUpdate = color => { const {h, s, v} = colorsys.hex2Hsv(color) const {left, top} = this.calcCartesian(h, s / 100) this.setState({currentColor: color}) this.props.onColorChange({h, s, v}) Animated.spring(this.state.pan, { toValue: { x: left - this.props.thumbSize / 2, y: top - this.props.thumbSize / 2, }, }).start() } render () { const {radius} = this.state const thumbStyle = [ styles.circle, this.props.thumbStyle, { width: this.props.thumbSize, height: this.props.thumbSize, borderRadius: this.props.thumbSize / 2, backgroundColor: this.state.currentColor, opacity: this.state.offset.x === 0 ? 0 : 1, }, ] const panHandlers = this._panResponder && this._panResponder.panHandlers || {} return ( <View ref={node => { this.self = node }} {...panHandlers} onLayout={nativeEvent => this.onLayout(nativeEvent)} style={[styles.coverResponder, this.props.style]}> <Image style={[styles.img, { height: radius * 2 - this.props.thumbSize, width: radius * 2 - this.props.thumbSize }]} source={require('./color-wheel.png')} /> <Animated.View style={[this.state.pan.getLayout(), thumbStyle]} /> </View> ) } } const styles = StyleSheet.create({ coverResponder: { flex: 1, alignItems: 'center', justifyContent: 'center', }, img: { alignSelf: 'center', }, circle: { position: 'absolute', backgroundColor: '#EEEEEE', borderWidth: 3, borderColor: '#EEEEEE', elevation: 3, shadowColor: 'rgb(46, 48, 58)', shadowOffset: {width: 0, height: 2}, shadowOpacity: 0.8, shadowRadius: 2, }, })