├── .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 |
33 | { imagelist.map((item, i)=>{
34 | return (
)
35 | })}
36 |
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 |
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 |
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 |
--------------------------------------------------------------------------------