├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── babel.config.js ├── demo ├── App.js ├── AppEntry.js ├── app.json └── metro.config.js ├── package-lock.json ├── package.json ├── setup-tests.js ├── src ├── HsvColorPicker.js ├── HuePicker.js ├── SaturationValuePicker.js ├── index.js └── utils.js └── test ├── __snapshots__ └── index.test.js.snap └── index.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | parser: 'babel-eslint', 5 | extends: 'airbnb', 6 | plugins: [ 7 | 'react', 8 | 'react-native' 9 | ], 10 | env: { 11 | 'jest': true, 12 | 'react-native/react-native': true 13 | }, 14 | rules: { 15 | // allow js file extension 16 | 'react/jsx-filename-extension': [ 17 | 'error', 18 | { 19 | extensions: ['.js', '.jsx'] 20 | } 21 | ], 22 | // for post defining style object in react-native 23 | 'no-use-before-define': ['error', { variables: false }], 24 | // react-native rules 25 | 'react-native/no-unused-styles': 2, 26 | 'react-native/split-platform-components': 2, 27 | 'react-native/no-inline-styles': 2, 28 | 'react-native/no-raw-text': 2, 29 | // allow devDependencies 30 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | npm-debug.* 3 | yarn-error.log 4 | node_modules 5 | 6 | # mac 7 | .DS_Store 8 | 9 | # windows 10 | Thumbs.db 11 | 12 | # webstorm 13 | .idea/ 14 | 15 | # jest 16 | coverage/ 17 | 18 | # expo 19 | .expo/* 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-hsv-color-picker 3 | > a react native HSV(hue, saturation, value) color picker 4 | 5 | ![npm](https://img.shields.io/npm/v/react-native-hsv-color-picker.svg?style=flat-square) ![](https://img.shields.io/travis/yuanfux/react-native-hsv-color-picker/master.svg?style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/yuanfux/react-native-hsv-color-picker.svg?style=flat-square) ![NPM](https://img.shields.io/npm/l/react-native-hsv-color-picker.svg?style=flat-square) ![](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) ![](https://img.shields.io/maintenance/yes/2021.svg?style=flat-square) 6 | 7 |

8 | 9 |

10 | 11 | ## Preview 12 | [View Live Demo](https://snack.expo.io/@fuyuanx/react-native-hsv-color-picker) 13 | 14 | `react-native-hsv-color-picker` is a React Native component for building an [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) (hue, saturation, value) color picker. 15 | 16 | Highlighted Features: 17 | 1. **Real Rendering** - no image involved / all colors are truly rendered 18 | 2. **Performance** - empowered by native gradient lib 19 | 4. **Fully Controlled** - no inner state involved 20 | 3. **Fully Supported** - support both React Native & Expo projects 21 | 22 | ## Install 23 | ```bash 24 | $ npm install react-native-hsv-color-picker --save 25 | ``` 26 | 27 | ### Use with Expo Project 28 | > You are all set. 29 | 30 | ### Use with React Native Project 31 | > `react-native-hsv-color-picker` is powered by the lib [`expo-linear-gradient`](https://github.com/react-native-community/react-native-linear-gradient). Besides above command, you have to follow this [Instruction](https://github.com/expo/expo/tree/master/packages/expo-linear-gradient#installation-in-bare-react-native-projects) to add relevant dependencies to your project. 32 | 33 | ## Usage 34 | > a minimally-configured HSV color picker 35 | ```js 36 | import React from 'react'; 37 | import { StyleSheet, View } from 'react-native'; 38 | import HsvColorPicker from 'react-native-hsv-color-picker'; 39 | 40 | export default class Example extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | hue: 0, 45 | sat: 0, 46 | val: 1, 47 | }; 48 | this.onSatValPickerChange = this.onSatValPickerChange.bind(this); 49 | this.onHuePickerChange = this.onHuePickerChange.bind(this); 50 | } 51 | 52 | onSatValPickerChange({ saturation, value }) { 53 | this.setState({ 54 | sat: saturation, 55 | val: value, 56 | }); 57 | } 58 | 59 | onHuePickerChange({ hue }) { 60 | this.setState({ 61 | hue, 62 | }); 63 | } 64 | 65 | render() { 66 | const { hue, sat, val } = this.state; 67 | return ( 68 | 69 | 79 | 80 | ); 81 | } 82 | } 83 | 84 | const styles = StyleSheet.create({ 85 | container: { 86 | flex: 1, 87 | backgroundColor: '#fff', 88 | alignItems: 'center', 89 | justifyContent: 'center', 90 | }, 91 | }); 92 | ``` 93 | 94 | 95 | ## Props 96 | #### Basic Props 97 | | Prop | Type | Default | Description | 98 | |--|--|--| -- | 99 | | `containerStyle` | ViewPropTypes.style | `{}` | style for the outmost container | 100 | | `huePickerContainerStyle` | ViewPropTypes.style | `{}` | style for the hue picker container | 101 | | `huePickerBorderRadius` | number | `0` | border radius for the hue picker | 102 | | `huePickerHue` | number | `0` | hue value(`h` in `hsv`, ranged in `[0, 360]`) for the hue picker | 103 | | `huePickerBarWidth` | number | `12` | bar width for the hue picker | 104 | | `huePickerBarHeight` | number | `200` | bar height for the hue picker | 105 | | `huePickerSliderSize` | number | `24` | slider diameter for the hue picker | 106 | | `satValPickerContainerStyle` | ViewPropTypes.style | `{}` | style for the saturation & value picker container | 107 | | `satValPickerBorderRadius` | number | `0` | border radius for the saturation & value picker | 108 | | `satValPickerSize` | number | `200` | width / height for the saturation & value picker | 109 | | `satValPickerSliderSize` | number | `24` | slider diameter for the saturation & value picker | 110 | | `satValPickerHue` | number | `0` | hue value(`h` in `hsv`, ranged in `[0, 360]`) for the saturation & value picker | 111 | | `satValPickerSaturation` | number | `1` | saturation value(`s` in `hsv`, ranged in `[0, 1]`) for the saturation & value picker | 112 | | `satValPickerValue` | number | `1` | value(`v` in `hsv`, ranged in `[0, 1]`) for the saturation & value picker | 113 | 114 | #### Callback Props 115 | | Prop | Callback Params | Description | 116 | |--|--| -- | 117 | | `onHuePickerDragStart` | {
    hue: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when hue picker starts to drag | 118 | | `onHuePickerDragMove` | {
    hue: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when hue picker is dragging | 119 | | `onHuePickerDragEnd` | {
    hue: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when hue picker stops dragging | 120 | | `onHuePickerDragTerminate` | {
    hue: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when another component has become the responder | 121 | | `onHuePickerPress` | {
    hue: number,
    nativeEvent: [nativeEvent](https://facebook.github.io/react-native/docs/panresponder)
} | called when hue picker is pressed | 122 | | `onSatValPickerDragStart` | {
    saturation: number,
    value: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when saturation & value picker starts to drag | 123 | | `onSatValPickerDragMove` | {
    saturation: number,
    value: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when saturation & value picker is dragging | 124 | | `onSatValPickerDragEnd` | {
    saturation: number,
    value: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when saturation & value picker stops dragging | 125 | | `onSatValPickerDragTerminate` | {
    saturation: number,
    value: number,
    gestureState: [gestureState](https://facebook.github.io/react-native/docs/panresponder)
} | called when another component has become the responder | 126 | | `onSatValPickerPress` | {
    saturation: number,
    value: number,
    nativeEvent: [nativeEvent](https://facebook.github.io/react-native/docs/panresponder)
} | called when saturation & value picker is pressed | 127 | 128 | ## Methods 129 | #### Instance Methods 130 | > Use [`ref`](https://facebook.github.io/react/docs/refs-and-the-dom.html) to call instance methods 131 | 132 | | Method | Params | Return Type| Description | 133 | |--|:--:|:--:| -- | 134 | | `getCurrentColor` | - | `string` | get current picked color in hex format | 135 | 136 | 137 | 138 | ## Dev 139 | > The `demo` folder contains a standalone Expo project, which can be used for dev purpose. 140 | 141 | > react-native - 0.61
142 | > react - 16.9 143 | 144 | 1. Start Expo 145 | ```bash 146 | $ npm install 147 | 148 | $ npm start 149 | ``` 150 | 151 | 2. Run on `simulator` 152 | - type the following command in the `Terminal` after the project is booted up 153 | - `a` for `android` simulator 154 | - `i` for `iOS` simulator 155 | 156 | 3. Run on `device` 157 | - require the installation of corresponding [`iOS client`](https://itunes.apple.com/app/apple-store/id982107779) or [`android client`](https://play.google.com/store/apps/details?id=host.exp.exponent&referrer=www) on the device 158 | - scan the QR code from `Terminal` using the device 159 | 160 | 4. More on [`Expo Guide`](https://docs.expo.io/versions/v36.0.0/) 161 | 162 | ## Related 163 | - scaffolded by [**react-native-component-cli**](https://github.com/yuanfux/react-native-component-cli) 164 | 165 | ## License 166 | MIT 167 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | const isBabelJest = api.caller(caller => caller && caller.name === 'babel-jest'); 3 | if (isBabelJest) { 4 | return { 5 | presets: [ 6 | 'module:metro-react-native-babel-preset', 7 | ], 8 | }; 9 | } 10 | return { 11 | presets: ['babel-preset-expo'], 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /demo/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import HsvColorPicker from '../src'; 4 | 5 | export default class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | hue: 0, 10 | sat: 0, 11 | val: 1, 12 | }; 13 | this.onSatValPickerChange = this.onSatValPickerChange.bind(this); 14 | this.onHuePickerChange = this.onHuePickerChange.bind(this); 15 | this.hsvColorPicker = React.createRef(); 16 | } 17 | 18 | onSatValPickerChange({ saturation, value }) { 19 | this.setState({ 20 | sat: saturation, 21 | val: value, 22 | }); 23 | } 24 | 25 | onHuePickerChange({ hue }) { 26 | this.setState({ 27 | hue, 28 | }); 29 | } 30 | 31 | render() { 32 | const { hue, sat, val } = this.state; 33 | return ( 34 | 35 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | const styles = StyleSheet.create({ 51 | container: { 52 | flex: 1, 53 | backgroundColor: '#fff', 54 | alignItems: 'center', 55 | justifyContent: 'center', 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /demo/AppEntry.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | import { activateKeepAwake } from 'expo-keep-awake'; 3 | 4 | import App from './App'; 5 | 6 | if (__DEV__) { 7 | activateKeepAwake(); 8 | } 9 | 10 | registerRootComponent(App); 11 | -------------------------------------------------------------------------------- /demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-demo-app", 4 | "slug": "react-native-demo-app", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "entryPoint": "demo/AppEntry.js", 8 | "platforms": [ 9 | "ios", 10 | "android" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "splash": { 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "updates": { 18 | "fallbackToCacheTimeout": 0 19 | }, 20 | "assetBundlePatterns": [ 21 | "**/*" 22 | ], 23 | "ios": { 24 | "supportsTablet": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | projectRoot: path.resolve(__dirname), 5 | watchFolders: [ 6 | path.resolve(__dirname, '..', 'src'), 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-hsv-color-picker", 3 | "version": "1.0.2", 4 | "description": "a react native HSV(hue, saturation, value) color picker", 5 | "author": "Yuan Fu", 6 | "bugs": { 7 | "url": "https://github.com/yuanfux/react-native-hsv-color-picker/issues" 8 | }, 9 | "homepage": "https://github.com/yuanfux/react-native-hsv-color-picker", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/yuanfux/react-native-hsv-color-picker.git" 13 | }, 14 | "keywords": [ 15 | "react native", 16 | "react-native", 17 | "color", 18 | "picker", 19 | "color picker", 20 | "hsv color picker" 21 | ], 22 | "main": "src/index.js", 23 | "files": [ 24 | "src/*" 25 | ], 26 | "dependencies": { 27 | "chroma-js": "^2.1.0", 28 | "expo-linear-gradient": "^8.0.0" 29 | }, 30 | "peerDependencies": { 31 | "prop-types": "^15.7.2" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.2.2", 35 | "@babel/runtime": "^7.3.1", 36 | "babel-eslint": "^10.0.1", 37 | "babel-jest": "^24.0.0", 38 | "babel-preset-expo": "~8.0.0", 39 | "enzyme": "^3.10.0", 40 | "enzyme-adapter-react-16": "^1.14.0", 41 | "enzyme-to-json": "^3.3.5", 42 | "escape-regex-string": "^1.0.6", 43 | "eslint": "^5.13.0", 44 | "eslint-config-airbnb": "^17.1.0", 45 | "eslint-plugin-import": "^2.16.0", 46 | "eslint-plugin-jsx-a11y": "^6.2.1", 47 | "eslint-plugin-react": "^7.12.4", 48 | "eslint-plugin-react-native": "^3.6.0", 49 | "expo": "~36.0.0", 50 | "expo-keep-awake": "^8.0.0", 51 | "jest": "^24.0.0", 52 | "jest-enzyme": "^7.0.2", 53 | "jest-expo": "^36.0.1", 54 | "jsdom": "^15.1.1", 55 | "react": "~16.9.0", 56 | "react-dom": "~16.9.0", 57 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.1.tar.gz" 58 | }, 59 | "scripts": { 60 | "start": "npx expo-cli start --config \"./demo/app.json\" -c", 61 | "eject": "npx expo-cli eject --config \"./demo/app.json\"", 62 | "test:lint": "npx eslint \"src/*.js\"", 63 | "test": "npm run test:lint && npx jest" 64 | }, 65 | "jest": { 66 | "preset": "jest-expo", 67 | "snapshotSerializers": [ 68 | "enzyme-to-json/serializer" 69 | ], 70 | "setupFilesAfterEnv": [ 71 | "/setup-tests.js" 72 | ], 73 | "modulePathIgnorePatterns": [ 74 | "/demo/" 75 | ], 76 | "collectCoverage": true 77 | }, 78 | "license": "MIT" 79 | } 80 | -------------------------------------------------------------------------------- /setup-tests.js: -------------------------------------------------------------------------------- 1 | // setup-tests.js 2 | 3 | import 'react-native'; 4 | import 'jest-enzyme'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | import Enzyme from 'enzyme'; 7 | 8 | /** 9 | * Set up DOM in node.js environment for Enzyme to mount to 10 | */ 11 | const { JSDOM } = require('jsdom'); 12 | 13 | const jsdom = new JSDOM(''); 14 | const { window } = jsdom; 15 | 16 | function copyProps(src, target) { 17 | const props = Object.getOwnPropertyNames(src) 18 | .filter(prop => typeof target[prop] === 'undefined') 19 | .map(prop => Object.getOwnPropertyDescriptor(src, prop)); 20 | Object.defineProperties(target, props); 21 | } 22 | 23 | global.window = window; 24 | global.document = window.document; 25 | global.navigator = { 26 | userAgent: 'node.js', 27 | }; 28 | copyProps(window, global); 29 | 30 | /** 31 | * Set up Enzyme to mount to DOM, simulate events, 32 | * and inspect the DOM in tests. 33 | */ 34 | Enzyme.configure({ adapter: new Adapter() }); 35 | 36 | // Ignore React Web errors when using React Native 37 | console.error = message => message; 38 | -------------------------------------------------------------------------------- /src/HsvColorPicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | ViewPropTypes, 5 | StyleSheet, 6 | } from 'react-native'; 7 | import PropTypes from 'prop-types'; 8 | import HuePicker from './HuePicker'; 9 | import SaturationValuePicker from './SaturationValuePicker'; 10 | 11 | export default class HsvColorPicker extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.satValPicker = React.createRef(); 15 | } 16 | 17 | getCurrentColor() { 18 | return this.satValPicker.current.getCurrentColor(); 19 | } 20 | 21 | render() { 22 | const { 23 | containerStyle, 24 | huePickerContainerStyle, 25 | huePickerBorderRadius, 26 | huePickerHue, 27 | huePickerBarWidth, 28 | huePickerBarHeight, 29 | huePickerSliderSize, 30 | onHuePickerDragStart, 31 | onHuePickerDragMove, 32 | onHuePickerDragEnd, 33 | onHuePickerDragTerminate, 34 | onHuePickerPress, 35 | satValPickerContainerStyle, 36 | satValPickerBorderRadius, 37 | satValPickerSize, 38 | satValPickerSliderSize, 39 | satValPickerHue, 40 | satValPickerSaturation, 41 | satValPickerValue, 42 | onSatValPickerDragStart, 43 | onSatValPickerDragMove, 44 | onSatValPickerDragEnd, 45 | onSatValPickerDragTerminate, 46 | onSatValPickerPress, 47 | } = this.props; 48 | return ( 49 | 50 | 65 | 78 | 79 | ); 80 | } 81 | } 82 | 83 | const styles = StyleSheet.create({ 84 | container: { 85 | flexDirection: 'row', 86 | justifyContent: 'center', 87 | alignItems: 'center', 88 | }, 89 | }); 90 | 91 | HsvColorPicker.propTypes = { 92 | containerStyle: ViewPropTypes.style, 93 | huePickerContainerStyle: ViewPropTypes.style, 94 | huePickerBorderRadius: PropTypes.number, 95 | huePickerHue: PropTypes.number, 96 | huePickerBarWidth: PropTypes.number, 97 | huePickerBarHeight: PropTypes.number, 98 | huePickerSliderSize: PropTypes.number, 99 | onHuePickerDragStart: PropTypes.func, 100 | onHuePickerDragMove: PropTypes.func, 101 | onHuePickerDragEnd: PropTypes.func, 102 | onHuePickerDragTerminate: PropTypes.func, 103 | onHuePickerPress: PropTypes.func, 104 | satValPickerContainerStyle: ViewPropTypes.style, 105 | satValPickerBorderRadius: PropTypes.number, 106 | satValPickerSize: PropTypes.number, 107 | satValPickerSliderSize: PropTypes.number, 108 | satValPickerHue: PropTypes.number, 109 | satValPickerSaturation: PropTypes.number, 110 | satValPickerValue: PropTypes.number, 111 | onSatValPickerDragStart: PropTypes.func, 112 | onSatValPickerDragMove: PropTypes.func, 113 | onSatValPickerDragEnd: PropTypes.func, 114 | onSatValPickerDragTerminate: PropTypes.func, 115 | onSatValPickerPress: PropTypes.func, 116 | }; 117 | 118 | HsvColorPicker.defaultProps = { 119 | containerStyle: {}, 120 | huePickerContainerStyle: {}, 121 | huePickerBorderRadius: 0, 122 | huePickerHue: 0, 123 | huePickerBarWidth: 12, 124 | huePickerBarHeight: 200, 125 | huePickerSliderSize: 24, 126 | onHuePickerDragStart: null, 127 | onHuePickerDragMove: null, 128 | onHuePickerDragEnd: null, 129 | onHuePickerDragTerminate: null, 130 | onHuePickerPress: null, 131 | satValPickerContainerStyle: {}, 132 | satValPickerBorderRadius: 0, 133 | satValPickerSize: 200, 134 | satValPickerSliderSize: 24, 135 | satValPickerHue: 0, 136 | satValPickerSaturation: 1, 137 | satValPickerValue: 1, 138 | onSatValPickerDragStart: null, 139 | onSatValPickerDragMove: null, 140 | onSatValPickerDragEnd: null, 141 | onSatValPickerDragTerminate: null, 142 | onSatValPickerPress: null, 143 | }; 144 | -------------------------------------------------------------------------------- /src/HuePicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Animated, 4 | View, 5 | TouchableWithoutFeedback, 6 | ViewPropTypes, 7 | PanResponder, 8 | StyleSheet, 9 | } from 'react-native'; 10 | import { LinearGradient } from 'expo-linear-gradient'; 11 | import PropTypes from 'prop-types'; 12 | import chroma from 'chroma-js'; 13 | import normalizeValue from './utils'; 14 | 15 | export default class HuePicker extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.hueColors = [ 19 | '#ff0000', 20 | '#ffff00', 21 | '#00ff00', 22 | '#00ffff', 23 | '#0000ff', 24 | '#ff00ff', 25 | '#ff0000', 26 | ]; 27 | this.firePressEvent = this.firePressEvent.bind(this); 28 | this.sliderY = new Animated.Value(props.barHeight * props.hue / 360); 29 | this.panResponder = PanResponder.create({ 30 | onStartShouldSetPanResponder: () => true, 31 | onStartShouldSetPanResponderCapture: () => true, 32 | onMoveShouldSetPanResponder: () => true, 33 | onMoveShouldSetPanResponderCapture: () => true, 34 | onPanResponderGrant: (evt, gestureState) => { 35 | const { hue } = this.props; 36 | this.dragStartValue = hue; 37 | this.fireDragEvent('onDragStart', gestureState); 38 | }, 39 | onPanResponderMove: (evt, gestureState) => { 40 | this.fireDragEvent('onDragMove', gestureState); 41 | }, 42 | onPanResponderTerminationRequest: () => true, 43 | onPanResponderRelease: (evt, gestureState) => { 44 | this.fireDragEvent('onDragEnd', gestureState); 45 | }, 46 | onPanResponderTerminate: (evt, gestureState) => { 47 | this.fireDragEvent('onDragTerminate', gestureState); 48 | }, 49 | onShouldBlockNativeResponder: () => true, 50 | }); 51 | } 52 | 53 | componentDidUpdate(prevProps) { 54 | const { hue, barHeight } = this.props; 55 | if ( 56 | prevProps.hue !== hue 57 | || prevProps.barHeight !== barHeight 58 | ) { 59 | this.sliderY.setValue(barHeight * hue / 360); 60 | } 61 | } 62 | 63 | getContainerStyle() { 64 | const { sliderSize, barWidth, containerStyle } = this.props; 65 | const paddingTop = sliderSize / 2; 66 | const paddingLeft = sliderSize - barWidth > 0 ? (sliderSize - barWidth) / 2 : 0; 67 | return [ 68 | styles.container, 69 | containerStyle, 70 | { 71 | paddingTop, 72 | paddingBottom: paddingTop, 73 | paddingLeft, 74 | paddingRight: paddingLeft, 75 | }, 76 | ]; 77 | } 78 | 79 | getCurrentColor() { 80 | const { hue } = this.props; 81 | return chroma.hsl(hue, 1, 0.5).hex(); 82 | } 83 | 84 | computeHueValueDrag(gestureState) { 85 | const { dy } = gestureState; 86 | const { barHeight } = this.props; 87 | const { dragStartValue } = this; 88 | const diff = dy / barHeight; 89 | const updatedHue = normalizeValue(dragStartValue / 360 + diff) * 360; 90 | return updatedHue; 91 | } 92 | 93 | computeHueValuePress(event) { 94 | const { nativeEvent } = event; 95 | const { locationY } = nativeEvent; 96 | const { barHeight } = this.props; 97 | const updatedHue = normalizeValue(locationY / barHeight) * 360; 98 | return updatedHue; 99 | } 100 | 101 | fireDragEvent(eventName, gestureState) { 102 | const { [eventName]: event } = this.props; 103 | if (event) { 104 | event({ 105 | hue: this.computeHueValueDrag(gestureState), 106 | gestureState, 107 | }); 108 | } 109 | } 110 | 111 | firePressEvent(event) { 112 | const { onPress } = this.props; 113 | if (onPress) { 114 | onPress({ 115 | hue: this.computeHueValuePress(event), 116 | nativeEvent: event.nativeEvent, 117 | }); 118 | } 119 | } 120 | 121 | render() { 122 | const { hueColors } = this; 123 | const { 124 | sliderSize, 125 | barWidth, 126 | barHeight, 127 | borderRadius, 128 | } = this.props; 129 | return ( 130 | 131 | 132 | 138 | 142 | 143 | 144 | 160 | 161 | ); 162 | } 163 | } 164 | 165 | const styles = StyleSheet.create({ 166 | container: { 167 | justifyContent: 'center', 168 | alignItems: 'center', 169 | }, 170 | slider: { 171 | top: 0, 172 | position: 'absolute', 173 | borderColor: '#fff', 174 | }, 175 | }); 176 | 177 | HuePicker.propTypes = { 178 | containerStyle: ViewPropTypes.style, 179 | borderRadius: PropTypes.number, 180 | hue: PropTypes.number, 181 | barWidth: PropTypes.number, 182 | barHeight: PropTypes.number, 183 | sliderSize: PropTypes.number, 184 | onDragStart: PropTypes.func, 185 | onDragMove: PropTypes.func, 186 | onDragEnd: PropTypes.func, 187 | onDragTerminate: PropTypes.func, 188 | onPress: PropTypes.func, 189 | }; 190 | 191 | HuePicker.defaultProps = { 192 | containerStyle: {}, 193 | borderRadius: 0, 194 | hue: 0, 195 | barWidth: 12, 196 | barHeight: 200, 197 | sliderSize: 24, 198 | onDragStart: null, 199 | onDragMove: null, 200 | onDragEnd: null, 201 | onDragTerminate: null, 202 | onPress: null, 203 | }; 204 | -------------------------------------------------------------------------------- /src/SaturationValuePicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | View, 4 | TouchableWithoutFeedback, 5 | ViewPropTypes, 6 | PanResponder, 7 | StyleSheet, 8 | } from 'react-native'; 9 | import { LinearGradient } from 'expo-linear-gradient'; 10 | import PropTypes from 'prop-types'; 11 | import chroma from 'chroma-js'; 12 | import normalizeValue from './utils'; 13 | 14 | export default class SaturationValuePicker extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.firePressEvent = this.firePressEvent.bind(this); 18 | this.panResponder = PanResponder.create({ 19 | onStartShouldSetPanResponder: () => true, 20 | onStartShouldSetPanResponderCapture: () => true, 21 | onMoveShouldSetPanResponder: () => true, 22 | onMoveShouldSetPanResponderCapture: () => true, 23 | onPanResponderGrant: (evt, gestureState) => { 24 | const { saturation, value } = this.props; 25 | this.dragStartValue = { 26 | saturation, 27 | value, 28 | }; 29 | this.fireDragEvent('onDragStart', gestureState); 30 | }, 31 | onPanResponderMove: (evt, gestureState) => { 32 | this.fireDragEvent('onDragMove', gestureState); 33 | }, 34 | onPanResponderTerminationRequest: () => true, 35 | onPanResponderRelease: (evt, gestureState) => { 36 | this.fireDragEvent('onDragEnd', gestureState); 37 | }, 38 | onPanResponderTerminate: (evt, gestureState) => { 39 | this.fireDragEvent('onDragTerminate', gestureState); 40 | }, 41 | onShouldBlockNativeResponder: () => true, 42 | }); 43 | } 44 | 45 | getCurrentColor() { 46 | const { hue, saturation, value } = this.props; 47 | return chroma.hsv( 48 | hue, 49 | saturation, 50 | value, 51 | ).hex(); 52 | } 53 | 54 | computeSatValDrag(gestureState) { 55 | const { dx, dy } = gestureState; 56 | const { size } = this.props; 57 | const { saturation, value } = this.dragStartValue; 58 | const diffx = dx / size; 59 | const diffy = dy / size; 60 | return { 61 | saturation: normalizeValue(saturation + diffx), 62 | value: normalizeValue(value - diffy), 63 | }; 64 | } 65 | 66 | computeSatValPress(event) { 67 | const { nativeEvent } = event; 68 | const { locationX, locationY } = nativeEvent; 69 | const { size } = this.props; 70 | return { 71 | saturation: normalizeValue(locationX / size), 72 | value: 1 - normalizeValue(locationY / size), 73 | }; 74 | } 75 | 76 | fireDragEvent(eventName, gestureState) { 77 | const { [eventName]: event } = this.props; 78 | if (event) { 79 | event({ 80 | ...this.computeSatValDrag(gestureState), 81 | gestureState, 82 | }); 83 | } 84 | } 85 | 86 | firePressEvent(event) { 87 | const { onPress } = this.props; 88 | if (onPress) { 89 | onPress({ 90 | ...this.computeSatValPress(event), 91 | nativeEvent: event.nativeEvent, 92 | }); 93 | } 94 | } 95 | 96 | render() { 97 | const { 98 | size, 99 | sliderSize, 100 | hue, 101 | value, 102 | saturation, 103 | containerStyle, 104 | borderRadius, 105 | } = this.props; 106 | return ( 107 | 117 | 118 | 127 | 133 | 139 | 140 | 141 | 142 | 159 | 160 | ); 161 | } 162 | } 163 | 164 | const styles = StyleSheet.create({ 165 | container: { 166 | justifyContent: 'center', 167 | alignItems: 'center', 168 | }, 169 | slider: { 170 | top: 0, 171 | left: 0, 172 | position: 'absolute', 173 | borderColor: '#fff', 174 | }, 175 | linearGradient: { 176 | overflow: 'hidden', 177 | }, 178 | }); 179 | 180 | SaturationValuePicker.propTypes = { 181 | containerStyle: ViewPropTypes.style, 182 | borderRadius: PropTypes.number, 183 | size: PropTypes.number, 184 | sliderSize: PropTypes.number, 185 | hue: PropTypes.number, 186 | saturation: PropTypes.number, 187 | value: PropTypes.number, 188 | onDragStart: PropTypes.func, 189 | onDragMove: PropTypes.func, 190 | onDragEnd: PropTypes.func, 191 | onDragTerminate: PropTypes.func, 192 | onPress: PropTypes.func, 193 | }; 194 | 195 | SaturationValuePicker.defaultProps = { 196 | containerStyle: {}, 197 | borderRadius: 0, 198 | size: 200, 199 | sliderSize: 24, 200 | hue: 0, 201 | saturation: 1, 202 | value: 1, 203 | onDragStart: null, 204 | onDragMove: null, 205 | onDragEnd: null, 206 | onDragTerminate: null, 207 | onPress: null, 208 | }; 209 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import HsvColorPicker from './HsvColorPicker'; 2 | 3 | export default HsvColorPicker; 4 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const normalizeValue = (value) => { 2 | if (value < 0) return 0; 3 | if (value > 1) return 1; 4 | return value; 5 | }; 6 | 7 | export default normalizeValue; 8 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders correctly 1`] = ` 4 | 30 | 42 | 54 | 68 | 83 | 98 | 101 | 140 | 179 | ) 180 | accessible={true} 181 | colors={ 182 | Array [ 183 | 4294967295, 184 | 4294901760, 185 | ] 186 | } 187 | endPoint={ 188 | Array [ 189 | 1, 190 | 0.5, 191 | ] 192 | } 193 | focusable={true} 194 | onClick={[Function]} 195 | onResponderGrant={[Function]} 196 | onResponderMove={[Function]} 197 | onResponderRelease={[Function]} 198 | onResponderTerminate={[Function]} 199 | onResponderTerminationRequest={[Function]} 200 | onStartShouldSetResponder={[Function]} 201 | startPoint={ 202 | Array [ 203 | 0, 204 | 0.5, 205 | ] 206 | } 207 | style={ 208 | Array [ 209 | Object { 210 | "borderRadius": 0, 211 | }, 212 | Object { 213 | "overflow": "hidden", 214 | }, 215 | ] 216 | } 217 | > 218 | 256 | 294 | 302 | 310 | ) 311 | colors={ 312 | Array [ 313 | 0, 314 | 4278190080, 315 | ] 316 | } 317 | > 318 | 331 | 344 | 352 | 360 | 361 | 362 | 363 | )> 364 | 365 | 366 | 367 | 368 | )> 369 | 370 | 371 | 372 | 411 | 450 | 451 | 452 | 453 | 454 | 467 | 484 | 501 | 504 | 531 | 558 | ) 559 | accessible={true} 560 | colors={ 561 | Array [ 562 | 4294901760, 563 | 4294967040, 564 | 4278255360, 565 | 4278255615, 566 | 4278190335, 567 | 4294902015, 568 | 4294901760, 569 | ] 570 | } 571 | focusable={true} 572 | onClick={[Function]} 573 | onResponderGrant={[Function]} 574 | onResponderMove={[Function]} 575 | onResponderRelease={[Function]} 576 | onResponderTerminate={[Function]} 577 | onResponderTerminationRequest={[Function]} 578 | onStartShouldSetResponder={[Function]} 579 | style={ 580 | Object { 581 | "borderRadius": 0, 582 | } 583 | } 584 | > 585 | 617 | 649 | 657 | 665 | 666 | 667 | 668 | )> 669 | 670 | 671 | 672 | 707 | 738 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | `; 778 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import HsvColorPicker from '../src'; 4 | 5 | describe('', () => { 6 | test('renders correctly', () => { 7 | const wrapper = mount(); 8 | expect(wrapper).toMatchSnapshot(); 9 | }); 10 | }); 11 | --------------------------------------------------------------------------------