├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── examples ├── .babelrc ├── dist │ └── bundle.js ├── index.html ├── index.js ├── package.json ├── src │ ├── components.js │ ├── index.js │ ├── index.less │ ├── libs │ │ ├── alloyfinger.js │ │ └── transform.js │ └── net-error.png └── webpack.config.js ├── package.json ├── src ├── components.js ├── index.js ├── index.less ├── libs │ ├── alloyfinger.js │ └── transform.js └── net-error.png └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | .DS_Store 4 | .tmp 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 中文 | [English](#user-content-english--中文) 2 | 3 | ## React图片查看器 4 | 使用React打造的H5图片查看器 5 | 6 | ## 特性 7 | * 各类手势快速响应 8 | * 急速滑动翻页 9 | * 支持双指缩放、旋转、双击放大 10 | * 支持放大后局部拖拽、翻页 11 | * 支持超长(纵向)拼接图查看 12 | * 支持下载 13 | * 图片懒加载、预加载 14 | 15 | ## 示例 16 | 您可以下载代码在examples文件夹中找到例子或者[在线示例](https://alloyteam.github.io/AlloyViewer/examples/) 17 | 18 | ## 使用方法 19 | 20 | ### 1、安装NPM依赖 21 | `npm install react-imageview --save` 22 | 23 | ### 2、随意使用 24 | ``` 25 | // 例 1: 26 | 27 | import React, { Component } from 'react' 28 | import ImageView from 'react-imageview' 29 | 30 | import 'react-imageview/dist/react-imageview.min.css' 31 | 32 | class Main extends Component { 33 | state = { 34 | showViewer: false 35 | } 36 | render() { 37 | const imagelist = ['./1.png','./2.png','./3.png','./4.png'] 38 | return ( 39 |
40 | { 41 | !!this.state.showViewer && 42 | } 43 | 44 |
45 | ) 46 | } 47 | show() { 48 | this.setState({ showViewer: true }) 49 | } 50 | close() { 51 | this.setState({ showViewer: false}) 52 | } 53 | } 54 | 55 | // 例 2(推荐使用): 56 | 57 | import { SingleImgView } from 'react-imageview' 58 | import 'react-imageview/dist/react-imageview.min.css' 59 | 60 | const imagelist = ['./1.png','./2.png','./3.png','./4.png'] 61 | 62 | // 仅创建一个ImageView实例 63 | SingleImgView.show({ 64 | imagelist, 65 | close: () => { SingleImgView.hide() } 66 | }); 67 | ``` 68 | 69 | ## 配置说明 70 | | 参数 | 类型 | 描述 | 必需 | 默认值 | 71 | | :------------- | :------------- | :------------- | :------------- | :------------- | 72 | | imagelist | array | 要预览的图片列表 | 是 | 无 | 73 | | current | number | 当前展示的图片序号(从0开始) | 否 | 0 | 74 | | close | function | 图片查看器关闭方法 | 是 | | 75 | | gap | number | 轮播图间距 | 否 | 30 | 76 | | maxScale | number | 最大缩放倍数 | 否 | 2 | 77 | | disablePinch | bool | 禁用缩小放大 | 否 | false | 78 | | enableRotate | bool | 启用旋转 | 否(默认关闭) | false | 79 | | disableDoubleTap | bool | 禁用双击放大 | 否 | false | 80 | | initCallback | function | 初始化后回调 | 否 | | 81 | | longTap | function | 长按回调 | 否 | | 82 | | changeIndex | function | 轮播后回调 | 否 | | 83 | 84 | 85 | 86 | ## English | [中文](#user-content-中文--english) 87 | 88 | ## react-imageview 89 | Imageview component built with react 90 | 91 | ## Demo 92 | You can download the code and find demo in folder which is named as examples or [demo online](https://alloyteam.github.io/AlloyViewer/examples/) 93 | 94 | ## Usage with React 95 | 96 | ### 1、Install the package 97 | `npm install react-imageview --save` 98 | 99 | ### 2、Using as your need 100 | ``` 101 | // Example 1: 102 | 103 | import React, { Component } from 'react' 104 | import ImageView from 'react-imageview' 105 | 106 | import 'react-imageview/dist/react-imageview.min.css' 107 | 108 | class Main extends Component { 109 | state = { 110 | showViewer: false 111 | } 112 | render() { 113 | const imagelist = ['./1.png','./2.png','./3.png','./4.png'] 114 | return ( 115 |
116 | { 117 | !!this.state.showViewer && 118 | } 119 | 120 |
121 | ) 122 | } 123 | show() { 124 | this.setState({ showViewer: true }) 125 | } 126 | close() { 127 | this.setState({ showViewer: false}) 128 | } 129 | } 130 | 131 | // Example 2(Recommended): 132 | 133 | import { SingleImgView } from 'react-imageview' 134 | import 'react-imageview/dist/react-imageview.min.css' 135 | 136 | // You can call SingleImgView.show anywhere and anytime, there will be only one View DOM node be added. 137 | 138 | const imagelist = ['./1.png','./2.png','./3.png','./4.png'] 139 | SingleImgView.show({ 140 | imagelist, 141 | close: () => { SingleImgView.hide() } 142 | }); 143 | ``` 144 | 145 | ## Configuration 146 | | Param | Type | Description | Required | 147 | | :------------- | :------------- | :------------- | :------------- | 148 | | imagelist | array | The list of images to view | Yes | 149 | | current | number | The current image to first view | No | 150 | | close | function | The method to close the viewer | Yes | 151 | | disablePinch | bool | Disable pinch function | No | 152 | | disableRotate | bool | Disable rotate function | No | 153 | | disableDoubleTap | bool | Disable double tap function | No | 154 | | longTap | function | Events called after the long tap | No | 155 | 156 | ## License 157 | Copyright(c) 2016-2017 AlloyTeam. Licensed under MIT license. 158 | -------------------------------------------------------------------------------- /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | examples 10 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { SingleImgView } from './src/index.js' 4 | // import ImageView from 'react-imageview' 5 | 6 | // import 'react-imageview/dist/react-imageview.css' 7 | import Mlogger from '@tencent/mlogger' 8 | 9 | class Main extends Component { 10 | constructor(){ 11 | super(); 12 | Mlogger.init({}); 13 | } 14 | 15 | render() { 16 | let imagelist = [ 17 | 'https://p.qpic.cn/qqconadmin/0/e4a67754b2d1485aa186a4d38dbf07e1/0', 18 | 'https://gpic.qpic.cn/gbar_pic/2aqluyraXicEfqicaK3aV4iaib5icib78qF0eFxokIEKSewIg8hQW0kiavCQg/1000', 19 | 'https://gpic.qpic.cn/gbar_pic/3MSgRdnPzZAQnkIModguuoU1PXSKZUup1B67V82b3KicfhjAVwh19BRFia4DgWfxgg/1000', 20 | 'https://gpic.qpic.cn/gbar_pic/2aqluyraXicEfqicaK3aV4iazVolQTREmcvaEG92Hy9oibhyDJHNzu1s3w/1000', 21 | 'https://gpic.qpic.cn/gbar_pic/emH5YQz0vOJ2E0L6ZljlcW9nFgQzMXtpN240iaeB7PFUhZSWvvpbtLA/1000', 22 | 'https://gpic.qpic.cn/gbar_pic/hVlQlSGMCtYlKrqpM5xwdmJrbh4iaawOgY6lFT1eNWTib7qv2Z2QuJWXmchPUqBriay/1000', 23 | 'https://gpic.qpic.cn/gbar_pic/lDVAjxOVicMnyU4OWLShicffM3TvZYFia4ywL0B5oC3BLPDCoIkgdkJLA/0', 24 | 'https://gpic.qpic.cn/gbar_pic/2aqluyraXicEfqicaK3aV4ia3YQE3mKcibH02jibympJ4gzCUEjk2Iz5BwQ/1000', 25 | 'https://gpic.qpic.cn/gbar_pic/rqlh3lfegUYAvWGGNA8wyC5kly2PwLzONQsSatcxicqJOw0gz9MGmZg/1000', 26 | 'https://gpic.qpic.cn/gbar_pic/PR0vBBjLNC7PpwKQ5YmKjo9ricr8EqAZFQVzXJG96SKCr4hVoWiaT4OQ/0', 27 | ]; 28 | 29 | return ( 30 |
31 |

Click image to open the viewer.

32 | 37 |
38 | ) 39 | } 40 | 41 | show(imagelist, current){ 42 | SingleImgView.show({ 43 | imagelist, 44 | current, 45 | maxScale: 3, 46 | close: ()=>{SingleImgView.hide()}, 47 | initCallback: ()=>{ 48 | // 禁止右滑关闭webview 49 | // if(mqq){ 50 | // mqq.ui.setWebViewBehavior({ 51 | // swipeBack: 0 52 | // }); 53 | 54 | // // 禁用系统的长按功能(如果没有配置长按事件则启用系统长按事件) 55 | // if (mqq.compare('5.8') > -1) { 56 | // mqq.invoke('ui', 'disableLongPress', { 57 | // enable: true 58 | // }); 59 | // } else if (mqq.compare('5.8') > -1) { 60 | // mqq.invoke('ui', 'disableLongPress', { 61 | // enable: false 62 | // }); 63 | // } 64 | // } 65 | } 66 | }) 67 | } 68 | } 69 | 70 | render( 71 |
, 72 | document.getElementById('app') 73 | ) 74 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --history-api-fallback --no-info --open", 8 | "dev": "webpack --progress --colors --watch" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "babel-core": "^6.21.0", 14 | "babel-loader": "^6.2.10", 15 | "babel-preset-es2015": "^6.18.0", 16 | "babel-preset-react": "^6.16.0", 17 | "babel-preset-stage-1": "^6.16.0", 18 | "css-loader": "^0.26.1", 19 | "less": "^2.7.1", 20 | "less-loader": "^2.2.3", 21 | "style-loader": "^0.13.1", 22 | "url-loader": "^0.5.7", 23 | "webpack": "^1.14.0", 24 | "webpack-dev-server": "^1.16.2" 25 | }, 26 | "dependencies": { 27 | "react": "^15.4.1", 28 | "react-dom": "^15.4.1", 29 | "react-imageview": "^1.1.0", 30 | "react-singleton": "^1.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/src/components.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | const PRELOADNUM = 3; 4 | 5 | export class CenterImage extends Component { 6 | state = { 7 | loading: true, 8 | error: false, 9 | loaded: false 10 | } 11 | 12 | render(){ 13 | const { loading, error } = this.state, 14 | { index, current, lazysrc, ...childProps } = this.props, 15 | img = (); 16 | 17 | // init first image, others have been preloaded 18 | if( index === current ){ return img } 19 | if(loading){ return } 20 | if(error){ return } 21 | 22 | return img; 23 | } 24 | 25 | componentWillMount() { 26 | this.loadImg(); 27 | } 28 | 29 | componentWillReceiveProps(nextProps){ 30 | !this.state.loaded && this.loadImg(); 31 | } 32 | 33 | loadImg() { 34 | const { index, current, lazysrc } = this.props; 35 | 36 | if( lazysrc && index <= current + PRELOADNUM && index >= current - PRELOADNUM ){ 37 | let img = new Image(); 38 | 39 | img.src = lazysrc; 40 | img.onload = () => { 41 | this.setState({ 42 | loading: false 43 | }) 44 | }; 45 | img.onerror = () => { 46 | this.setState({ 47 | loading: false, 48 | error: true 49 | }) 50 | }; 51 | } 52 | } 53 | 54 | onImgLoad(e) { 55 | 56 | this.setState({ loaded: true }); 57 | 58 | const target = e.target, 59 | h = target.naturalHeight, 60 | w = target.naturalWidth, 61 | r = h / w, 62 | height = window.innerHeight || window.screen.availHeight, 63 | width = window.innerWidth || window.screen.availWidth, 64 | rate = height / width; 65 | 66 | let imgStyle = {}; 67 | 68 | if(r >= 3.5){ 69 | // imgStyle.width = width + "px"; 70 | // imgStyle.height = h * width / w + "px"; 71 | target.setAttribute('long', true); 72 | } 73 | 74 | if(r > rate){ 75 | imgStyle.height = height + "px"; 76 | imgStyle.width = w * height / h + "px"; 77 | imgStyle.left = width / 2 - (w * height / h) / 2 + "px"; 78 | }else if( r < rate){ 79 | imgStyle.width = width + "px"; 80 | imgStyle.height = h * width / w + "px"; 81 | imgStyle.top = height / 2 - (h * width / w) / 2 + "px" 82 | } else { 83 | imgStyle.width = width; 84 | imgStyle.height = height; 85 | } 86 | 87 | target.setAttribute('style', `width:${imgStyle.width}; height:${imgStyle.height}; left:${imgStyle.left}; top:${imgStyle.top};`); 88 | target.setAttribute('rate', 1/r); 89 | } 90 | } 91 | 92 | const Loading = () => { 93 | return ( 94 |
95 |
96 |
97 |
98 | ) 99 | } 100 | 101 | const Error = () => { 102 | return ( 103 |
加载失败
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | /********************************************************************************************** 2 | * This component is designed for Tribe Project in QQ mobile as a Imageviewer 3 | * You can use it as a independent component in your App 4 | * 5 | * @ examples you can find examples in folder examples or README.md 6 | * 7 | * @ param(array) imagelist: The list of images to view 8 | * @ param(bool) disablePinch: Disable pinch function 9 | * @ param(bool) disableRotate: Disable rotate function 10 | * @ param(bool) disableDoubleTap: Disable double tap function 11 | * @ param(function) longTap: Events called after the long tap 12 | * @ param(function) close: the function to close the viewer 13 | * 14 | * Copyright by nemoliao( liaozksysu@gmail.com), nemo is a member of AlloyTeam in Tencent. 15 | * 16 | **********************************************************************************************/ 17 | import React, { Component } from 'react' 18 | import AlloyFinger from './libs/alloyfinger.js' 19 | import Transform from './libs/transform.js' 20 | import { CenterImage } from './components.js' 21 | import Singleton from 'react-singleton' 22 | 23 | import './index.less' 24 | 25 | const MARGIN = 30 26 | 27 | class ImageView extends Component { 28 | static defaultProps = { 29 | gap: MARGIN, 30 | current: 0, 31 | disablePageNum: false, 32 | desc: '', 33 | maxScale: 2 34 | } 35 | 36 | static propTypes = { 37 | gap: React.PropTypes.number, 38 | maxScale: React.PropTypes.number, 39 | current: React.PropTypes.number, 40 | imagelist: React.PropTypes.array.isRequired, 41 | disablePageNum: React.PropTypes.bool, 42 | disablePinch: React.PropTypes.bool, 43 | enableRotate: React.PropTypes.bool, 44 | disableDoubleTap: React.PropTypes.bool, 45 | longTap: React.PropTypes.func, 46 | close: React.PropTypes.func.isRequired, 47 | changeIndex: React.PropTypes.func, 48 | initCallback: React.PropTypes.func 49 | } 50 | 51 | constructor(props) { 52 | super(); 53 | this.arrLength = props.imagelist.length; 54 | this.state = { 55 | current: props.current 56 | } 57 | } 58 | 59 | initScale = 1; 60 | screenWidth = window.innerWidth || window.screen.availWidth; 61 | screenHeight = window.innerHeight || window.screen.availHeight; 62 | list = null; 63 | ob = null; 64 | focused = null; 65 | 66 | render() { 67 | const { desc, disablePageNum, children, gap } = this.props; 68 | 69 | return ( 70 |
71 | 75 |
    76 | { 77 | this.props.imagelist.map((item, i) => { 78 | return ( 79 |
  • 80 | 88 | 89 | 90 |
  • 91 | ) 92 | }) 93 | } 94 |
95 |
96 | { 97 | disablePageNum ? null :
{ this.state.current + 1 } / { this.arrLength }
98 | } 99 | { 100 | desc ?
: null 101 | } 102 | { children } 103 |
104 | ) 105 | } 106 | 107 | componentDidMount() { 108 | const { current } = this.state, 109 | { imagelist, initCallback } = this.props; 110 | 111 | this.arrLength = imagelist.length; 112 | this.list = this.refs['imagelist']; 113 | 114 | Transform(this.list); 115 | 116 | current && this.changeIndex(current, false); 117 | 118 | this.bindStyle(current); 119 | 120 | initCallback && initCallback(); 121 | } 122 | 123 | onSingleTap(){ 124 | this.props.close && this.props.close(); 125 | } 126 | 127 | onPressMove(evt){ 128 | const { current } = this.state; 129 | 130 | this.endAnimation(); 131 | 132 | if( !this.focused ){ 133 | if((current === 0 && evt.deltaX > 0) || (current === this.arrLength - 1 && evt.deltaX < 0)){ 134 | this.list.translateX += evt.deltaX / 3; 135 | }else{ 136 | this.list.translateX += evt.deltaX; 137 | } 138 | } 139 | 140 | evt.preventDefault(); 141 | } 142 | 143 | onSwipe(evt){ 144 | const { direction } = evt; 145 | 146 | let { current } = this.state; 147 | if( this.focused ){ 148 | return false; 149 | } 150 | switch(direction) { 151 | case 'Left': 152 | current < this.arrLength-1 && ++current && this.bindStyle(current); 153 | break; 154 | case 'Right': 155 | current > 0 && current-- && this.bindStyle(current); 156 | break; 157 | } 158 | this.changeIndex(current) 159 | } 160 | 161 | onPicPressMove(evt) { 162 | const { deltaX, deltaY } = evt, 163 | isLongPic = this.ob.getAttribute('long'), 164 | { scaleX, width } = this.ob; 165 | 166 | if(this.ob.scaleX <= 1 || evt.touches.length > 1){ 167 | return; 168 | } 169 | 170 | if(this.ob && this.checkBoundary(deltaX, deltaY)){ 171 | !isLongPic && (this.ob.translateX += deltaX); 172 | this.ob.translateY += deltaY; 173 | 174 | if(isLongPic && scaleX * width === this.screenWidth){ 175 | this.focused = false; 176 | }else{ 177 | this.focused = true; 178 | } 179 | }else { 180 | this.focused = false; 181 | } 182 | // console.log('translate ',this.ob.translateX, this.ob.translateY); 183 | } 184 | 185 | onMultipointStart(){ 186 | this.initScale = this.ob.scaleX; 187 | } 188 | 189 | onPinch(evt){ 190 | if( this.props.disablePinch || this.ob.getAttribute('long')){ 191 | return false; 192 | } 193 | this.ob.style.webkitTransition = 'cubic-bezier(.25,.01,.25,1)' 194 | 195 | const { originX, originY } = this.ob, 196 | originX2 = evt.center.x - this.screenWidth/2 - document.body.scrollLeft, 197 | originY2 = evt.center.y - this.screenHeight/2 - document.body.scrollTop; 198 | 199 | this.ob.originX = originX2; 200 | this.ob.originY = originY2; 201 | this.ob.translateX = this.ob.translateX + (originX2 - originX) * this.ob.scaleX; 202 | this.ob.translateY = this.ob.translateY + (originY2 - originY) * this.ob.scaleY; 203 | 204 | this.ob.scaleX = this.ob.scaleY = this.initScale * evt.scale; 205 | } 206 | 207 | onRotate(evt){ 208 | if( !this.props.enableRotate || this.ob.getAttribute('rate') >= 3.5){ 209 | return false; 210 | } 211 | 212 | this.ob.style.webkitTransition = 'cubic-bezier(.25,.01,.25,1)' 213 | 214 | this.ob.rotateZ += evt.angle; 215 | } 216 | 217 | onLongTap(){ 218 | this.props.longTap && this.props.longTap(); 219 | } 220 | 221 | onMultipointEnd(evt){ 222 | // translate to normal 223 | this.changeIndex(this.state.current); 224 | 225 | if(!this.ob){ 226 | return; 227 | } 228 | 229 | this.ob.style.webkitTransition = '300ms ease'; 230 | 231 | const { maxScale } = this.props, 232 | isLongPic = this.ob.getAttribute('long'); 233 | // scale to normal 234 | if (this.ob.scaleX < 1) { 235 | this.restore(false); 236 | } 237 | if (this.ob.scaleX > maxScale && !isLongPic){ 238 | this.setScale(maxScale); 239 | } 240 | 241 | // rotate to normal 242 | let rotation = this.ob.rotateZ % 360, 243 | rate = this.ob.getAttribute('rate'); 244 | 245 | if(rotation < 0){ 246 | rotation = 360 + rotation; 247 | } 248 | this.ob.rotateZ = rotation; 249 | 250 | if (rotation > 0 && rotation < 45) { 251 | this.ob.rotateZ = 0; 252 | } else if (rotation >= 315) { 253 | this.ob.rotateZ = 360; 254 | } else if (rotation >= 45 && rotation < 135) { 255 | this.ob.rotateZ = 90; 256 | this.setScale(rate); 257 | } else if (rotation >= 135 && rotation < 225) { 258 | this.ob.rotateZ = 180; 259 | } else if (rotation >= 225 && rotation < 315) { 260 | this.ob.rotateZ = 270; 261 | this.setScale(rate); 262 | } 263 | } 264 | 265 | onDoubleTap(evt){ 266 | if( this.props.disableDoubleTap ){ 267 | return false; 268 | } 269 | 270 | const { origin } = evt, 271 | originX = origin[0] - this.screenWidth/2 - document.body.scrollLeft, 272 | originY = origin[1] - this.screenHeight/2 - document.body.scrollTop, 273 | isLongPic = this.ob.getAttribute('long'); 274 | 275 | if(this.ob.scaleX === 1){ 276 | !isLongPic && (this.ob.translateX = this.ob.originX = originX); 277 | !isLongPic && (this.ob.translateY = this.ob.originY = originY); 278 | this.setScale(isLongPic ? this.screenWidth / this.ob.width : this.props.maxScale); 279 | }else{ 280 | this.ob.translateX = this.ob.originX; 281 | this.ob.translateY = this.ob.originY; 282 | this.setScale(1); 283 | } 284 | 285 | // console.log('origin',this.ob.originX, this.ob.originY); 286 | } 287 | 288 | bindStyle(current) { 289 | this.setState({ current }, () => { 290 | this.ob && this.restore(); 291 | this.ob = document.getElementById(`view${current}`); 292 | if(this.ob && !this.ob.scaleX){ 293 | Transform(this.ob) 294 | } 295 | // ease hide page number 296 | const page = this.refs.page; 297 | if(page){ 298 | page.classList.remove('hide'); 299 | setTimeout(()=>{ 300 | page.classList.add('hide'); 301 | }, 2000); 302 | } 303 | }) 304 | } 305 | 306 | changeIndex(current, ease=true) { 307 | ease && (this.list.style.webkitTransition = '300ms ease'); 308 | this.list.translateX = -current*(this.screenWidth + this.props.gap); 309 | 310 | this.props.changeIndex && this.props.changeIndex(current); 311 | } 312 | 313 | setScale(size) { 314 | this.ob.style.webkitTransition = '300ms ease-in-out'; 315 | this.ob.scaleX = this.ob.scaleY = size; 316 | } 317 | 318 | restore(rotate=true) { 319 | this.ob.translateX = this.ob.translateY = 0; 320 | !!rotate && (this.ob.rotateZ = 0); 321 | this.ob.scaleX = this.ob.scaleY = 1; 322 | this.ob.originX = this.ob.originY = 0; 323 | } 324 | 325 | endAnimation() { 326 | this.list.style.webkitTransition = '0'; 327 | this.ob && this.ob.style && (this.ob.style.webkitTransition = '0'); 328 | } 329 | 330 | checkBoundary(deltaX = 0, deltaY = 0) { 331 | // console.log(this.ob.width, this.ob.height); 332 | const { scaleX, translateX, translateY, originX, originY, width, height } = this.ob, 333 | rate = this.ob.getAttribute('rate'); 334 | 335 | if(scaleX !== 1 || scaleX !== rate){ 336 | // include long picture 337 | const rangeLeft = (scaleX - 1) * (width / 2 + originX) + originX, 338 | rangeRight = -(scaleX - 1) * (width / 2 - originX) + originX, 339 | rangeUp = (scaleX - 1) * (height / 2 + originY) + originY, 340 | rangeDown = -(scaleX - 1) * (height / 2 - originY) + originY; 341 | 342 | // console.log(rangeLeft, rangeRight, rangeUp, rangeDown); 343 | 344 | if(translateX + deltaX <= rangeLeft 345 | && translateX + deltaX >= rangeRight 346 | && translateY + deltaY <= rangeUp 347 | && translateY + deltaY >= rangeDown ) { 348 | return true; 349 | } 350 | } 351 | return false; 352 | } 353 | } 354 | 355 | export const SingleImgView = new Singleton(ImageView) 356 | 357 | export default ImageView 358 | -------------------------------------------------------------------------------- /examples/src/index.less: -------------------------------------------------------------------------------- 1 | html, body, div, ul, li, a { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | .hide { 7 | opacity: 0; 8 | transition: opacity 0.2s; 9 | -webkit-transition: opacity 0.2s; 10 | } 11 | 12 | .imageview { 13 | position: fixed; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: 0; 18 | width: 100%; 19 | height: 100%; 20 | z-index: 900; 21 | background-color: #000; 22 | overflow: hidden; 23 | animation: easeshow 0.25s; 24 | 25 | .page { 26 | font-family: -apple-system-font, 'Helvetica Neue', Helvetica, STHeiTi,sans-serif; 27 | position: fixed; 28 | font-size: 14px; 29 | color: #fff; 30 | padding: 2px 5px; 31 | bottom: 10px; 32 | left: 50%; 33 | -webkit-transform: translateX(-50%); 34 | transform: translateX(-50%); 35 | -webkit-touch-callout: none; 36 | -webkit-user-select: none; 37 | } 38 | 39 | .spinner { 40 | width: 40px; 41 | height: 40px; 42 | position: absolute; 43 | top: 45%; 44 | left: 50%; 45 | transform: translate(-50%,-50%); 46 | } 47 | 48 | .double-bounce1, .double-bounce2 { 49 | width: 100%; 50 | height: 100%; 51 | border-radius: 50%; 52 | background-color: #333; 53 | opacity: 0.6; 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | 58 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 59 | animation: sk-bounce 2.0s infinite ease-in-out; 60 | } 61 | 62 | .double-bounce2 { 63 | -webkit-animation-delay: -1.0s; 64 | animation-delay: -1.0s; 65 | } 66 | 67 | .errorpage { 68 | position: absolute; 69 | font-size: 16px; 70 | text-align: center; 71 | color: rgb(170, 170, 170); 72 | top: 28%; 73 | left: 50%; 74 | margin-left: -70px; 75 | 76 | &:before { 77 | content:''; 78 | display: block; 79 | width: 150px; 80 | height: 140px; 81 | margin: 0 auto; 82 | padding-bottom: 20px; 83 | background: url('./net-error.png') no-repeat; 84 | background-size: 100%; 85 | opacity: .4; 86 | } 87 | } 88 | } 89 | 90 | @keyframes easeshow { 91 | from { opacity: 0; } 92 | to { opacity: 1; } 93 | } 94 | @-webkit-keyframes easeshow { 95 | from { opacity: 0; } 96 | to { opacity: 1; } 97 | } 98 | 99 | .imagelist { 100 | display: -webkit-box; 101 | display: box; 102 | height: 100%; 103 | list-style-type: none; 104 | -webkit-touch-callout: none; 105 | -webkit-user-select: none; 106 | .imagelist-item { 107 | display: -webkit-box; 108 | -webkit-box-pack: center; 109 | -webkit-box-align: center; 110 | width: 100%; 111 | height: 100%; 112 | text-align: center; 113 | position: relative; 114 | background-color: #000; 115 | overflow-y: scroll; 116 | 117 | .imagelist-item-img { 118 | position: absolute; 119 | top: 0; 120 | left: 0; 121 | max-width: 100%; 122 | -webkit-touch-callout: none; 123 | -webkit-user-select: none; 124 | } 125 | } 126 | } 127 | 128 | @-webkit-keyframes sk-bounce { 129 | 0%, 100% { -webkit-transform: scale(0.0) } 130 | 50% { -webkit-transform: scale(1.0) } 131 | } 132 | 133 | @keyframes sk-bounce { 134 | 0%, 100% { 135 | transform: scale(0.0); 136 | -webkit-transform: scale(0.0); 137 | } 50% { 138 | transform: scale(1.0); 139 | -webkit-transform: scale(1.0); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/src/libs/alloyfinger.js: -------------------------------------------------------------------------------- 1 | /* AlloyFinger v0.1.0 2 | * By dntzhang 3 | * Reedited by nemoliao 4 | * Github: https://github.com/AlloyTeam/AlloyFinger 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | 9 | export default class AlloyFinger extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.preV = { x: null, y: null }; 14 | this.pinchStartLen = null; 15 | this.scale = 1; 16 | this.isDoubleTap = false; 17 | this.delta = null; 18 | this.last = null; 19 | this.now = null; 20 | this.end = null; 21 | this.multiTouch = false; 22 | this.tapTimeout = null; 23 | this.longTapTimeout = null; 24 | this.singleTapTimeout = null; 25 | this.swipeTimeout=null; 26 | this.x1 = this.x2 = this.y1 = this.y2 = null; 27 | this.preTapPosition={x:null,y:null}; 28 | } 29 | 30 | getLen(v) { 31 | return Math.sqrt(v.x * v.x + v.y * v.y); 32 | } 33 | 34 | dot(v1, v2) { 35 | return v1.x * v2.x + v1.y * v2.y; 36 | } 37 | 38 | getAngle(v1, v2) { 39 | var mr = this.getLen(v1) * this.getLen(v2); 40 | if (mr === 0) return 0; 41 | var r = this.dot(v1, v2) / mr; 42 | if (r > 1) r = 1; 43 | return Math.acos(r); 44 | } 45 | 46 | cross(v1, v2) { 47 | return v1.x * v2.y - v2.x * v1.y; 48 | } 49 | 50 | getRotateAngle(v1, v2) { 51 | var angle = this.getAngle(v1, v2); 52 | if (this.cross(v1, v2) > 0) { 53 | angle *= -1; 54 | } 55 | 56 | return angle * 180 / Math.PI; 57 | } 58 | 59 | _resetState() { 60 | this.setState({ 61 | x: null, 62 | y: null, 63 | swiping: false, 64 | start: 0 65 | }); 66 | } 67 | 68 | 69 | _emitEvent(name, ...arg) { 70 | if (this.props[name]) { 71 | this.props[name](...arg); 72 | } 73 | } 74 | 75 | _handleTouchStart (evt) { 76 | 77 | if(!evt.touches) return; 78 | this.now = Date.now(); 79 | this.x1 = evt.touches[0].pageX; 80 | this.y1 = evt.touches[0].pageY; 81 | this.delta = this.now - (this.last || this.now); 82 | if(this.preTapPosition.x!==null){ 83 | this.isDoubleTap = (this.delta > 0 && this.delta <= 250&&Math.abs(this.preTapPosition.x-this.x1)<30&&Math.abs(this.preTapPosition.y-this.y1)<30); 84 | } 85 | this.preTapPosition.x=this.x1; 86 | this.preTapPosition.y=this.y1; 87 | this.last = this.now; 88 | var preV = this.preV, 89 | len = evt.touches.length; 90 | 91 | if (len > 1) { 92 | this._cancelLongTap(); 93 | this._cancelSingleTap(); 94 | var v = { x: evt.touches[1].pageX - this.x1, y: evt.touches[1].pageY - this.y1 }; 95 | preV.x = v.x; 96 | preV.y = v.y; 97 | this.pinchStartLen = this.getLen(preV); 98 | this._emitEvent('onMultipointStart', evt); 99 | } 100 | this.longTapTimeout = setTimeout(() => { 101 | this._emitEvent('onLongTap', evt); 102 | }, 750); 103 | } 104 | 105 | _handleTouchMove(evt){ 106 | var preV = this.preV, 107 | len = evt.touches.length, 108 | currentX = evt.touches[0].pageX, 109 | currentY = evt.touches[0].pageY; 110 | this.isDoubleTap=false; 111 | if (len > 1) { 112 | var v = { x: evt.touches[1].pageX - currentX, y: evt.touches[1].pageY - currentY }; 113 | if (preV.x !== null) { 114 | if (this.pinchStartLen > 0) { 115 | evt.center = { 116 | x: (evt.touches[1].pageX + currentX) / 2, 117 | y: (evt.touches[1].pageY + currentY) / 2 118 | }; 119 | evt.scale = this.getLen(v) / this.pinchStartLen; 120 | this._emitEvent('onPinch', evt); 121 | } 122 | evt.angle = this.getRotateAngle(v, preV); 123 | this._emitEvent('onRotate', evt); 124 | } 125 | preV.x = v.x; 126 | preV.y = v.y; 127 | this.multiTouch = true; 128 | } else { 129 | if (this.x2 !== null) { 130 | evt.deltaX = currentX - this.x2; 131 | evt.deltaY = currentY - this.y2; 132 | }else{ 133 | evt.deltaX = 0; 134 | evt.deltaY = 0; 135 | } 136 | this._emitEvent('onPressMove', evt); 137 | } 138 | this._cancelLongTap(); 139 | this.x2 = currentX; 140 | this.y2 = currentY; 141 | 142 | if(len > 1) { 143 | evt.preventDefault(); 144 | } 145 | } 146 | 147 | _handleTouchCancel(){ 148 | clearInterval(this.singleTapTimeout); 149 | clearInterval(this.tapTimeout); 150 | clearInterval(this.longTapTimeout); 151 | clearInterval(this.swipeTimeout); 152 | } 153 | 154 | _handleTouchEnd(evt){ 155 | 156 | this.end = Date.now(); 157 | this._cancelLongTap(); 158 | 159 | if( evt.touches.length<2){ 160 | this._emitEvent('onMultipointEnd', evt); 161 | } 162 | 163 | evt.origin = [this.x1, this.y1]; 164 | if(this.multiTouch === false){ 165 | if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) || 166 | (this.y2 && Math.abs(this.preV.y - this.y2) > 30)) { 167 | evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2); 168 | evt.distance = Math.abs(this.x1 - this.x2); 169 | this.swipeTimeout = setTimeout(() => { 170 | this._emitEvent('onSwipe', evt); 171 | }, 0) 172 | } else { 173 | this.tapTimeout = setTimeout(() => { 174 | this._emitEvent('onTap', evt); 175 | if (this.isDoubleTap) { 176 | this._emitEvent('onDoubleTap', evt); 177 | clearTimeout(this.singleTapTimeout); 178 | this.isDoubleTap = false; 179 | } else { 180 | this.singleTapTimeout = setTimeout(()=>{ 181 | this._emitEvent('onSingleTap', evt); 182 | }, 250); 183 | } 184 | }, 0) 185 | } 186 | } 187 | 188 | this.preV.x = 0; 189 | this.preV.y = 0; 190 | this.scale = 1; 191 | this.pinchStartLen = null; 192 | this.x1 = this.x2 = this.y1 = this.y2 = null; 193 | this.multiTouch = false; 194 | } 195 | 196 | _cancelLongTap () { 197 | clearTimeout(this.longTapTimeout); 198 | } 199 | 200 | _cancelSingleTap () { 201 | clearTimeout(this.singleTapTimeout); 202 | } 203 | 204 | _swipeDirection (x1, x2, y1, y2) { 205 | if(Math.abs(x1 - x2) > 80 || this.end-this.now < 250){ 206 | return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down') 207 | }else { 208 | return 'Nochange' 209 | } 210 | 211 | } 212 | 213 | render() { 214 | return React.cloneElement(React.Children.only(this.props.children), { 215 | onTouchStart: this._handleTouchStart.bind(this), 216 | onTouchMove: this._handleTouchMove.bind(this), 217 | onTouchCancel: this._handleTouchCancel.bind(this), 218 | onTouchEnd: this._handleTouchEnd.bind(this) 219 | }); 220 | } 221 | } -------------------------------------------------------------------------------- /examples/src/libs/transform.js: -------------------------------------------------------------------------------- 1 | /* transformjs 2 | * By dntzhang 3 | */ 4 | 5 | 6 | var Matrix3D = function (n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44) { 7 | this.elements =window.Float32Array ? new Float32Array(16) : []; 8 | var te = this.elements; 9 | te[0] = (n11 !== undefined) ? n11 : 1; te[4] = n12 || 0; te[8] = n13 || 0; te[12] = n14 || 0; 10 | te[1] = n21 || 0; te[5] = (n22 !== undefined) ? n22 : 1; te[9] = n23 || 0; te[13] = n24 || 0; 11 | te[2] = n31 || 0; te[6] = n32 || 0; te[10] = (n33 !== undefined) ? n33 : 1; te[14] = n34 || 0; 12 | te[3] = n41 || 0; te[7] = n42 || 0; te[11] = n43 || 0; te[15] = (n44 !== undefined) ? n44 : 1; 13 | }; 14 | 15 | Matrix3D.DEG_TO_RAD = Math.PI / 180; 16 | 17 | Matrix3D.prototype = { 18 | set: function (n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44) { 19 | var te = this.elements; 20 | te[0] = n11; te[4] = n12; te[8] = n13; te[12] = n14; 21 | te[1] = n21; te[5] = n22; te[9] = n23; te[13] = n24; 22 | te[2] = n31; te[6] = n32; te[10] = n33; te[14] = n34; 23 | te[3] = n41; te[7] = n42; te[11] = n43; te[15] = n44; 24 | return this; 25 | }, 26 | identity: function () { 27 | this.set( 28 | 1, 0, 0, 0, 29 | 0, 1, 0, 0, 30 | 0, 0, 1, 0, 31 | 0, 0, 0, 1 32 | ); 33 | return this; 34 | }, 35 | multiplyMatrices: function (a, be) { 36 | 37 | var ae = a.elements; 38 | var te = this.elements; 39 | var a11 = ae[0], a12 = ae[4], a13 = ae[8], a14 = ae[12]; 40 | var a21 = ae[1], a22 = ae[5], a23 = ae[9], a24 = ae[13]; 41 | var a31 = ae[2], a32 = ae[6], a33 = ae[10], a34 = ae[14]; 42 | var a41 = ae[3], a42 = ae[7], a43 = ae[11], a44 = ae[15]; 43 | 44 | var b11 = be[0], b12 = be[1], b13 = be[2], b14 = be[3]; 45 | var b21 = be[4], b22 = be[5], b23 = be[6], b24 = be[7]; 46 | var b31 = be[8], b32 = be[9], b33 = be[10], b34 = be[11]; 47 | var b41 = be[12], b42 = be[13], b43 = be[14], b44 = be[15]; 48 | 49 | te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41; 50 | te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42; 51 | te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43; 52 | te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44; 53 | 54 | te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41; 55 | te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42; 56 | te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43; 57 | te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44; 58 | 59 | te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41; 60 | te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42; 61 | te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43; 62 | te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44; 63 | 64 | te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41; 65 | te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42; 66 | te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43; 67 | te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44; 68 | 69 | return this; 70 | 71 | }, 72 | // 解决角度为90的整数倍导致Math.cos得到极小的数,其实是0。导致不渲染 73 | _rounded: function(value,i){ 74 | i= Math.pow(10, i || 15); 75 | // default 76 | return Math.round(value*i)/i; 77 | }, 78 | appendTransform: function (x, y, z, scaleX, scaleY, scaleZ, rotateX, rotateY, rotateZ,skewX,skewY, originX, originY, originZ) { 79 | 80 | var rx = rotateX * Matrix3D.DEG_TO_RAD; 81 | var cosx =this._rounded( Math.cos(rx)); 82 | var sinx = this._rounded(Math.sin(rx)); 83 | var ry = rotateY * Matrix3D.DEG_TO_RAD; 84 | var cosy =this._rounded( Math.cos(ry)); 85 | var siny = this._rounded(Math.sin(ry)); 86 | var rz = rotateZ * Matrix3D.DEG_TO_RAD; 87 | var cosz =this._rounded( Math.cos(rz * -1)); 88 | var sinz =this._rounded( Math.sin(rz * -1)); 89 | 90 | this.multiplyMatrices(this, [ 91 | 1, 0, 0, x, 92 | 0, cosx, sinx, y, 93 | 0, -sinx, cosx, z, 94 | 0, 0, 0, 1 95 | ]); 96 | 97 | this.multiplyMatrices(this, [ 98 | cosy, 0, siny, 0, 99 | 0, 1, 0, 0, 100 | -siny, 0, cosy, 0, 101 | 0, 0, 0, 1 102 | ]); 103 | 104 | this.multiplyMatrices(this,[ 105 | cosz * scaleX, sinz * scaleY, 0, 0, 106 | -sinz * scaleX, cosz * scaleY, 0, 0, 107 | 0, 0, 1 * scaleZ, 0, 108 | 0, 0, 0, 1 109 | ]); 110 | 111 | if(skewX||skewY){ 112 | this.multiplyMatrices(this,[ 113 | this._rounded(Math.cos(skewX* Matrix3D.DEG_TO_RAD)), this._rounded( Math.sin(skewX* Matrix3D.DEG_TO_RAD)), 0, 0, 114 | -1*this._rounded(Math.sin(skewY* Matrix3D.DEG_TO_RAD)), this._rounded( Math.cos(skewY* Matrix3D.DEG_TO_RAD)), 0, 0, 115 | 0, 0, 1, 0, 116 | 0, 0, 0, 1 117 | ]); 118 | } 119 | 120 | if (originX || originY || originZ) { 121 | this.elements[12] -= originX * this.elements[0] + originY * this.elements[4] + originZ * this.elements[8]; 122 | this.elements[13] -= originX * this.elements[1] + originY * this.elements[5] + originZ * this.elements[9]; 123 | this.elements[14] -= originX * this.elements[2] + originY * this.elements[6] + originZ * this.elements[10]; 124 | } 125 | return this; 126 | } 127 | }; 128 | 129 | function observe(target, props, callback) { 130 | for (var i = 0, len = props.length; i < len; i++) { 131 | var prop = props[i]; 132 | watch(target, prop, callback); 133 | } 134 | } 135 | 136 | function watch(target, prop, callback) { 137 | Object.defineProperty(target, prop, { 138 | get: function () { 139 | return this["__" + prop]; 140 | }, 141 | set: function (value) { 142 | if (value !== this["__" + prop]) { 143 | this["__" + prop] = value; 144 | callback(); 145 | } 146 | 147 | } 148 | }); 149 | } 150 | 151 | var Transform = function (element) { 152 | 153 | observe( 154 | element, 155 | ["translateX", "translateY", "translateZ", "scaleX", "scaleY", "scaleZ" , "rotateX", "rotateY", "rotateZ","skewX","skewY", "originX", "originY", "originZ"], 156 | function () { 157 | var mtx = element.matrix3D.identity().appendTransform( element.translateX, element.translateY, element.translateZ, element.scaleX, element.scaleY, element.scaleZ, element.rotateX, element.rotateY, element.rotateZ,element.skewX,element.skewY, element.originX, element.originY, element.originZ); 158 | element.style.transform = element.style.msTransform = element.style.OTransform = element.style.MozTransform = element.style.webkitTransform = "perspective("+element.perspective+"px) matrix3d(" + Array.prototype.slice.call(mtx.elements).join(",") + ")"; 159 | }); 160 | 161 | observe( 162 | element, 163 | [ "perspective"], 164 | function () { 165 | element.style.transform = element.style.msTransform = element.style.OTransform = element.style.MozTransform = element.style.webkitTransform = "perspective("+element.perspective+"px) matrix3d(" + Array.prototype.slice.call(element.matrix3D.elements).join(",") + ")"; 166 | }); 167 | 168 | element.matrix3D = new Matrix3D(); 169 | element.perspective = 500; 170 | element.scaleX = element.scaleY = element.scaleZ = 1; 171 | //由于image自带了x\y\z,所有加上translate前缀 172 | element.translateX = element.translateY = element.translateZ = element.rotateX = element.rotateY = element.rotateZ =element.skewX=element.skewY= element.originX = element.originY = element.originZ = 0; 173 | } 174 | 175 | module.exports = Transform; 176 | -------------------------------------------------------------------------------- /examples/src/net-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Caesor/react-imageview/de08f947ad074e24cf07c9999baf87f70dec0aef/examples/src/net-error.png -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './index.js', 6 | output: { 7 | path: path.join(__dirname, 'dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: /\.css$/, 14 | loaders: ['style', 'css'] 15 | }, 16 | { 17 | test: /\.less$/, 18 | loaders: ['style', 'css', 'less'] 19 | }, 20 | { 21 | test: /\.(png|jpg|gif|woff|woff2)$/, 22 | loader: 'url-loader?limit=8192' 23 | }, 24 | { 25 | test: /\.(js|jsx)$/, 26 | loaders: ['babel'], 27 | exclude: /node_modules/, 28 | include: [__dirname, path.resolve(__dirname, './../src')] 29 | } 30 | ] 31 | }, 32 | plugins: [ 33 | new webpack.optimize.UglifyJsPlugin({ 34 | compress: { 35 | warnings: false 36 | } 37 | }) 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-imageview", 3 | "version": "1.3.1", 4 | "description": "A image viewer built with react", 5 | "main": "dist/react-imageview.js", 6 | "scripts": { 7 | "build": "npm run build:umd & npm run build:umd:min", 8 | "build:umd": "webpack", 9 | "build:umd:min": "set NODE_ENV=production webpack", 10 | "dev": "webpack --progress --colors --watch", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "prepublish": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Caesor/react-imageview.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "imageview" 21 | ], 22 | "author": "liaozksysu@gmail.com", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/Caesor/react-imageview/issues" 26 | }, 27 | "homepage": "https://github.com/Caesor/react-imageview#readme", 28 | "dependencies": { 29 | "react": "^15.4.1", 30 | "react-dom": "^15.4.1", 31 | "react-singleton": "^1.3.3" 32 | }, 33 | "devDependencies": { 34 | "babel-core": "^6.21.0", 35 | "babel-loader": "^6.2.10", 36 | "babel-preset-es2015": "^6.18.0", 37 | "babel-preset-react": "^6.16.0", 38 | "babel-preset-stage-1": "^6.16.0", 39 | "css-loader": "^0.26.1", 40 | "extract-text-webpack-plugin": "^1.0.1", 41 | "less": "^2.7.1", 42 | "less-loader": "^2.2.3", 43 | "style-loader": "^0.13.1", 44 | "url-loader": "^0.5.7", 45 | "webpack": "^1.14.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | const PRELOADNUM = 3; 4 | 5 | export class CenterImage extends Component { 6 | state = { 7 | loading: true, 8 | error: false, 9 | loaded: false 10 | } 11 | 12 | render(){ 13 | const { loading, error } = this.state, 14 | { index, current, lazysrc, ...childProps } = this.props, 15 | img = (); 16 | 17 | // init first image, others have been preloaded 18 | if( index === current ){ return img } 19 | if(loading){ return } 20 | if(error){ return } 21 | 22 | return img; 23 | } 24 | 25 | componentWillMount() { 26 | this.loadImg(); 27 | } 28 | 29 | componentWillReceiveProps(nextProps){ 30 | !this.state.loaded && this.loadImg(); 31 | } 32 | 33 | loadImg() { 34 | const { index, current, lazysrc } = this.props; 35 | 36 | if( lazysrc && index <= current + PRELOADNUM && index >= current - PRELOADNUM ){ 37 | let img = new Image(); 38 | 39 | img.src = lazysrc; 40 | img.onload = () => { 41 | this.setState({ 42 | loading: false 43 | }) 44 | }; 45 | img.onerror = () => { 46 | this.setState({ 47 | loading: false, 48 | error: true 49 | }) 50 | }; 51 | } 52 | } 53 | 54 | onImgLoad(e) { 55 | 56 | this.setState({ loaded: true }); 57 | 58 | const target = e.target, 59 | h = target.naturalHeight, 60 | w = target.naturalWidth, 61 | r = h / w, 62 | height = window.innerHeight || window.screen.availHeight, 63 | width = window.innerWidth || window.screen.availWidth, 64 | rate = height / width; 65 | 66 | let imgStyle = {}; 67 | 68 | if(r >= 3.5){ 69 | // imgStyle.width = width + "px"; 70 | // imgStyle.height = h * width / w + "px"; 71 | target.setAttribute('long', true); 72 | } 73 | 74 | if(r > rate){ 75 | imgStyle.height = height + "px"; 76 | imgStyle.width = w * height / h + "px"; 77 | imgStyle.left = width / 2 - (w * height / h) / 2 + "px"; 78 | }else if( r < rate){ 79 | imgStyle.width = width + "px"; 80 | imgStyle.height = h * width / w + "px"; 81 | imgStyle.top = height / 2 - (h * width / w) / 2 + "px" 82 | } else { 83 | imgStyle.width = width; 84 | imgStyle.height = height; 85 | } 86 | 87 | target.setAttribute('style', `width:${imgStyle.width}; height:${imgStyle.height}; left:${imgStyle.left}; top:${imgStyle.top};`); 88 | target.setAttribute('rate', 1/r); 89 | } 90 | } 91 | 92 | const Loading = () => { 93 | return ( 94 |
95 |
96 |
97 |
98 | ) 99 | } 100 | 101 | const Error = () => { 102 | return ( 103 |
加载失败
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /********************************************************************************************** 2 | * This component is designed for Tribe Project in QQ mobile as a Imageviewer 3 | * You can use it as a independent component in your App 4 | * 5 | * @ examples you can find examples in folder examples or README.md 6 | * 7 | * @ param(array) imagelist: The list of images to view 8 | * @ param(bool) disablePinch: Disable pinch function 9 | * @ param(bool) disableRotate: Disable rotate function 10 | * @ param(bool) disableDoubleTap: Disable double tap function 11 | * @ param(function) longTap: Events called after the long tap 12 | * @ param(function) close: the function to close the viewer 13 | * 14 | * Copyright by nemoliao( liaozksysu@gmail.com), nemo is a member of AlloyTeam in Tencent. 15 | * 16 | **********************************************************************************************/ 17 | import React, { Component } from 'react' 18 | import AlloyFinger from './libs/alloyfinger.js' 19 | import Transform from './libs/transform.js' 20 | import { CenterImage } from './components.js' 21 | import Singleton from 'react-singleton' 22 | 23 | import './index.less' 24 | 25 | const MARGIN = 30 26 | 27 | class ImageView extends Component { 28 | static defaultProps = { 29 | gap: MARGIN, 30 | current: 0, 31 | disablePageNum: false, 32 | desc: '', 33 | maxScale: 2 34 | } 35 | 36 | static propTypes = { 37 | gap: React.PropTypes.number, 38 | maxScale: React.PropTypes.number, 39 | current: React.PropTypes.number, 40 | imagelist: React.PropTypes.array.isRequired, 41 | disablePageNum: React.PropTypes.bool, 42 | disablePinch: React.PropTypes.bool, 43 | enableRotate: React.PropTypes.bool, 44 | disableDoubleTap: React.PropTypes.bool, 45 | longTap: React.PropTypes.func, 46 | close: React.PropTypes.func.isRequired, 47 | changeIndex: React.PropTypes.func, 48 | initCallback: React.PropTypes.func 49 | } 50 | 51 | constructor(props) { 52 | super(); 53 | this.arrLength = props.imagelist.length; 54 | this.state = { 55 | current: props.current 56 | } 57 | } 58 | 59 | initScale = 1; 60 | screenWidth = window.innerWidth || window.screen.availWidth; 61 | screenHeight = window.innerHeight || window.screen.availHeight; 62 | list = null; 63 | ob = null; 64 | focused = null; 65 | 66 | render() { 67 | const { desc, disablePageNum, children, gap } = this.props; 68 | 69 | return ( 70 |
71 | 75 |
    76 | { 77 | this.props.imagelist.map((item, i) => { 78 | return ( 79 |
  • 80 | 88 | 89 | 90 |
  • 91 | ) 92 | }) 93 | } 94 |
95 |
96 | { 97 | disablePageNum ? null :
{ this.state.current + 1 } / { this.arrLength }
98 | } 99 | { 100 | desc ?
: null 101 | } 102 | { children } 103 |
104 | ) 105 | } 106 | 107 | componentDidMount() { 108 | const { current } = this.state, 109 | { imagelist, initCallback } = this.props; 110 | 111 | this.arrLength = imagelist.length; 112 | this.list = this.refs['imagelist']; 113 | 114 | Transform(this.list); 115 | 116 | current && this.changeIndex(current, false); 117 | 118 | this.bindStyle(current); 119 | 120 | initCallback && initCallback(); 121 | } 122 | 123 | onSingleTap(){ 124 | this.props.close && this.props.close(); 125 | } 126 | 127 | onPressMove(evt){ 128 | const { current } = this.state; 129 | 130 | this.endAnimation(); 131 | 132 | if( !this.focused ){ 133 | if((current === 0 && evt.deltaX > 0) || (current === this.arrLength - 1 && evt.deltaX < 0)){ 134 | this.list.translateX += evt.deltaX / 3; 135 | }else{ 136 | this.list.translateX += evt.deltaX; 137 | } 138 | } 139 | 140 | evt.preventDefault(); 141 | } 142 | 143 | onSwipe(evt){ 144 | const { direction } = evt; 145 | 146 | let { current } = this.state; 147 | if( this.focused ){ 148 | return false; 149 | } 150 | switch(direction) { 151 | case 'Left': 152 | current < this.arrLength-1 && ++current && this.bindStyle(current); 153 | break; 154 | case 'Right': 155 | current > 0 && current-- && this.bindStyle(current); 156 | break; 157 | } 158 | this.changeIndex(current) 159 | } 160 | 161 | onPicPressMove(evt) { 162 | const { deltaX, deltaY } = evt, 163 | isLongPic = this.ob.getAttribute('long'), 164 | { scaleX, width } = this.ob; 165 | 166 | if(this.ob.scaleX <= 1 || evt.touches.length > 1){ 167 | return; 168 | } 169 | 170 | if(this.ob && this.checkBoundary(deltaX, deltaY)){ 171 | !isLongPic && (this.ob.translateX += deltaX); 172 | this.ob.translateY += deltaY; 173 | 174 | if(isLongPic && scaleX * width === this.screenWidth){ 175 | this.focused = false; 176 | }else{ 177 | this.focused = true; 178 | } 179 | }else { 180 | this.focused = false; 181 | } 182 | // console.log('translate ',this.ob.translateX, this.ob.translateY); 183 | } 184 | 185 | onMultipointStart(){ 186 | this.initScale = this.ob.scaleX; 187 | } 188 | 189 | onPinch(evt){ 190 | if( this.props.disablePinch || this.ob.getAttribute('long')){ 191 | return false; 192 | } 193 | this.ob.style.webkitTransition = 'cubic-bezier(.25,.01,.25,1)' 194 | 195 | const { originX, originY } = this.ob, 196 | originX2 = evt.center.x - this.screenWidth/2 - document.body.scrollLeft, 197 | originY2 = evt.center.y - this.screenHeight/2 - document.body.scrollTop; 198 | 199 | this.ob.originX = originX2; 200 | this.ob.originY = originY2; 201 | this.ob.translateX = this.ob.translateX + (originX2 - originX) * this.ob.scaleX; 202 | this.ob.translateY = this.ob.translateY + (originY2 - originY) * this.ob.scaleY; 203 | 204 | this.ob.scaleX = this.ob.scaleY = this.initScale * evt.scale; 205 | } 206 | 207 | onRotate(evt){ 208 | if( !this.props.enableRotate || this.ob.getAttribute('rate') >= 3.5){ 209 | return false; 210 | } 211 | 212 | this.ob.style.webkitTransition = 'cubic-bezier(.25,.01,.25,1)' 213 | 214 | this.ob.rotateZ += evt.angle; 215 | } 216 | 217 | onLongTap(){ 218 | this.props.longTap && this.props.longTap(); 219 | } 220 | 221 | onMultipointEnd(evt){ 222 | // translate to normal 223 | this.changeIndex(this.state.current); 224 | 225 | if(!this.ob){ 226 | return; 227 | } 228 | 229 | this.ob.style.webkitTransition = '300ms ease'; 230 | 231 | const { maxScale } = this.props, 232 | isLongPic = this.ob.getAttribute('long'); 233 | // scale to normal 234 | if (this.ob.scaleX < 1) { 235 | this.restore(false); 236 | } 237 | if (this.ob.scaleX > maxScale && !isLongPic){ 238 | this.setScale(maxScale); 239 | } 240 | 241 | // rotate to normal 242 | let rotation = this.ob.rotateZ % 360, 243 | rate = this.ob.getAttribute('rate'); 244 | 245 | if(rotation < 0){ 246 | rotation = 360 + rotation; 247 | } 248 | this.ob.rotateZ = rotation; 249 | 250 | if (rotation > 0 && rotation < 45) { 251 | this.ob.rotateZ = 0; 252 | } else if (rotation >= 315) { 253 | this.ob.rotateZ = 360; 254 | } else if (rotation >= 45 && rotation < 135) { 255 | this.ob.rotateZ = 90; 256 | this.setScale(rate); 257 | } else if (rotation >= 135 && rotation < 225) { 258 | this.ob.rotateZ = 180; 259 | } else if (rotation >= 225 && rotation < 315) { 260 | this.ob.rotateZ = 270; 261 | this.setScale(rate); 262 | } 263 | } 264 | 265 | onDoubleTap(evt){ 266 | if( this.props.disableDoubleTap ){ 267 | return false; 268 | } 269 | 270 | const { origin } = evt, 271 | originX = origin[0] - this.screenWidth/2 - document.body.scrollLeft, 272 | originY = origin[1] - this.screenHeight/2 - document.body.scrollTop, 273 | isLongPic = this.ob.getAttribute('long'); 274 | 275 | if(this.ob.scaleX === 1){ 276 | !isLongPic && (this.ob.translateX = this.ob.originX = originX); 277 | !isLongPic && (this.ob.translateY = this.ob.originY = originY); 278 | this.setScale(isLongPic ? this.screenWidth / this.ob.width : this.props.maxScale); 279 | }else{ 280 | this.ob.translateX = this.ob.originX; 281 | this.ob.translateY = this.ob.originY; 282 | this.setScale(1); 283 | } 284 | 285 | // console.log('origin',this.ob.originX, this.ob.originY); 286 | } 287 | 288 | bindStyle(current) { 289 | this.setState({ current }, () => { 290 | this.ob && this.restore(); 291 | this.ob = document.getElementById(`view${current}`); 292 | if(this.ob && !this.ob.scaleX){ 293 | Transform(this.ob) 294 | } 295 | // ease hide page number 296 | const page = this.refs.page; 297 | if(page){ 298 | page.classList.remove('hide'); 299 | setTimeout(()=>{ 300 | page.classList.add('hide'); 301 | }, 2000); 302 | } 303 | }) 304 | } 305 | 306 | changeIndex(current, ease=true) { 307 | ease && (this.list.style.webkitTransition = '300ms ease'); 308 | this.list.translateX = -current*(this.screenWidth + this.props.gap); 309 | 310 | this.props.changeIndex && this.props.changeIndex(current); 311 | } 312 | 313 | setScale(size) { 314 | this.ob.style.webkitTransition = '300ms ease-in-out'; 315 | this.ob.scaleX = this.ob.scaleY = size; 316 | } 317 | 318 | restore(rotate=true) { 319 | this.ob.translateX = this.ob.translateY = 0; 320 | !!rotate && (this.ob.rotateZ = 0); 321 | this.ob.scaleX = this.ob.scaleY = 1; 322 | this.ob.originX = this.ob.originY = 0; 323 | } 324 | 325 | endAnimation() { 326 | this.list.style.webkitTransition = '0'; 327 | this.ob && this.ob.style && (this.ob.style.webkitTransition = '0'); 328 | } 329 | 330 | checkBoundary(deltaX = 0, deltaY = 0) { 331 | // console.log(this.ob.width, this.ob.height); 332 | const { scaleX, translateX, translateY, originX, originY, width, height } = this.ob, 333 | rate = this.ob.getAttribute('rate'); 334 | 335 | if(scaleX !== 1 || scaleX !== rate){ 336 | // include long picture 337 | const rangeLeft = (scaleX - 1) * (width / 2 + originX) + originX, 338 | rangeRight = -(scaleX - 1) * (width / 2 - originX) + originX, 339 | rangeUp = (scaleX - 1) * (height / 2 + originY) + originY, 340 | rangeDown = -(scaleX - 1) * (height / 2 - originY) + originY; 341 | 342 | // console.log(rangeLeft, rangeRight, rangeUp, rangeDown); 343 | 344 | if(translateX + deltaX <= rangeLeft 345 | && translateX + deltaX >= rangeRight 346 | && translateY + deltaY <= rangeUp 347 | && translateY + deltaY >= rangeDown ) { 348 | return true; 349 | } 350 | } 351 | return false; 352 | } 353 | } 354 | 355 | export const SingleImgView = new Singleton(ImageView) 356 | 357 | export default ImageView 358 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | html, body, div, ul, li, a { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | .hide { 7 | opacity: 0; 8 | transition: opacity 0.2s; 9 | -webkit-transition: opacity 0.2s; 10 | } 11 | 12 | .imageview { 13 | position: fixed; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: 0; 18 | width: 100%; 19 | height: 100%; 20 | z-index: 900; 21 | background-color: #000; 22 | overflow: hidden; 23 | animation: easeshow 0.25s; 24 | 25 | .page { 26 | font-family: -apple-system-font, 'Helvetica Neue', Helvetica, STHeiTi,sans-serif; 27 | position: fixed; 28 | font-size: 14px; 29 | color: #fff; 30 | padding: 2px 5px; 31 | bottom: 10px; 32 | left: 50%; 33 | -webkit-transform: translateX(-50%); 34 | transform: translateX(-50%); 35 | -webkit-touch-callout: none; 36 | -webkit-user-select: none; 37 | } 38 | 39 | .spinner { 40 | width: 40px; 41 | height: 40px; 42 | position: absolute; 43 | top: 45%; 44 | left: 50%; 45 | transform: translate(-50%,-50%); 46 | } 47 | 48 | .double-bounce1, .double-bounce2 { 49 | width: 100%; 50 | height: 100%; 51 | border-radius: 50%; 52 | background-color: #333; 53 | opacity: 0.6; 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | 58 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 59 | animation: sk-bounce 2.0s infinite ease-in-out; 60 | } 61 | 62 | .double-bounce2 { 63 | -webkit-animation-delay: -1.0s; 64 | animation-delay: -1.0s; 65 | } 66 | 67 | .errorpage { 68 | position: absolute; 69 | font-size: 16px; 70 | text-align: center; 71 | color: rgb(170, 170, 170); 72 | top: 28%; 73 | left: 50%; 74 | margin-left: -70px; 75 | 76 | &:before { 77 | content:''; 78 | display: block; 79 | width: 150px; 80 | height: 140px; 81 | margin: 0 auto; 82 | padding-bottom: 20px; 83 | background: url('./net-error.png') no-repeat; 84 | background-size: 100%; 85 | opacity: .4; 86 | } 87 | } 88 | } 89 | 90 | @keyframes easeshow { 91 | from { opacity: 0; } 92 | to { opacity: 1; } 93 | } 94 | @-webkit-keyframes easeshow { 95 | from { opacity: 0; } 96 | to { opacity: 1; } 97 | } 98 | 99 | .imagelist { 100 | display: -webkit-box; 101 | display: box; 102 | height: 100%; 103 | list-style-type: none; 104 | -webkit-touch-callout: none; 105 | -webkit-user-select: none; 106 | .imagelist-item { 107 | display: -webkit-box; 108 | -webkit-box-pack: center; 109 | -webkit-box-align: center; 110 | width: 100%; 111 | height: 100%; 112 | text-align: center; 113 | position: relative; 114 | background-color: #000; 115 | overflow-y: scroll; 116 | 117 | .imagelist-item-img { 118 | position: absolute; 119 | top: 0; 120 | left: 0; 121 | max-width: 100%; 122 | -webkit-touch-callout: none; 123 | -webkit-user-select: none; 124 | } 125 | } 126 | } 127 | 128 | @-webkit-keyframes sk-bounce { 129 | 0%, 100% { -webkit-transform: scale(0.0) } 130 | 50% { -webkit-transform: scale(1.0) } 131 | } 132 | 133 | @keyframes sk-bounce { 134 | 0%, 100% { 135 | transform: scale(0.0); 136 | -webkit-transform: scale(0.0); 137 | } 50% { 138 | transform: scale(1.0); 139 | -webkit-transform: scale(1.0); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/libs/alloyfinger.js: -------------------------------------------------------------------------------- 1 | /* AlloyFinger v0.1.0 2 | * By dntzhang 3 | * Reedited by nemoliao 4 | * Github: https://github.com/AlloyTeam/AlloyFinger 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | 9 | export default class AlloyFinger extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.preV = { x: null, y: null }; 14 | this.pinchStartLen = null; 15 | this.scale = 1; 16 | this.isDoubleTap = false; 17 | this.delta = null; 18 | this.last = null; 19 | this.now = null; 20 | this.end = null; 21 | this.multiTouch = false; 22 | this.tapTimeout = null; 23 | this.longTapTimeout = null; 24 | this.singleTapTimeout = null; 25 | this.swipeTimeout=null; 26 | this.x1 = this.x2 = this.y1 = this.y2 = null; 27 | this.preTapPosition={x:null,y:null}; 28 | } 29 | 30 | getLen(v) { 31 | return Math.sqrt(v.x * v.x + v.y * v.y); 32 | } 33 | 34 | dot(v1, v2) { 35 | return v1.x * v2.x + v1.y * v2.y; 36 | } 37 | 38 | getAngle(v1, v2) { 39 | var mr = this.getLen(v1) * this.getLen(v2); 40 | if (mr === 0) return 0; 41 | var r = this.dot(v1, v2) / mr; 42 | if (r > 1) r = 1; 43 | return Math.acos(r); 44 | } 45 | 46 | cross(v1, v2) { 47 | return v1.x * v2.y - v2.x * v1.y; 48 | } 49 | 50 | getRotateAngle(v1, v2) { 51 | var angle = this.getAngle(v1, v2); 52 | if (this.cross(v1, v2) > 0) { 53 | angle *= -1; 54 | } 55 | 56 | return angle * 180 / Math.PI; 57 | } 58 | 59 | _resetState() { 60 | this.setState({ 61 | x: null, 62 | y: null, 63 | swiping: false, 64 | start: 0 65 | }); 66 | } 67 | 68 | 69 | _emitEvent(name, ...arg) { 70 | if (this.props[name]) { 71 | this.props[name](...arg); 72 | } 73 | } 74 | 75 | _handleTouchStart (evt) { 76 | 77 | if(!evt.touches) return; 78 | this.now = Date.now(); 79 | this.x1 = evt.touches[0].pageX; 80 | this.y1 = evt.touches[0].pageY; 81 | this.delta = this.now - (this.last || this.now); 82 | if(this.preTapPosition.x!==null){ 83 | this.isDoubleTap = (this.delta > 0 && this.delta <= 250&&Math.abs(this.preTapPosition.x-this.x1)<30&&Math.abs(this.preTapPosition.y-this.y1)<30); 84 | } 85 | this.preTapPosition.x=this.x1; 86 | this.preTapPosition.y=this.y1; 87 | this.last = this.now; 88 | var preV = this.preV, 89 | len = evt.touches.length; 90 | 91 | if (len > 1) { 92 | this._cancelLongTap(); 93 | this._cancelSingleTap(); 94 | var v = { x: evt.touches[1].pageX - this.x1, y: evt.touches[1].pageY - this.y1 }; 95 | preV.x = v.x; 96 | preV.y = v.y; 97 | this.pinchStartLen = this.getLen(preV); 98 | this._emitEvent('onMultipointStart', evt); 99 | } 100 | this.longTapTimeout = setTimeout(() => { 101 | this._emitEvent('onLongTap', evt); 102 | }, 750); 103 | } 104 | 105 | _handleTouchMove(evt){ 106 | var preV = this.preV, 107 | len = evt.touches.length, 108 | currentX = evt.touches[0].pageX, 109 | currentY = evt.touches[0].pageY; 110 | this.isDoubleTap=false; 111 | if (len > 1) { 112 | var v = { x: evt.touches[1].pageX - currentX, y: evt.touches[1].pageY - currentY }; 113 | if (preV.x !== null) { 114 | if (this.pinchStartLen > 0) { 115 | evt.center = { 116 | x: (evt.touches[1].pageX + currentX) / 2, 117 | y: (evt.touches[1].pageY + currentY) / 2 118 | }; 119 | evt.scale = this.getLen(v) / this.pinchStartLen; 120 | this._emitEvent('onPinch', evt); 121 | } 122 | evt.angle = this.getRotateAngle(v, preV); 123 | this._emitEvent('onRotate', evt); 124 | } 125 | preV.x = v.x; 126 | preV.y = v.y; 127 | this.multiTouch = true; 128 | } else { 129 | if (this.x2 !== null) { 130 | evt.deltaX = currentX - this.x2; 131 | evt.deltaY = currentY - this.y2; 132 | }else{ 133 | evt.deltaX = 0; 134 | evt.deltaY = 0; 135 | } 136 | this._emitEvent('onPressMove', evt); 137 | } 138 | this._cancelLongTap(); 139 | this.x2 = currentX; 140 | this.y2 = currentY; 141 | 142 | if(len > 1) { 143 | evt.preventDefault(); 144 | } 145 | } 146 | 147 | _handleTouchCancel(){ 148 | clearInterval(this.singleTapTimeout); 149 | clearInterval(this.tapTimeout); 150 | clearInterval(this.longTapTimeout); 151 | clearInterval(this.swipeTimeout); 152 | } 153 | 154 | _handleTouchEnd(evt){ 155 | 156 | this.end = Date.now(); 157 | this._cancelLongTap(); 158 | 159 | if( evt.touches.length<2){ 160 | this._emitEvent('onMultipointEnd', evt); 161 | } 162 | 163 | evt.origin = [this.x1, this.y1]; 164 | if(this.multiTouch === false){ 165 | if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) || 166 | (this.y2 && Math.abs(this.preV.y - this.y2) > 30)) { 167 | evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2); 168 | evt.distance = Math.abs(this.x1 - this.x2); 169 | this.swipeTimeout = setTimeout(() => { 170 | this._emitEvent('onSwipe', evt); 171 | }, 0) 172 | } else { 173 | this.tapTimeout = setTimeout(() => { 174 | this._emitEvent('onTap', evt); 175 | if (this.isDoubleTap) { 176 | this._emitEvent('onDoubleTap', evt); 177 | clearTimeout(this.singleTapTimeout); 178 | this.isDoubleTap = false; 179 | } else { 180 | this.singleTapTimeout = setTimeout(()=>{ 181 | this._emitEvent('onSingleTap', evt); 182 | }, 250); 183 | } 184 | }, 0) 185 | } 186 | } 187 | 188 | this.preV.x = 0; 189 | this.preV.y = 0; 190 | this.scale = 1; 191 | this.pinchStartLen = null; 192 | this.x1 = this.x2 = this.y1 = this.y2 = null; 193 | this.multiTouch = false; 194 | } 195 | 196 | _cancelLongTap () { 197 | clearTimeout(this.longTapTimeout); 198 | } 199 | 200 | _cancelSingleTap () { 201 | clearTimeout(this.singleTapTimeout); 202 | } 203 | 204 | _swipeDirection (x1, x2, y1, y2) { 205 | if(Math.abs(x1 - x2) > 80 || this.end-this.now < 250){ 206 | return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down') 207 | }else { 208 | return 'Nochange' 209 | } 210 | 211 | } 212 | 213 | render() { 214 | return React.cloneElement(React.Children.only(this.props.children), { 215 | onTouchStart: this._handleTouchStart.bind(this), 216 | onTouchMove: this._handleTouchMove.bind(this), 217 | onTouchCancel: this._handleTouchCancel.bind(this), 218 | onTouchEnd: this._handleTouchEnd.bind(this) 219 | }); 220 | } 221 | } -------------------------------------------------------------------------------- /src/libs/transform.js: -------------------------------------------------------------------------------- 1 | /* transformjs 2 | * By dntzhang 3 | */ 4 | 5 | 6 | var Matrix3D = function (n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44) { 7 | this.elements =window.Float32Array ? new Float32Array(16) : []; 8 | var te = this.elements; 9 | te[0] = (n11 !== undefined) ? n11 : 1; te[4] = n12 || 0; te[8] = n13 || 0; te[12] = n14 || 0; 10 | te[1] = n21 || 0; te[5] = (n22 !== undefined) ? n22 : 1; te[9] = n23 || 0; te[13] = n24 || 0; 11 | te[2] = n31 || 0; te[6] = n32 || 0; te[10] = (n33 !== undefined) ? n33 : 1; te[14] = n34 || 0; 12 | te[3] = n41 || 0; te[7] = n42 || 0; te[11] = n43 || 0; te[15] = (n44 !== undefined) ? n44 : 1; 13 | }; 14 | 15 | Matrix3D.DEG_TO_RAD = Math.PI / 180; 16 | 17 | Matrix3D.prototype = { 18 | set: function (n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44) { 19 | var te = this.elements; 20 | te[0] = n11; te[4] = n12; te[8] = n13; te[12] = n14; 21 | te[1] = n21; te[5] = n22; te[9] = n23; te[13] = n24; 22 | te[2] = n31; te[6] = n32; te[10] = n33; te[14] = n34; 23 | te[3] = n41; te[7] = n42; te[11] = n43; te[15] = n44; 24 | return this; 25 | }, 26 | identity: function () { 27 | this.set( 28 | 1, 0, 0, 0, 29 | 0, 1, 0, 0, 30 | 0, 0, 1, 0, 31 | 0, 0, 0, 1 32 | ); 33 | return this; 34 | }, 35 | multiplyMatrices: function (a, be) { 36 | 37 | var ae = a.elements; 38 | var te = this.elements; 39 | var a11 = ae[0], a12 = ae[4], a13 = ae[8], a14 = ae[12]; 40 | var a21 = ae[1], a22 = ae[5], a23 = ae[9], a24 = ae[13]; 41 | var a31 = ae[2], a32 = ae[6], a33 = ae[10], a34 = ae[14]; 42 | var a41 = ae[3], a42 = ae[7], a43 = ae[11], a44 = ae[15]; 43 | 44 | var b11 = be[0], b12 = be[1], b13 = be[2], b14 = be[3]; 45 | var b21 = be[4], b22 = be[5], b23 = be[6], b24 = be[7]; 46 | var b31 = be[8], b32 = be[9], b33 = be[10], b34 = be[11]; 47 | var b41 = be[12], b42 = be[13], b43 = be[14], b44 = be[15]; 48 | 49 | te[0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41; 50 | te[4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42; 51 | te[8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43; 52 | te[12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44; 53 | 54 | te[1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41; 55 | te[5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42; 56 | te[9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43; 57 | te[13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44; 58 | 59 | te[2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41; 60 | te[6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42; 61 | te[10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43; 62 | te[14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44; 63 | 64 | te[3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41; 65 | te[7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42; 66 | te[11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43; 67 | te[15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44; 68 | 69 | return this; 70 | 71 | }, 72 | // 解决角度为90的整数倍导致Math.cos得到极小的数,其实是0。导致不渲染 73 | _rounded: function(value,i){ 74 | i= Math.pow(10, i || 15); 75 | // default 76 | return Math.round(value*i)/i; 77 | }, 78 | appendTransform: function (x, y, z, scaleX, scaleY, scaleZ, rotateX, rotateY, rotateZ,skewX,skewY, originX, originY, originZ) { 79 | 80 | var rx = rotateX * Matrix3D.DEG_TO_RAD; 81 | var cosx =this._rounded( Math.cos(rx)); 82 | var sinx = this._rounded(Math.sin(rx)); 83 | var ry = rotateY * Matrix3D.DEG_TO_RAD; 84 | var cosy =this._rounded( Math.cos(ry)); 85 | var siny = this._rounded(Math.sin(ry)); 86 | var rz = rotateZ * Matrix3D.DEG_TO_RAD; 87 | var cosz =this._rounded( Math.cos(rz * -1)); 88 | var sinz =this._rounded( Math.sin(rz * -1)); 89 | 90 | this.multiplyMatrices(this, [ 91 | 1, 0, 0, x, 92 | 0, cosx, sinx, y, 93 | 0, -sinx, cosx, z, 94 | 0, 0, 0, 1 95 | ]); 96 | 97 | this.multiplyMatrices(this, [ 98 | cosy, 0, siny, 0, 99 | 0, 1, 0, 0, 100 | -siny, 0, cosy, 0, 101 | 0, 0, 0, 1 102 | ]); 103 | 104 | this.multiplyMatrices(this,[ 105 | cosz * scaleX, sinz * scaleY, 0, 0, 106 | -sinz * scaleX, cosz * scaleY, 0, 0, 107 | 0, 0, 1 * scaleZ, 0, 108 | 0, 0, 0, 1 109 | ]); 110 | 111 | if(skewX||skewY){ 112 | this.multiplyMatrices(this,[ 113 | this._rounded(Math.cos(skewX* Matrix3D.DEG_TO_RAD)), this._rounded( Math.sin(skewX* Matrix3D.DEG_TO_RAD)), 0, 0, 114 | -1*this._rounded(Math.sin(skewY* Matrix3D.DEG_TO_RAD)), this._rounded( Math.cos(skewY* Matrix3D.DEG_TO_RAD)), 0, 0, 115 | 0, 0, 1, 0, 116 | 0, 0, 0, 1 117 | ]); 118 | } 119 | 120 | if (originX || originY || originZ) { 121 | this.elements[12] -= originX * this.elements[0] + originY * this.elements[4] + originZ * this.elements[8]; 122 | this.elements[13] -= originX * this.elements[1] + originY * this.elements[5] + originZ * this.elements[9]; 123 | this.elements[14] -= originX * this.elements[2] + originY * this.elements[6] + originZ * this.elements[10]; 124 | } 125 | return this; 126 | } 127 | }; 128 | 129 | function observe(target, props, callback) { 130 | for (var i = 0, len = props.length; i < len; i++) { 131 | var prop = props[i]; 132 | watch(target, prop, callback); 133 | } 134 | } 135 | 136 | function watch(target, prop, callback) { 137 | Object.defineProperty(target, prop, { 138 | get: function () { 139 | return this["__" + prop]; 140 | }, 141 | set: function (value) { 142 | if (value !== this["__" + prop]) { 143 | this["__" + prop] = value; 144 | callback(); 145 | } 146 | 147 | } 148 | }); 149 | } 150 | 151 | var Transform = function (element) { 152 | 153 | observe( 154 | element, 155 | ["translateX", "translateY", "translateZ", "scaleX", "scaleY", "scaleZ" , "rotateX", "rotateY", "rotateZ","skewX","skewY", "originX", "originY", "originZ"], 156 | function () { 157 | var mtx = element.matrix3D.identity().appendTransform( element.translateX, element.translateY, element.translateZ, element.scaleX, element.scaleY, element.scaleZ, element.rotateX, element.rotateY, element.rotateZ,element.skewX,element.skewY, element.originX, element.originY, element.originZ); 158 | element.style.transform = element.style.msTransform = element.style.OTransform = element.style.MozTransform = element.style.webkitTransform = "perspective("+element.perspective+"px) matrix3d(" + Array.prototype.slice.call(mtx.elements).join(",") + ")"; 159 | }); 160 | 161 | observe( 162 | element, 163 | [ "perspective"], 164 | function () { 165 | element.style.transform = element.style.msTransform = element.style.OTransform = element.style.MozTransform = element.style.webkitTransform = "perspective("+element.perspective+"px) matrix3d(" + Array.prototype.slice.call(element.matrix3D.elements).join(",") + ")"; 166 | }); 167 | 168 | element.matrix3D = new Matrix3D(); 169 | element.perspective = 500; 170 | element.scaleX = element.scaleY = element.scaleZ = 1; 171 | //由于image自带了x\y\z,所有加上translate前缀 172 | element.translateX = element.translateY = element.translateZ = element.rotateX = element.rotateY = element.rotateZ =element.skewX=element.skewY= element.originX = element.originY = element.originZ = 0; 173 | } 174 | 175 | module.exports = Transform; 176 | -------------------------------------------------------------------------------- /src/net-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Caesor/react-imageview/de08f947ad074e24cf07c9999baf87f70dec0aef/src/net-error.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | webpack = require('webpack'), 3 | ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var config = { 6 | entry: { 7 | "react-imageview": './src/index.js' 8 | }, 9 | externals: { 10 | "react": "react", 11 | "react-dom": "react-dom", 12 | "react-singleton": "react-singleton" 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.css$/, 18 | loader: ExtractTextPlugin.extract("style-loader", "css-loader") 19 | }, 20 | { 21 | test: /\.less/, 22 | loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader") 23 | }, 24 | { 25 | test: /\.(png|jpg|gif|woff|woff2)$/, 26 | loader: 'url-loader?limit=8192' 27 | }, 28 | { 29 | test: /\.jsx?$/, 30 | loader: 'babel', 31 | exclude: /node_modules/, 32 | include: [__dirname] 33 | } 34 | ] 35 | }, 36 | output: { 37 | path: 'dist/', 38 | library: 'react-imageview', 39 | libraryTarget: 'commonjs2' 40 | }, 41 | plugins: [ 42 | new webpack.optimize.DedupePlugin() 43 | ] 44 | } 45 | 46 | if(process.env.NODE_ENV === 'production') { 47 | config.output.filename = '[name].min.js'; 48 | config.plugins = config.plugins.concat( 49 | new webpack.optimize.UglifyJsPlugin({ 50 | compress: { 51 | warnings: false 52 | } 53 | }), 54 | new ExtractTextPlugin('[name].min.css') 55 | ); 56 | }else { 57 | config.output.filename = '[name].js'; 58 | config.plugins.push(new ExtractTextPlugin('[name].css')); 59 | } 60 | 61 | module.exports = config 62 | --------------------------------------------------------------------------------