├── .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 |
205 |
210 |
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; --------------------------------------------------------------------------------