├── .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 |      
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 |
--------------------------------------------------------------------------------