├── .babelrc
├── .gitignore
├── Gestures.js
├── README.md
├── assets
├── index.css
├── index.html
└── index.js
├── bundle.js
├── index.css
├── index.js
├── package.json
├── test.css
├── test.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"],
3 | "plugins": []
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/Gestures.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropTypes,
3 | Component
4 | } from 'react';
5 |
6 | /**
7 | * Figer Detection, currenty only support MOVE
8 | */
9 | export default class Gestures extends Component {
10 | constructor(props) {
11 | super(props);
12 | this._onTouchStart = this._onTouchStart.bind(this);
13 | this._onTouchMove = this._onTouchMove.bind(this);
14 | this._onTouchCancel = this._onTouchCancel.bind(this);
15 | this._onTouchEnd = this._onTouchEnd.bind(this);
16 | this._emitEvent = this._emitEvent.bind(this);
17 | this.startX = this.startY = this.moveX = this.moveY = null;
18 | }
19 | _emitEvent(eventType,e) {
20 | let eventHandler = this.props[eventType];
21 | if(!eventHandler)return;
22 | eventHandler.call(this,e);
23 | };
24 | _onTouchStart(e) {
25 | let point = e.touches ? e.touches[0] : e;
26 | this.startX = point.pageX;
27 | this.startY = point.pageY;
28 | }
29 | _onTouchMove(e) {
30 | let point = e.touches ? e.touches[0] :e;
31 | let deltaX = this.moveX === null ? 0 : point.pageX - this.moveX;
32 | let deltaY = this.moveY === null ? 0 : point.pageY - this.moveY;
33 | this._emitEvent('onMove',{
34 | deltaX,
35 | deltaY
36 | });
37 | this.moveX = point.pageX;
38 | this.moveY = point.pageY;
39 | e.preventDefault();
40 | }
41 | _onTouchCancel(e) {
42 | this._onTouchEnd();
43 | }
44 | _onTouchEnd(e) {
45 | /**
46 | * 在X轴或Y轴发生过移动
47 | */
48 | if(this.moveX !== null && Math.abs(this.moveX - this.startX) > 10 ||
49 | this.moveY !== null && Math.abs(this.moveY - this.startY) > 10) {
50 | }else {
51 | this._emitEvent('onTap');
52 | }
53 | this.startX = this.startY = this.moveX = this.moveY = null;
54 | }
55 | render() {
56 | return React.cloneElement(React.Children.only(this.props.children), {
57 | onTouchStart: this._onTouchStart.bind(this),
58 | onTouchMove: this._onTouchMove.bind(this),
59 | onTouchCancel: this._onTouchCancel.bind(this),
60 | onTouchEnd: this._onTouchEnd.bind(this)
61 | });
62 | }
63 | }
64 |
65 | Gestures.propTypes = {
66 | onMove: PropTypes.func
67 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 支持手指滑动的 Switch 组件,基于 React。
2 |
3 | [Blog](https://zhuanlan.zhihu.com/p/21573490)
4 |
5 | [在线体验](http://eeandrew.github.io/demos/switch/index.html)
6 |
--------------------------------------------------------------------------------
/assets/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/bundle.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import webpackConfig from './webpack.config.js';
3 |
4 | const bundler = webpack(webpackConfig);
5 |
6 | bundler.run((err,stats)=>{
7 | if(err) {
8 | console.error(err);
9 | }
10 | if(stats) {
11 | console.log(stats);
12 | }
13 | });
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | .switch-wrapper {
2 | height:30px;
3 | background: #FFF;
4 | border-radius: 19px;
5 | border:2px solid #ddd;
6 | position: relative;
7 | box-sizing: border-box;
8 | .switch-togger {
9 | box-sizing: border-box;
10 | position: absolute;
11 | width:26px;
12 | height:26px;
13 | top:0px;
14 | left:0px;
15 | border-radius:50%;
16 | background:#fff;
17 | -webkit-box-shadow: 0 2px 5px rgba(0,0,0,.4);
18 | box-shadow: 0 2px 5px rgba(0,0,0,.4);
19 | }
20 | &.normal {
21 | width:74px;
22 | }
23 | &.small {
24 | width: 47px;
25 | }
26 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react';
5 | import ReactDOM from 'react-dom';
6 | import Gestures from './Gestures';
7 | import classNames from 'classnames';
8 | import './index.css';
9 |
10 | export default class Switch extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.onMove = this.onMove.bind(this);
14 | this.onToggerTouchStart = this.onToggerTouchStart.bind(this);
15 | this.onToggerTouchCancel = this.onToggerTouchCancel.bind(this);
16 | this.setToggerTranslateX = this.setToggerTranslateX.bind(this);
17 | this.enableTransition = this.enableTransition.bind(this);
18 | this.onXTranslateEnd = this.onXTranslateEnd.bind(this);
19 | this.getWrapperStyle = this.getWrapperStyle.bind(this);
20 | this.onMoving = this.onMoving.bind(this);
21 | this.onTap = this.onTap.bind(this);
22 | this.movingEnable = false;
23 | this.status = false;
24 | this.xBoundary = 0;
25 | this.translateX = 0;
26 | this.state = {
27 | translateX:0,
28 | transition: null,
29 | background: '#FFF',
30 | border:'#DDD',
31 | };
32 | }
33 |
34 | componentDidMount() {
35 | this.xBoundary = ReactDOM.findDOMNode(this.refs.wrapper).clientWidth - ReactDOM.findDOMNode(this.refs.togger).offsetWidth;
36 | this.toggerDOM = ReactDOM.findDOMNode(this.refs.togger);
37 | this.toggerDOM.translateX = 0;
38 | this.toggerDOM.addEventListener('transitionend',this.onXTranslateEnd,false);
39 | this.status = this.props.isOpen;
40 | if(this.props.isOpen) {
41 | this.setToggerTranslateX(this.xBoundary);
42 | }
43 | }
44 |
45 | componentWillReceiveProps(nextProps) {
46 | if( 'isOpen' in nextProps && this.props.isOpen !== nextProps.isOpen){
47 | const {
48 | isOpen
49 | } = nextProps;
50 | this.enableTransition(true);
51 | if(isOpen) {
52 | this.setToggerTranslateX(this.xBoundary);
53 | }else {
54 | this.setToggerTranslateX(this.xBoundary * -1);
55 | }
56 | }
57 | }
58 |
59 | onMove(e) {
60 | if(this.props.disabled) return;
61 | if(!this.movingEnable)return;
62 | this.setToggerTranslateX(e.deltaX);
63 | }
64 |
65 | onTap(e) {
66 | if(this.props.disabled) return;
67 | this.enableTransition(true);
68 | if(this.translateX === 0) {
69 | this.setToggerTranslateX(this.xBoundary);
70 | }else if(this.translateX === this.xBoundary) {
71 | this.setToggerTranslateX(this.xBoundary * -1);
72 | }
73 | }
74 |
75 | onXTranslateEnd() {
76 | this.setState({
77 | transition:null
78 | });
79 | const {
80 | onValueChanged
81 | } = this.props;
82 | if(this.translateX <= 1 && this.status === true) {
83 | this.status = false;
84 | onValueChanged && onValueChanged.call(this,false);
85 | }else if(this.translateX >= this.xBoundary && this.status === false) {
86 | this.status = true;
87 | onValueChanged && onValueChanged.call(this,true);
88 | }
89 | }
90 |
91 | setToggerTranslateX(deltaX) {
92 | if(!this.toggerDOM) return;
93 | this.translateX += deltaX;
94 | if(this.translateX >= this.xBoundary) this.translateX = this.xBoundary;
95 | this.translateX = this.translateX <=1 ? 0 : this.translateX;
96 | this.setState({
97 | translateX: this.translateX
98 | });
99 | this.onMoving();
100 | }
101 |
102 | onToggerTouchStart(e) {
103 | if(this.props.disabled) return;
104 | if(this.movingEnable) return;
105 | this.movingEnable = true;
106 | this.enableTransition(false);
107 | }
108 |
109 | onToggerTouchCancel(e) {
110 | if(this.props.disabled) return;
111 | this.movingEnable = false;
112 | if(this.translateX <= 1 && this.status === false) return;
113 | if(this.translateX >= this.xBoundary && this.status === true) return;
114 | const {
115 | onValueChanged
116 | } = this.props;
117 | if(this.translateX <= 1) {
118 | this.status = false;
119 | onValueChanged && onValueChanged.call(this,false);
120 | return;
121 | }else if(this.translateX >= this.xBoundary) {
122 | this.status = true;
123 | onValueChanged && onValueChanged.call(this,true);
124 | return;
125 | }
126 | this.enableTransition(true);
127 | if(this.translateX < this.xBoundary /2) {
128 | this.translateX = 0;
129 | }else {
130 | this.translateX = this.xBoundary;
131 | }
132 | this.setState({
133 | translateX: this.translateX,
134 | });
135 | this.onMoving();
136 | }
137 |
138 |
139 | onMoving() {
140 | let background = '#FFF';
141 | let border = '#DDD';
142 | let {
143 | color,
144 | } = this.props;
145 | if(this.translateX > this.xBoundary /2) {
146 | background = this.getWrapperStyle(color);
147 | border = background;
148 | }
149 | this.setState({
150 | background,
151 | border
152 | });
153 | }
154 |
155 | enableTransition(isEnable) {
156 | if(isEnable) {
157 | this.setState({
158 | transition: 'transform 0.1s linear'
159 | });
160 | }else {
161 | this.setState({
162 | transition: null
163 | });
164 | }
165 | }
166 |
167 | getWrapperStyle(style) {
168 | switch(style.toLowerCase()) {
169 | case 'primary':
170 | return '#4cd964';
171 | case 'blue':
172 | return '#007aff';
173 | default:
174 | return '#fff';
175 | }
176 | }
177 |
178 |
179 | render() {
180 | let {
181 | translateX,
182 | transition,
183 | background,
184 | border
185 | } = this.state;
186 | let toggleStyle = {
187 | transform: `translate(${translateX}px,0px) translateZ(0)`,
188 | WebkitTransform: `translate(${translateX}px,0px) translateZ(0)`,
189 | transition,
190 | WebkitTranssition: transition
191 | }
192 | let wrapperStyle = {
193 | background,
194 | border:`2px solid ${border}`,
195 | };
196 | if(this.props.disabled) {
197 | wrapperStyle.opacity = 0.5;
198 | }
199 | let {
200 | size
201 | } = this.props;
202 | return(
203 |
204 |
211 |
212 | );
213 | }
214 | }
215 |
216 | Switch.propTypes = {
217 | disabled: PropTypes.bool, //是否禁用
218 | isOpen: PropTypes.bool, //初始状态
219 | onValueChanged:PropTypes.func, //回调函数
220 | color: PropTypes.string, //颜色 primary,blue
221 | size: PropTypes.string,//大小 normal,small
222 | };
223 |
224 | Switch.defaultProps = {
225 | disabled: false,
226 | isOpen: true,
227 | color: 'primary',
228 | size: 'normal',
229 | onValueChanged: (isOpen) => {console.log(isOpen);}
230 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Switch",
3 | "version": "1.0.0",
4 | "description": "Fingure supported Switch",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "bundle": "babel-node bundle.js"
8 | },
9 | "author": "boyzhoulin@sina.com",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "babel-core": "^6.5.2",
13 | "babel-loader": "^6.2.2",
14 | "babel-preset-es2015": "^6.9.0",
15 | "babel-preset-react": "^6.5.0",
16 | "css-loader": "^0.23.1",
17 | "less-loader": "^2.2.3",
18 | "style-loader": "^0.13.0",
19 | "webpack": "^1.13.1"
20 | },
21 | "dependencies": {
22 | "classnames": "^2.2.3",
23 | "less": "^2.7.1",
24 | "react": "^0.14.7",
25 | "react-dom": "^0.14.7"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | width: 100%;
3 | height: 50px;
4 | display: flex;
5 | align-items: center;
6 | border-bottom: 1px solid #ccc;
7 | text-align: right;
8 | justify-content: space-between;
9 | }
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | import ReactDom from 'react-dom';
2 | import Switch from './index';
3 | import React from 'react';
4 | import './test.css';
5 |
6 | class SwitchLabel extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.onValueChanged = this.onValueChanged.bind(this);
10 | this.state = {
11 | isOpen: true
12 | };
13 | }
14 |
15 | onValueChanged(isOpen) {
16 | this.setState({
17 | isOpen
18 | });
19 | console.log(isOpen);
20 | }
21 |
22 | componentDidMount() {
23 | let {
24 | isOpen
25 | } = this.props;
26 | if(isOpen === false) {
27 | this.setState({
28 | isOpen
29 | });
30 | }
31 | }
32 |
33 | render() {
34 | let {
35 | isOpen
36 | } = this.state;
37 | return (
38 |
39 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | class App extends React.Component {
47 | constructor(props) {
48 | super(props);
49 | this.onBtnClick = this.onBtnClick.bind(this);
50 | this.state = {
51 | isOpen: false
52 | }
53 | this.onValueChanged = this.onValueChanged.bind(this);
54 | }
55 |
56 | onBtnClick() {
57 | this.isOpen = !this.isOpen;
58 | this.setState({
59 | isOpen: this.isOpen
60 | });
61 | }
62 |
63 | onValueChanged(isOpen) {
64 | this.isOpen = isOpen;
65 | }
66 |
67 | render() {return (
68 |
69 |
70 |
71 |
72 |
73 |
74 | );
75 | }
76 | }
77 |
78 | ReactDom.render(,document.getElementById('app'))
79 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | context : __dirname,
3 | entry : {
4 | index:'./test.js'
5 | },
6 | output : {
7 | path: __dirname + '/assets/',
8 | filename: '[name].js',
9 | },
10 | module: {
11 | loaders : [
12 | {
13 | test:/\.jsx?$/,
14 | loader :'babel',
15 | exclude : /node_modules/,
16 | query: {
17 | presets:['es2015','react']
18 | }
19 | },
20 | {
21 | test:/\.css$/,
22 | loader:"style!css!less"
23 | }
24 | ]
25 | }
26 | };
27 |
28 | export default config;
--------------------------------------------------------------------------------