├── .eslintignore
├── example
├── assets
│ ├── 0.jpg
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── 3.jpg
│ └── 4.jpg
├── App.less
├── index.html
├── main.js
└── App.jsx
├── src
├── index.js
└── components
│ ├── Loading.jsx
│ ├── Pointer.jsx
│ ├── WxImageViewer.jsx
│ ├── WrapViewer.jsx
│ ├── WxImageViewer.css
│ ├── ListContainer.jsx
│ ├── tween.js
│ └── ImageContainer.jsx
├── .vscode
└── settings.json
├── calculate.js
├── postcss.config.js
├── index.html
├── LICENSE
├── .gitignore
├── .npmignore
├── .eslintrc
├── package.json
├── webpack.config.example.js
├── gulpfile.js
├── webpack.config.js
├── README.md
└── README-cn.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 |
3 | .vscode
4 |
5 | .publish
6 |
7 | demo
8 |
9 | gulpfile.js
--------------------------------------------------------------------------------
/example/assets/0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-ld/react-wx-images-viewer/HEAD/example/assets/0.jpg
--------------------------------------------------------------------------------
/example/assets/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-ld/react-wx-images-viewer/HEAD/example/assets/1.jpg
--------------------------------------------------------------------------------
/example/assets/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-ld/react-wx-images-viewer/HEAD/example/assets/2.jpg
--------------------------------------------------------------------------------
/example/assets/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-ld/react-wx-images-viewer/HEAD/example/assets/3.jpg
--------------------------------------------------------------------------------
/example/assets/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-ld/react-wx-images-viewer/HEAD/example/assets/4.jpg
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import WxImageViewer from './components/WxImageViewer';
2 |
3 | export default WxImageViewer;
--------------------------------------------------------------------------------
/example/App.less:
--------------------------------------------------------------------------------
1 |
2 | .app{
3 | .img-list{
4 | display: flex; justify-content: space-between; flex-wrap: wrap;
5 | .img{
6 | width: 33.3%;
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = (props) => {
4 | return (
5 |
6 |
7 | );
8 | };
9 |
10 | export default Loading;
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.autoFixOnSave": true,
3 | "files.autoSave": "off",
4 | "eslint.validate": [
5 | "javascript",
6 | "javascriptreact"
7 | ],
8 | "javascript.implicitProjectConfig.experimentalDecorators": true
9 | }
--------------------------------------------------------------------------------
/calculate.js:
--------------------------------------------------------------------------------
1 | var y,
2 | x,
3 | start = 0,
4 | end = 300,
5 | time = 147,
6 | distance = 1000,
7 | json = "[";
8 |
9 | for(x = start; x < end;){
10 | // y = x * Math.sqrt(distance) / Math.sqrt(time);
11 | y = x * x ;
12 | var str = '[' + x + ',' + y + '],\n';
13 | json+=str;
14 | x = x+10
15 | }
16 | json +="]"
17 | console.info(json);
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WxImageViewer demo
6 |
7 |
8 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = ({ file, options, env }) => ({
2 | // parser: file.extname === '.sss' ? 'sugarss' : false,
3 | // plugins: {
4 | // 'postcss-import': { root: file.dirname },
5 | // 'postcss-cssnext': options.cssnext ? options.cssnext : false,
6 | // 'autoprefixer': env == 'production' ? options.autoprefixer : false,
7 | // 'cssnano': env === 'production' ? options.cssnano : false
8 | // }
9 | plugins: [ require('autoprefixer')({ browsers: ["Android >= 4", "iOS >= 7"]}) ]
10 | })
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ReactPullLoad demo
6 |
7 |
8 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/example/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { AppContainer } from 'react-hot-loader';
5 | // AppContainer 是一个 HMR 必须的包裹(wrapper)组件
6 |
7 | import App from './App.jsx';
8 |
9 | const render = (Component) => {
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 | };
17 |
18 | render(App);
19 |
20 | // 模块热替换的 API
21 | if (module.hot) {
22 | module.hot.accept('./App.jsx', () => {
23 | render(App);
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Pointer.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Pointer extends PureComponent {
5 | static propTypes = {
6 | length: PropTypes.number.isRequired,
7 | index: PropTypes.number.isRequired,
8 | changeIndex: PropTypes.func
9 | }
10 |
11 | render() {
12 | console.info("Point render")
13 |
14 | const {
15 | length,
16 | changeIndex,
17 | index
18 | } = this.props
19 |
20 | let i = 0, items = [];
21 | for (i; i < length; i++) {
22 | if (i === index) {
23 | items.push();
24 | } else {
25 | items.push();
26 | }
27 | }
28 |
29 | return (
30 |
31 | {items}
32 |
33 | );
34 | }
35 | }
36 |
37 | export default Pointer;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 react-ld
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | demo
61 |
62 | deploy.config.json
63 |
64 | dist
65 |
66 | .publish
67 |
68 | package-lock.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | demo
61 |
62 | deploy.config.json
63 |
64 | .publish
65 |
66 | example
67 |
68 | postcss.config.js
69 |
70 | webpack.config.example.js
71 |
72 | webpack.config.js
73 |
74 | index.html
75 |
76 | calculate.js
77 |
78 | gulpfile.js
--------------------------------------------------------------------------------
/src/components/WxImageViewer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import PropTypes from 'prop-types';
4 |
5 | import WrapViewer from './WrapViewer';
6 |
7 | import './WxImageViewer.css';
8 |
9 | class WxImageViewer extends Component {
10 | static propTypes = {
11 | maxZoomNum: PropTypes.number, // 最大放大倍数
12 | zIndex: PropTypes.number, // 组件图层深度
13 | index: PropTypes.number, // 当前显示图片的http链接
14 | urls: PropTypes.array.isRequired, // 需要预览的图片http链接列表
15 | gap: PropTypes.number, // 间隙
16 | speed: PropTypes.number, // Duration of transition between slides (in ms)
17 | onClose: PropTypes.func.isRequired, // 关闭组件回调
18 | }
19 |
20 | static childContextTypes = {
21 | onClose: PropTypes.func,
22 | };
23 |
24 | static defaultProps = {
25 | maxZoomNum: 4,
26 | zIndex: 100,
27 | index: 0,
28 | gap: 10,
29 | speed: 300,
30 | }
31 |
32 | constructor(props) {
33 | super(props);
34 | this.node = document.createElement('div');
35 | }
36 |
37 | getChildContext() {
38 | return { onClose: this.props.onClose };
39 | }
40 |
41 | componentDidMount() {
42 | document.body.appendChild(this.node);
43 | }
44 |
45 | componentWillUnmount() {
46 | document.body.removeChild(this.node);
47 | }
48 |
49 | render() {
50 | return ReactDOM.createPortal(
51 | ,
54 | this.node,
55 | );
56 | }
57 | }
58 |
59 | export default WxImageViewer;
60 |
--------------------------------------------------------------------------------
/example/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | // import { render } from 'react-dom';
3 | // import PropTypes from 'prop-types';
4 |
5 | import WxImageViewer from 'index.js';
6 | import './App.less';
7 |
8 | class App extends Component {
9 | state = {
10 | imags: [
11 | /* eslint-disable */
12 | require('./assets/2.jpg'),
13 | require('./assets/1.jpg'),
14 | require('./assets/0.jpg'),
15 | require('./assets/3.jpg'),
16 | require('./assets/4.jpg'),
17 | /* eslint-enable */
18 | ],
19 | index: 0,
20 | isOpen: false,
21 | };
22 |
23 | onClose = () => {
24 | this.setState({
25 | isOpen: false,
26 | });
27 | }
28 |
29 | openViewer(index) {
30 | this.setState({
31 | index,
32 | isOpen: true,
33 | });
34 | }
35 |
36 | render() {
37 | const {
38 | imags,
39 | index,
40 | isOpen,
41 | } = this.state;
42 |
43 | return (
44 |
45 |
46 |
47 | {
48 | imags.map((item, subIndex) => {
49 | return (
50 |
51 |

52 |
53 | );
54 | })
55 | }
56 |
57 | {
58 | isOpen ?
: ''
59 | }
60 |
61 | );
62 | }
63 | }
64 |
65 | export default App;
66 |
--------------------------------------------------------------------------------
/src/components/WrapViewer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ListContainer from './ListContainer';
5 | import Pointer from './Pointer';
6 |
7 | const screenWidth = typeof document !== 'undefined' && document.documentElement.clientWidth;
8 | const screenHeight = typeof document !== 'undefined' && document.documentElement.clientHeight;
9 |
10 | class WrapViewer extends Component {
11 | static propTypes = {
12 | index: PropTypes.number.isRequired, // 当前显示图片的http链接
13 | urls: PropTypes.array.isRequired, // 需要预览的图片http链接列表
14 | maxZoomNum: PropTypes.number.isRequired, // 最大放大倍数
15 | zIndex: PropTypes.number.isRequired, // 组件图层深度
16 | gap: PropTypes.number.isRequired, // 间隙
17 | speed: PropTypes.number.isRequired, // Duration of transition between slides (in ms)
18 | }
19 |
20 | state = {
21 | index: 0,
22 | }
23 |
24 | componentWillMount() {
25 | const {
26 | index,
27 | } = this.props;
28 |
29 | this.setState({
30 | index,
31 | });
32 | }
33 |
34 | changeIndex = (index) => {
35 | console.info('changeIndex index = ', index);
36 | this.setState({
37 | index,
38 | });
39 | }
40 |
41 | render() {
42 | const {
43 | zIndex,
44 | urls,
45 | maxZoomNum,
46 | gap,
47 | speed,
48 | } = this.props;
49 |
50 | const {
51 | index,
52 | } = this.state;
53 |
54 | return (
55 | {/* root */}
56 |
57 |
67 |
68 |
69 | );
70 | }
71 | }
72 |
73 | export default WrapViewer;
74 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "plugins": ["compat"],
5 | "env": {
6 | "browser": true,
7 | "node": true,
8 | "es6": true,
9 | "mocha": true,
10 | "jest": true,
11 | "jasmine": true
12 | },
13 | "rules": {
14 | "generator-star-spacing": [0],
15 | "consistent-return": [0],
16 | "react/forbid-prop-types": [0],
17 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
18 | "global-require": [1],
19 | "import/prefer-default-export": [0],
20 | "react/jsx-no-bind": [0],
21 | "react/prop-types": [0],
22 | "react/prefer-stateless-function": [0],
23 | "react/jsx-wrap-multilines": ["error", {
24 | "declaration": "parens-new-line",
25 | "assignment": "parens-new-line",
26 | "return": "parens-new-line",
27 | "arrow": "parens-new-line",
28 | "condition": "parens-new-line",
29 | "logical": "parens-new-line",
30 | "prop": "ignore"
31 | }],
32 | "react/no-multi-comp": [0],
33 | "no-else-return": [0],
34 | "no-restricted-syntax": [0],
35 | "import/no-extraneous-dependencies": [0],
36 | "no-use-before-define": [0],
37 | "jsx-a11y/no-static-element-interactions": [0],
38 | "jsx-a11y/no-noninteractive-element-interactions": [0],
39 | "jsx-a11y/click-events-have-key-events": [0],
40 | "jsx-a11y/anchor-is-valid": [0],
41 | "no-nested-ternary": [0],
42 | "arrow-body-style": [0],
43 | "import/extensions": [0],
44 | "no-bitwise": [0],
45 | "no-cond-assign": [0],
46 | "import/no-unresolved": [0],
47 | "comma-dangle": ["error", {
48 | "arrays": "always-multiline",
49 | "objects": "always-multiline",
50 | "imports": "always-multiline",
51 | "exports": "always-multiline",
52 | "functions": "ignore"
53 | }],
54 | "object-curly-newline": [0],
55 | "function-paren-newline": [0],
56 | "no-restricted-globals": [0],
57 | "require-yield": [1],
58 | "compat/compat": "error",
59 | "no-console": [0],
60 | "no-underscore-dangle": [0]
61 | },
62 | "parserOptions": {
63 | "ecmaFeatures": {
64 | "experimentalObjectRestSpread": true
65 | }
66 | },
67 | "settings": {
68 | "polyfills": ["fetch", "promises"]
69 | }
70 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-wx-images-viewer",
3 | "version": "1.0.6",
4 | "description": "Images viewer is a react component use in mobile website App, that function same as Weixin native viewer.",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server --config webpack.config.js",
8 | "example": "rm -rf ./demo/* & NODE_ENV=development webpack --config webpack.config.example.js",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/react-ld/react-wx-images-viewer.git"
14 | },
15 | "keywords": [
16 | "react",
17 | "photos",
18 | "viewer",
19 | "mobile"
20 | ],
21 | "author": "dainli",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/react-ld/react-wx-images-viewer/issues"
25 | },
26 | "homepage": "https://github.com/react-ld/react-wx-images-viewer#readme",
27 | "devDependencies": {
28 | "autoprefixer": "^7.1.1",
29 | "babel-core": "^6.24.1",
30 | "babel-eslint": "^8.2.1",
31 | "babel-loader": "^7.0.0",
32 | "babel-preset-es2015": "^6.24.1",
33 | "babel-preset-react": "^6.24.1",
34 | "babel-preset-stage-1": "^6.24.1",
35 | "css-loader": "^0.28.4",
36 | "eslint": "^4.17.0",
37 | "eslint-config-airbnb": "^16.1.0",
38 | "eslint-plugin-babel": "^4.1.2",
39 | "eslint-plugin-compat": "^2.2.0",
40 | "eslint-plugin-import": "^2.8.0",
41 | "eslint-plugin-jsx-a11y": "^6.0.3",
42 | "eslint-plugin-react": "^7.6.1",
43 | "file-loader": "^0.11.1",
44 | "gulp": "^3.9.1",
45 | "gulp-babel": "^6.1.2",
46 | "gulp-clean": "^0.3.2",
47 | "gulp-gh-pages-will": "^0.5.4",
48 | "gulp-less": "^3.3.2",
49 | "gulp-util": "^3.0.8",
50 | "html-webpack-plugin": "^2.28.0",
51 | "less": "^2.7.2",
52 | "less-loader": "^4.0.4",
53 | "postcss-loader": "^2.0.5",
54 | "style-loader": "^0.18.1",
55 | "url-loader": "^0.5.8",
56 | "vinyl-ftp": "^0.6.0",
57 | "webpack": "^2.6.1",
58 | "react": "^16.0.0",
59 | "react-dom": "^16.0.0",
60 | "react-hot-loader": "^3.0.0-beta.7",
61 | "webpack-dev-server": "^2.4.5"
62 | },
63 | "dependencies": {
64 | "prop-types": "^15.6.0",
65 | "raf": "^3.1.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/webpack.config.example.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var port = 3010
5 |
6 | module.exports = {
7 | context: path.resolve(__dirname, 'example'), // string(绝对路径!)
8 | devtool: 'eval',
9 | cache: true,
10 | entry: [
11 | './main.js'
12 | ],
13 | output: {
14 | path: path.join(__dirname, 'demo/'),
15 | filename: '[name].js', //.[hash]
16 | // publicPath: 'http://public.dainli.com/17zt/viewer/assets/'
17 | },
18 | plugins: [
19 | new webpack.DefinePlugin({
20 | 'process.env': {
21 | NODE_ENV: JSON.stringify("development")
22 | }
23 | }),
24 | new webpack.NamedModulesPlugin(),
25 | // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息
26 | new webpack.optimize.UglifyJsPlugin({
27 | sourceMap: true,
28 | compress: {
29 | warnings: true,
30 | }
31 | }),
32 | new HtmlWebpackPlugin({
33 | title: 'Custom template',
34 | template: '../example/index.html', // Load a custom template (ejs by default see the FAQ for details)
35 | hash: true,
36 | filename:"./index.html"
37 | })
38 | ],
39 | resolve: {
40 | modules: [
41 | "node_modules",
42 | path.resolve(__dirname, "src")
43 | ],
44 | extensions: ['.js', '.jsx']
45 | },
46 | module: {
47 | rules: [
48 | {
49 | test: /\.(js|jsx)$/,
50 | loader: 'babel-loader' ,
51 | exclude: /node_modules/,
52 | include: __dirname,
53 | options: {
54 | presets: [["es2015", {"modules": false}], "stage-1", "react"]
55 | }
56 | },
57 | {
58 | test: /\.css$/,
59 | use: [
60 | 'style-loader',
61 | 'css-loader',
62 | { loader: 'postcss-loader', options: { config: { path: './postcss.config.js' } } }
63 | ]
64 | },
65 | {
66 | test: /\.less/,
67 | use: [
68 | 'style-loader',
69 | 'css-loader',
70 | { loader: 'postcss-loader', options: { config: { path: './postcss.config.js' } } },
71 | 'less-loader'
72 | ]
73 | },
74 | {
75 | test: /\.(gif|jpg|png|woff|svg|eot|ttf)$/,
76 | use: [
77 | { loader: 'file-loader'}
78 | ]
79 | }
80 | ]
81 | },
82 | }
83 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const webpack = require('webpack');
3 | const clean = require('gulp-clean');
4 | const gutil = require('gulp-util');
5 | const ftp = require('vinyl-ftp');
6 | const ghPages = require('gulp-gh-pages-will');
7 | const deploy = require('./deploy.config.json');
8 |
9 | const deploy_remote_path = '/public/17zt/viewer';
10 | const webpack_config_demo = require('./webpack.config.example.js');
11 | const babel = require('gulp-babel');
12 | const less = require('gulp-less');
13 | const path = require('path');
14 | // var webpack_config_dist = require('./webpack.config.dist.js');
15 |
16 | gulp.task('clean:demo', () => {
17 | return gulp.src('./demo', { read: false })
18 | .pipe(clean());
19 | });
20 |
21 | // 编译示例
22 | gulp.task('build:demo', ['clean:demo'], (callback) => {
23 | webpack(webpack_config_demo, () => {
24 | // gulp 异步任务必须明确执行 callback() 否则 gulp 将一直卡住
25 | callback();
26 | });
27 | });
28 |
29 | // 部署示例到自己的测试服务器
30 | gulp.task('deploy:demo', ['build:demo'], () => {
31 | deploy.log = gutil.log;
32 |
33 | const conn = ftp.create(deploy);
34 |
35 | return gulp.src('demo/**')
36 | .pipe(conn.dest(deploy_remote_path));
37 | });
38 |
39 | // 部署示例到 gh-pages
40 | gulp.task('deploy:gh-pages', ['build:demo'], () => {
41 | return gulp.src('./demo/**')
42 | .pipe(ghPages());
43 | });
44 |
45 | gulp.task('publish:clean', () => {
46 | return gulp.src('./dist', { read: false })
47 | .pipe(clean());
48 | });
49 |
50 | // 编译 js 文件
51 | gulp.task('publish:js', ['publish:clean'], () => {
52 | return gulp.src('src/**/*.{js,jsx}')
53 | .pipe(babel({
54 | presets: ['es2015', 'stage-1', 'react'],
55 | }))
56 | .pipe(gulp.dest('dist'));
57 | });
58 |
59 | // 编译 less 文件
60 | gulp.task('publish:less', ['publish:clean'], () => {
61 | return gulp.src('src/**/*.less')
62 | .pipe(less({
63 | paths: [path.join(__dirname, 'less', 'includes')],
64 | }))
65 | .pipe(gulp.dest('dist'));
66 | });
67 |
68 | // 发布 css 文件
69 | gulp.task('publish:css', ['publish:clean'], () => {
70 | return gulp.src('src/**/*.css')
71 | .pipe(gulp.dest('dist'));
72 | });
73 |
74 | // 打包发布 npm
75 | gulp.task('publish', ['publish:clean', 'publish:js', 'publish:css']);
76 |
77 | gulp.task('demo', ['deploy:demo']);
78 |
79 | gulp.task('gh-pages', ['deploy:gh-pages']);
80 |
81 | gulp.task('release', ['publish', 'gh-pages']);
82 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var path = require('path')
3 | var webpack = require('webpack')
4 | var HtmlWebpackPlugin = require('html-webpack-plugin');
5 | var port = 3010;
6 | var host = '0.0.0.0'
7 |
8 | module.exports = {
9 | context: path.resolve(__dirname, 'example'), // string(绝对路径!)
10 | devtool: 'eval',
11 | cache: true,
12 | entry: [
13 | 'react-hot-loader/patch',
14 | // 开启 React 代码的模块热替换(HMR)
15 | 'webpack-dev-server/client?http://' + host + ':' + port,
16 | 'webpack/hot/only-dev-server',
17 | './main.js'
18 | ],
19 | plugins: [
20 | new webpack.HotModuleReplacementPlugin(),
21 | new webpack.NamedModulesPlugin(),
22 | // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息
23 | new webpack.optimize.UglifyJsPlugin({
24 | sourceMap: true,
25 | compress: {
26 | warnings: true,
27 | }
28 | }),
29 | new HtmlWebpackPlugin({
30 | title: 'Custom template',
31 | template: './index.html', // Load a custom template (ejs by default see the FAQ for details)
32 | hash: true,
33 | filename:"./index.html"
34 | })
35 | ],
36 | resolve: {
37 | modules: [
38 | "node_modules",
39 | path.resolve(__dirname, "src")
40 | ],
41 | extensions: ['.js', '.jsx']
42 | },
43 | devServer: {
44 | hot: true,
45 | // 开启服务器的模块热替换(HMR)
46 | // host: 'localhost',
47 | host: host,
48 | port: port
49 | },
50 | module: {
51 | rules: [
52 | {
53 | test: /\.(js|jsx)$/,
54 | loader: 'babel-loader' ,
55 | exclude: /node_modules/,
56 | include: __dirname,
57 | options: {
58 | presets: [["es2015", {"modules": false}], "stage-1", "react"],
59 | plugins: [
60 | "react-hot-loader/babel"
61 | // 开启 React 代码的模块热替换(HMR)
62 | ]
63 | }
64 | },
65 | {
66 | test: /\.css$/,
67 | use: [
68 | 'style-loader',
69 | 'css-loader',
70 | { loader: 'postcss-loader', options: { config: { path: './postcss.config.js' } } }
71 | ]
72 | },
73 | {
74 | test: /\.less/,
75 | use: [
76 | 'style-loader',
77 | 'css-loader',
78 | { loader: 'postcss-loader', options: { config: { path: './postcss.config.js' } } },
79 | 'less-loader'
80 | ]
81 | },
82 | {
83 | test: /\.(gif|jpg|png|woff|svg|eot|ttf)$/,
84 | use: [
85 | { loader: 'file-loader'}
86 | ]
87 | }
88 | ]
89 | },
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/WxImageViewer.css:
--------------------------------------------------------------------------------
1 | @charset 'utf-8';
2 | .wx-image-viewer {
3 | position: fixed;
4 | left: 0;
5 | top: 0;
6 | width: 100%;
7 | height: 100%;
8 | }
9 | .wx-image-viewer .viewer-cover {
10 | position: absolute;
11 | left: 0;
12 | top: 0;
13 | width: 100%;
14 | height: 100%;
15 | background-color: #000000;
16 | }
17 | .wx-image-viewer .viewer-list-container {
18 | position: absolute;
19 | left: 0;
20 | top: 0;
21 | width: 100%;
22 | height: 100%;
23 | -webkit-transition-property: -webkit-transform;
24 | transition-property: -webkit-transform;
25 | -o-transition-property: transform;
26 | transition-property: transform;
27 | transition-property: transform,-webkit-transform;
28 | }
29 | .wx-image-viewer .viewer-image-container {
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | overflow: hidden;
34 | }
35 | .wx-image-viewer .viewer-image-container img {
36 | position: absolute;
37 | left: 0;
38 | top: 0;
39 | transform-origin: left top;
40 | -webkit-transform-origin: left top;
41 | -moz-transform-origin: left top;
42 | -o-transform-origin: left top;
43 | -webkit-transition-property: -webkit-transform;
44 | transition-property: -webkit-transform;
45 | -o-transition-property: transform;
46 | transition-property: transform;
47 | transition-property: transform,-webkit-transform;
48 | }
49 | .wx-image-viewer .viewer-image-pointer {
50 | position: fixed;
51 | bottom: 10px;
52 | left: 0;
53 | width: 100%;
54 | text-align: center;
55 | }
56 | .wx-image-viewer .viewer-image-pointer .pointer {
57 | display: inline-block;
58 | width: 8px;
59 | height: 8px;
60 | margin: 0 5px;
61 | border-radius: 100%;
62 | background-color: #333;
63 | }
64 | .wx-image-viewer .viewer-image-pointer .pointer.on {
65 | background-color: #fff;
66 | }
67 | .wx-image-viewer .viewer-image-loading {
68 | position: absolute;
69 | margin: auto;
70 | left: 0;
71 | right: 0;
72 | top: 0;
73 | bottom: 0;
74 | width: 32px;
75 | height: 32px;
76 | box-sizing: border-box;
77 | border-radius: 100%;
78 | border-width: 4px;
79 | border-style: solid;
80 | border-color: #333;
81 | border-bottom-color: #FFF;
82 | -webkit-animation: roll 1s linear infinite;
83 | animation: roll 1s linear infinite;
84 | }
85 | @-webkit-keyframes roll {
86 | from {
87 | -webkit-transform: rotate(0deg);
88 | }
89 | to {
90 | -webkit-transform: rotate(360deg);
91 | }
92 | }
93 | @keyframes roll {
94 | from {
95 | transform: rotate(0deg);
96 | }
97 | to {
98 | transform: rotate(360deg);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [中文文档](https://github.com/react-ld/react-wx-images-viewer/blob/master/README-cn.md)
2 |
3 | # Description
4 | [react-wx-images-viewer](https://github.com/react-ld/react-wx-images-viewer/tree/master) is a React
5 | common component use for images viewer *in mobile device*. It's function look like WeChat App previewImage.
6 | Finger drag left or right to preview each image. Two finger drag zoom in or zoom out the image.
7 |
8 | # Install
9 | ```shell
10 | npm install --save react-wx-images-viewer
11 | ```
12 |
13 | # dependence
14 | version 0.0.1 -> React ^15.5.4
15 |
16 | version ^1.0.0 -> React ^16.0.0
17 |
18 | # Example
19 | - [demo1](https://react-ld.github.io/react-wx-images-viewer/index.html)
20 |
21 | # How to use
22 | ```js
23 | import WxImageViewer from 'react-wx-images-viewer';
24 | class App extends Component {
25 |
26 | state = {
27 | imags: [
28 | require('./assets/2.jpg'),
29 | require('./assets/1.jpg'),
30 | require('./assets/0.jpg'),
31 | require('./assets/3.jpg'),
32 | require('./assets/4.jpg'),
33 | ],
34 | index: 0,
35 | isOpen: false
36 | };
37 |
38 | onClose = () =>{
39 | this.setState({
40 | isOpen: false
41 | })
42 | }
43 |
44 | openViewer (index){
45 | this.setState({
46 | index,
47 | isOpen: true
48 | })
49 | }
50 |
51 | render() {
52 | const {
53 | imags,
54 | index,
55 | isOpen
56 | } = this.state;
57 |
58 | return (
59 |
60 |
61 | {/*直接打开*/}
62 |
63 | {
64 | this.state.imags.map((item, index) => {
65 | return
66 |

67 |
68 | })
69 | }
70 |
71 | {
72 | isOpen ?
: ""
73 | }
74 |
75 | )
76 | }
77 | }
78 |
79 | export default App;
80 | ```
81 |
82 | # API:
83 | | Property | Description | Type | default | Remarks |
84 | | --- | --- | --- | --- | --- |
85 | | maxZoomNum | max zoom in times | Number | 4 | |
86 | | zIndex | the depth of the layer | Number | 100 | |
87 | | index | show which image in urls array when open | Number | 0 | |
88 | | gap | the gap between images | Number | 10 | unit is pixel |
89 | | urls | images url array | Array | | suggest the array length do not more than 10 |
90 | | onClose | handle close function | Function | | must remove WxViewer from current render and other sepcial logic |
91 | | loading | DIY loading style | component | WxImageViewer default [Loading](./src/components/Loading.jsx) | TODO |
92 | | pointer | DIY pointer | component | WxImageViewer default [Pointer](./src/components/Pointer.jsx) | TODO |
93 |
94 | # Reference
95 | - [react-modal](https://github.com/reactjs/react-modal)
96 | - [react-viewer-mobile](https://github.com/infeng/react-viewer-mobile/)
97 | - [react-image](https://github.com/mbrevda/react-image)
--------------------------------------------------------------------------------
/README-cn.md:
--------------------------------------------------------------------------------
1 | # [English](https://github.com/react-ld/react-wx-images-viewer/tree/master)
2 |
3 | # 描述
4 | [react-wx-images-viewer](https://github.com/react-ld/react-wx-images-viewer/tree/master)是一个通用型的移动端图片浏览 React 组件。主要功能仿照微信图片浏览功能开发。支持单指左右滑动切换图片,双指拖拽放大缩小图片。
5 |
6 | 通过 ReactDOM 在 body 根级创建独立的 div 进行渲染,参考 [react-modal](https://github.com/reactjs/react-modal) 使用 ReactDOM.unstable_renderSubtreeIntoContainer 进行渲染
7 |
8 | # 依赖关系
9 | version 0.0.1 -> React ^15.5.4
10 |
11 | version ^1.0.0 -> React ^16.0.0
12 |
13 | # 示例
14 | - [demo1](https://react-ld.github.io/react-wx-images-viewer/index.html)
15 |
16 | # 基础功能
17 | - 多图左右切换浏览,不支持循环
18 | - 图片默认样式:水平方向与屏幕等宽,垂直方向居中或者居顶
19 | - 支持图片缩放浏览
20 | - 单指左右滑动切换图片,双指拖拽放到或缩小图片
21 |
22 | # 扩展
23 | - 有默认加载图片动效
24 | - 可配置图层深度即 zIndex
25 | - 可配置初始显示图片序号
26 | - TODO:指示器可通过 React 组件方式自定义
27 | - TODO:加载动效可通过 React 组件方式自定义
28 |
29 | # 安装
30 | ```shell
31 | npm install --save react-wx-images-viewer
32 | ```
33 |
34 | # 使用
35 | ```js
36 | import WxImageViewer from 'react-wx-images-viewer';
37 | class App extends Component {
38 |
39 | state = {
40 | imags: [
41 | require('./assets/2.jpg'),
42 | require('./assets/1.jpg'),
43 | require('./assets/0.jpg'),
44 | require('./assets/3.jpg'),
45 | require('./assets/4.jpg'),
46 | ],
47 | index: 0,
48 | isOpen: false
49 | };
50 |
51 | onClose = () =>{
52 | this.setState({
53 | isOpen: false
54 | })
55 | }
56 |
57 | openViewer (index){
58 | this.setState({
59 | index,
60 | isOpen: true
61 | })
62 | }
63 |
64 | render() {
65 | const {
66 | imags,
67 | index,
68 | isOpen
69 | } = this.state;
70 |
71 | return (
72 |
73 |
74 | {/*直接打开*/}
75 |
76 | {
77 | this.state.imags.map((item, index) => {
78 | return
79 |

80 |
81 | })
82 | }
83 |
84 | {
85 | isOpen ?
: ""
86 | }
87 |
88 | )
89 | }
90 | }
91 |
92 | export default App;
93 | ```
94 |
95 | # 接口
96 | | Property | Description | Type | default | Remarks |
97 | | --- | --- | --- | --- | --- |
98 | | maxZoomNum | 图片放大最大倍数 | Number | 4 | |
99 | | zIndex | 组件图层深度 | Number | 100 | |
100 | | index | 初始显示图片序号 | Number | 0 | |
101 | | gap | 图片之间的间隙 | Number | 10 | unit is pixel |
102 | | urls | 图片 URL 数组 | Array | | suggest the array length do not more than 10 |
103 | | onClose | 关闭的回调处理函数 | Function | | 需要通过该函数将组件从渲染中移除 |
104 | | loading | 自定义图片加载组件 | component | WxImageViewer default [Loading](./src/components/Loading.jsx) | TODO |
105 | | pointer | 自定义指示器组件 | component | WxImageViewer default [Pointer](./src/components/Pointer.jsx) | TODO |
106 |
107 | # 参考
108 | - [react-modal](https://github.com/reactjs/react-modal)
109 | - [react-viewer-mobile](https://github.com/infeng/react-viewer-mobile/)
110 | - [react-image](https://github.com/mbrevda/react-image)
--------------------------------------------------------------------------------
/src/components/ListContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import ImageContainer from './ImageContainer';
5 |
6 | // 快速拖动时间限制
7 | const DEFAULT_TIME_DIFF = 200;
8 |
9 | class ListContainer extends PureComponent {
10 | static propTypes = {
11 | maxZoomNum: PropTypes.number.isRequired,
12 | changeIndex: PropTypes.func.isRequired,
13 | gap: PropTypes.number.isRequired,
14 | speed: PropTypes.number.isRequired, // Duration of transition between slides (in ms)
15 | }
16 |
17 | constructor() {
18 | super();
19 | this.isNeedSpring = false;
20 | }
21 |
22 | state = {
23 | left: 0,
24 | }
25 |
26 | componentWillMount() {
27 | const {
28 | screenWidth,
29 | urls,
30 | index,
31 | gap,
32 | } = this.props;
33 |
34 | this.length = urls.length;
35 | this.perDistance = screenWidth + gap;
36 | this.maxLeft = this.perDistance * (this.length - 1);
37 | this.isNeedSpring = false;
38 |
39 | this.setState({
40 | left: -this.perDistance * index,
41 | });
42 | }
43 |
44 | componentWillReceiveProps(nextProps) {
45 | if (this.props.index !== nextProps.index) {
46 | this.isNeedSpring = true;
47 | this.setState({
48 | left: -this.perDistance * nextProps.index,
49 | });
50 | }
51 | }
52 |
53 | /**
54 | * 拖拽的缓动公式 - easeOutSine
55 | * Link http://easings.net/zh-cn#
56 | * t: current time(当前时间);
57 | * b: beginning value(初始值);
58 | * c: change in value(变化量);
59 | * d: duration(持续时间)。
60 | */
61 | easing = (distance) => {
62 | const t = distance;
63 | const b = 0;
64 | const d = this.props.screenWidth; // 允许拖拽的最大距离
65 | const c = d / 2.5; // 提示标签最大有效拖拽距离
66 |
67 | return (c * Math.sin((t / d) * (Math.PI / 2))) + b;
68 | }
69 |
70 | handleStart = () => {
71 | // console.info("ListContainer handleStart")
72 | this.startLeft = this.state.left;
73 | this.startTime = (new Date()).getTime();
74 | this.isNeedSpring = false;
75 | }
76 |
77 | handleMove = (diffX) => {
78 | // console.info("ListContainer handleStart diffX = %s",diffX);
79 | let nDiffx = diffX;
80 | // 限制最大 diffx 值
81 | if (Math.abs(nDiffx) > this.props.screenWidth) {
82 | if (nDiffx < 0) { nDiffx = -this.props.screenWidth; }
83 | if (nDiffx > 0) { nDiffx = this.props.screenWidth; }
84 | }
85 |
86 | if (this.state.left >= 0 && nDiffx > 0) {
87 | nDiffx = this.easing(nDiffx);
88 | } else if (this.state.left <= -this.maxLeft && nDiffx < 0) {
89 | nDiffx = -this.easing(-nDiffx);
90 | }
91 |
92 | this.setState({
93 | left: this.startLeft + nDiffx,
94 | });
95 | }
96 |
97 | handleEnd = (isAllowChange) => {
98 | let index;
99 | const diffTime = (new Date()).getTime() - this.startTime;
100 | console.info('handleEnd %s', isAllowChange, diffTime, this.state.left, this.startLeft, this.props.index);
101 | // 快速拖动情况下切换图片
102 | if (isAllowChange && diffTime < DEFAULT_TIME_DIFF) {
103 | if (this.state.left < this.startLeft) {
104 | index = this.props.index + 1;
105 | } else {
106 | index = this.props.index - 1;
107 | }
108 | } else {
109 | index = Math.abs(Math.round(this.state.left / this.perDistance));
110 | }
111 |
112 | // 处理边界情况
113 | if (index < 0) { index = 0; } else if (index > this.length - 1) { index = this.length - 1; }
114 |
115 | this.setState({
116 | left: -this.perDistance * index,
117 | });
118 | this.isNeedSpring = true;
119 | if (index !== this.props.index) {
120 | this.props.changeIndex(index);
121 | return true;
122 | }
123 | return false;
124 | }
125 |
126 | render() {
127 | const {
128 | maxZoomNum,
129 | screenWidth,
130 | screenHeight,
131 | urls,
132 | speed,
133 | } = this.props;
134 |
135 | const {
136 | left,
137 | } = this.state;
138 |
139 | const defaultStyle = {};
140 |
141 | if (this.isNeedSpring) {
142 | const duration = `${speed}ms`;
143 | defaultStyle.WebkitTransitionDuration = duration;
144 | defaultStyle.transitionDuration = duration;
145 | }
146 | const translate = `translate3d(${left}px, 0, 0)`;
147 | defaultStyle.WebkitTransform = translate;
148 | defaultStyle.transform = translate;
149 |
150 | return (
151 |
155 | {
156 | urls.map((item, i) => (
157 |
168 | ))
169 | }
170 |
171 | );
172 | }
173 | }
174 |
175 | export default ListContainer;
176 |
--------------------------------------------------------------------------------
/src/components/tween.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | 'use strict';
3 | /**
4 | * t: current time(当前时间);
5 | * b: beginning value(初始值);
6 | * c: change in value(变化量);
7 | * _c: final value (最后值)
8 | * d: duration(持续时间)。
9 | */
10 | var tweenFunctions = {
11 | linear: function (t, b, _c, d) {
12 | var c = _c - b;
13 | return c * t / d + b;
14 | },
15 | easeInQuad: function (t, b, _c, d) {
16 | var c = _c - b;
17 | return c * (t /= d) * t + b;
18 | },
19 | easeOutQuad: function (t, b, _c, d) {
20 | var c = _c - b;
21 | return -c * (t /= d) * (t - 2) + b;
22 | },
23 | easeInOutQuad: function (t, b, _c, d) {
24 | var c = _c - b;
25 | if ((t /= d / 2) < 1) {
26 | return c / 2 * t * t + b;
27 | } else {
28 | return -c / 2 * ((--t) * (t - 2) - 1) + b;
29 | }
30 | },
31 | easeInCubic: function (t, b, _c, d) {
32 | var c = _c - b;
33 | return c * (t /= d) * t * t + b;
34 | },
35 | easeOutCubic: function (t, b, _c, d) {
36 | var c = _c - b;
37 | return c * ((t = t / d - 1) * t * t + 1) + b;
38 | },
39 | easeInOutCubic: function (t, b, _c, d) {
40 | var c = _c - b;
41 | if ((t /= d / 2) < 1) {
42 | return c / 2 * t * t * t + b;
43 | } else {
44 | return c / 2 * ((t -= 2) * t * t + 2) + b;
45 | }
46 | },
47 | easeInQuart: function (t, b, _c, d) {
48 | var c = _c - b;
49 | return c * (t /= d) * t * t * t + b;
50 | },
51 | easeOutQuart: function (t, b, _c, d) {
52 | var c = _c - b;
53 | return -c * ((t = t / d - 1) * t * t * t - 1) + b;
54 | },
55 | easeInOutQuart: function (t, b, _c, d) {
56 | var c = _c - b;
57 | if ((t /= d / 2) < 1) {
58 | return c / 2 * t * t * t * t + b;
59 | } else {
60 | return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
61 | }
62 | },
63 | easeInQuint: function (t, b, _c, d) {
64 | var c = _c - b;
65 | return c * (t /= d) * t * t * t * t + b;
66 | },
67 | easeOutQuint: function (t, b, _c, d) {
68 | var c = _c - b;
69 | return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
70 | },
71 | easeInOutQuint: function (t, b, _c, d) {
72 | var c = _c - b;
73 | if ((t /= d / 2) < 1) {
74 | return c / 2 * t * t * t * t * t + b;
75 | } else {
76 | return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
77 | }
78 | },
79 | easeInSine: function (t, b, _c, d) {
80 | var c = _c - b;
81 | return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
82 | },
83 | easeOutSine: function (t, b, _c, d) {
84 | var c = _c - b;
85 | return c * Math.sin(t / d * (Math.PI / 2)) + b;
86 | },
87 | easeInOutSine: function (t, b, _c, d) {
88 | var c = _c - b;
89 | return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
90 | },
91 | easeInExpo: function (t, b, _c, d) {
92 | var c = _c - b;
93 | return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
94 | },
95 | easeOutExpo: function (t, b, _c, d) {
96 | var c = _c - b;
97 | return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
98 | },
99 | easeInOutExpo: function (t, b, _c, d) {
100 | var c = _c - b;
101 | if (t === 0) {
102 | return b;
103 | }
104 | if (t === d) {
105 | return b + c;
106 | }
107 | if ((t /= d / 2) < 1) {
108 | return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
109 | } else {
110 | return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
111 | }
112 | },
113 | easeInCirc: function (t, b, _c, d) {
114 | var c = _c - b;
115 | return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
116 | },
117 | easeOutCirc: function (t, b, _c, d) {
118 | var c = _c - b;
119 | return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
120 | },
121 | easeInOutCirc: function (t, b, _c, d) {
122 | var c = _c - b;
123 | if ((t /= d / 2) < 1) {
124 | return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
125 | } else {
126 | return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
127 | }
128 | },
129 | easeInElastic: function (t, b, _c, d) {
130 | var c = _c - b;
131 | var a, p, s;
132 | s = 1.70158;
133 | p = 0;
134 | a = c;
135 | if (t === 0) {
136 | return b;
137 | } else if ((t /= d) === 1) {
138 | return b + c;
139 | }
140 | if (!p) {
141 | p = d * 0.3;
142 | }
143 | if (a < Math.abs(c)) {
144 | a = c;
145 | s = p / 4;
146 | } else {
147 | s = p / (2 * Math.PI) * Math.asin(c / a);
148 | }
149 | return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
150 | },
151 | easeOutElastic: function (t, b, _c, d) {
152 | var c = _c - b;
153 | var a, p, s;
154 | s = 1.70158;
155 | p = 0;
156 | a = c;
157 | if (t === 0) {
158 | return b;
159 | } else if ((t /= d) === 1) {
160 | return b + c;
161 | }
162 | if (!p) {
163 | p = d * 0.3;
164 | }
165 | if (a < Math.abs(c)) {
166 | a = c;
167 | s = p / 4;
168 | } else {
169 | s = p / (2 * Math.PI) * Math.asin(c / a);
170 | }
171 | return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
172 | },
173 | easeInOutElastic: function (t, b, _c, d) {
174 | var c = _c - b;
175 | var a, p, s;
176 | s = 1.70158;
177 | p = 0;
178 | a = c;
179 | if (t === 0) {
180 | return b;
181 | } else if ((t /= d / 2) === 2) {
182 | return b + c;
183 | }
184 | if (!p) {
185 | p = d * (0.3 * 1.5);
186 | }
187 | if (a < Math.abs(c)) {
188 | a = c;
189 | s = p / 4;
190 | } else {
191 | s = p / (2 * Math.PI) * Math.asin(c / a);
192 | }
193 | if (t < 1) {
194 | return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
195 | } else {
196 | return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
197 | }
198 | },
199 | easeInBack: function (t, b, _c, d, s) {
200 | var c = _c - b;
201 | if (s === void 0) {
202 | s = 1.70158;
203 | }
204 | return c * (t /= d) * t * ((s + 1) * t - s) + b;
205 | },
206 | easeOutBack: function (t, b, _c, d, s) {
207 | var c = _c - b;
208 | if (s === void 0) {
209 | s = 1.70158;
210 | }
211 | return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
212 | },
213 | easeInOutBack: function (t, b, _c, d, s) {
214 | var c = _c - b;
215 | if (s === void 0) {
216 | s = 1.70158;
217 | }
218 | if ((t /= d / 2) < 1) {
219 | return c / 2 * (t * t * (((s *= 1.525) + 1) * t - s)) + b;
220 | } else {
221 | return c / 2 * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b;
222 | }
223 | },
224 | easeInBounce: function (t, b, _c, d) {
225 | var c = _c - b;
226 | var v;
227 | v = tweenFunctions.easeOutBounce(d - t, 0, c, d);
228 | return c - v + b;
229 | },
230 | easeOutBounce: function (t, b, _c, d) {
231 | var c = _c - b;
232 | if ((t /= d) < 1 / 2.75) {
233 | return c * (7.5625 * t * t) + b;
234 | } else if (t < 2 / 2.75) {
235 | return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b;
236 | } else if (t < 2.5 / 2.75) {
237 | return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b;
238 | } else {
239 | return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b;
240 | }
241 | },
242 | easeInOutBounce: function (t, b, _c, d) {
243 | var c = _c - b;
244 | var v;
245 | if (t < d / 2) {
246 | v = tweenFunctions.easeInBounce(t * 2, 0, c, d);
247 | return v * 0.5 + b;
248 | } else {
249 | v = tweenFunctions.easeOutBounce(t * 2 - d, 0, c, d);
250 | return v * 0.5 + c * 0.5 + b;
251 | }
252 | }
253 | };
254 |
255 | module.exports = tweenFunctions;
--------------------------------------------------------------------------------
/src/components/ImageContainer.jsx:
--------------------------------------------------------------------------------
1 | /* eslint max-len: [0] */
2 |
3 | import React, { PureComponent } from 'react';
4 | import PropTypes from 'prop-types';
5 |
6 | import raf from 'raf';
7 | import tween from './tween.js';
8 | import Loading from './Loading';
9 |
10 | /**
11 | *
12 | * @param {number} value
13 | * @param {number} min
14 | * @param {number} max
15 | */
16 | function setScope(value, min, max) {
17 | if (value < min) { return min; }
18 | if (value > max) { return max; }
19 | return value;
20 | }
21 |
22 |
23 | function getDistanceBetweenTouches(e) {
24 | if (e.touches.length < 2) return 1;
25 | const x1 = e.touches[0].clientX;
26 | const y1 = e.touches[0].clientY;
27 | const x2 = e.touches[1].clientX;
28 | const y2 = e.touches[1].clientY;
29 | const distance = Math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2));
30 | return distance;
31 | }
32 |
33 | // const msPerFrame = 1000 / 60;
34 | const maxAnimateTime = 1000;
35 | const minTapMoveValue = 5;
36 | const maxTapTimeValue = 300;
37 |
38 | /**
39 | * 图片默认展示模式:宽度等于屏幕宽度,高度等比缩放;水平居中,垂直居中或者居顶(当高度大于屏幕高度时)
40 | * 图片实际尺寸: actualWith, actualHeight
41 | * 图片初始尺寸: originWidth, originHeight
42 | * 坐标位置:left, top
43 | * 放大倍数:zoom
44 | * 最大放大倍数:maxZoomNum
45 | * 坐标关系:-(maxZoomNum - 1) * originWidth / 2 < left < 0
46 | * -(maxZoomNum - 1) * originHeight / 2 < top < 0
47 | * 尺寸关系:width = zoom * originWidth
48 | * heigth = zoom * originHeight
49 | *
50 | * 放大点位置关系:
51 | * 初始点位置:oldPointLeft, oldPointTop
52 | * 放大后位置:newPointLeft, newPointTop
53 | * 对应关系: newPointLeft = zoom * oldPointLeft
54 | * newPointTop = zoom * oldPointTop
55 | *
56 | * 坐标位置:-1*left = -1*startLeft + (newPointLeft - oldPointLeft) =-1*startLeft (zoom - 1) * oldPointLeft
57 | * -1*top = -1*startTop + (newPointTop - oldPointTop) =-1*startLeft (zoom - 1) * oldPointTop
58 | * =>
59 | * left = startLeft + (1 - zoom) * oldPointLeft
60 | * top = startTop + (1 - zoom) * oldPointTop
61 | */
62 |
63 | class ImageContainer extends PureComponent {
64 | static propTypes = {
65 | maxZoomNum: PropTypes.number.isRequired,
66 | handleStart: PropTypes.func.isRequired,
67 | handleMove: PropTypes.func.isRequired,
68 | handleEnd: PropTypes.func.isRequired,
69 | }
70 |
71 | static contextTypes = {
72 | onClose: PropTypes.func,
73 | };
74 |
75 | constructor() {
76 | super();
77 | this.actualHeight = 0; // 图片实际高度
78 | this.actualWith = 0; // 图片实际宽度
79 |
80 | this.originHeight = 0; // 图片默认展示模式下高度
81 | this.originWidth = 0; // 图片默认展示模式下宽度
82 | this.originScale = 1; // 图片初始缩放比例
83 |
84 | this.startLeft = 0; // 开始触摸操作时的 left 值
85 | this.startTop = 0; // 开始触摸操作时的 top 值
86 | this.startScale = 1; // 开始缩放操作时的 scale 值
87 |
88 | this.onTouchStartTime = 0; // 单指触摸开始时间
89 |
90 | this.isTwoFingerMode = false; // 是否为双指模式
91 | this.oldPointLeft = 0;// 计算手指中间点在图片上的位置(坐标值)
92 | this.oldPointTop = 0;// 计算手指中间点在图片上的位置(坐标值)
93 | this._touchZoomDistanceStart = 0; // 用于记录双指距离
94 | this.haveCallMoveFn = false;
95 |
96 |
97 | this.diffX = 0;// 记录最后 move 事件 移动距离
98 | this.diffY = 0;// 记录最后 move 事件 移动距离
99 |
100 | this.animationID = 0;
101 | this.animateStartTime = 0;
102 | this.animateStartValue = {
103 | x: 0,
104 | y: 0,
105 | };
106 | this.animateFinalValue = {
107 | x: 0,
108 | y: 0,
109 | };
110 | }
111 |
112 | state = {
113 | width: 0,
114 | height: 0,
115 | scale: 1,
116 | left: 0,
117 | top: 0,
118 | isLoaded: false,
119 | }
120 |
121 | componentWillMount() {
122 | this.loadImg(this.props.src);
123 | }
124 |
125 | componentWillUnmount() {
126 | this.unloadImg();
127 | if (this.animationID) {
128 | raf.cancel(this.animationID);
129 | }
130 | }
131 |
132 | onLoad = () => {
133 | this.actualWith = this.img.width;
134 | this.actualHeight = this.img.height;
135 |
136 | const {
137 | screenHeight,
138 | screenWidth,
139 | } = this.props;
140 |
141 | const left = 0;
142 | let top = 0;
143 |
144 | this.originWidth = screenWidth;
145 | this.originHeight = (this.actualHeight / this.actualWith) * screenWidth;
146 | this.originScale = 1;
147 |
148 | if (this.actualHeight / this.actualWith < screenHeight / screenWidth) {
149 | top = parseInt((screenHeight - this.originHeight) / 2, 10);
150 | }
151 | this.originTop = top;
152 |
153 | this.setState({
154 | width: this.originWidth,
155 | height: this.originHeight,
156 | scale: 1,
157 | left,
158 | top,
159 | isLoaded: true,
160 | });
161 | }
162 |
163 | onError = () => {
164 | this.setState({
165 | isLoaded: true,
166 | });
167 | }
168 |
169 | loadImg = (url) => {
170 | this.img = new Image();
171 | this.img.src = url;
172 | this.img.onload = this.onLoad;
173 | this.img.onerror = this.onError;
174 |
175 | this.setState({
176 | isLoaded: false,
177 | });
178 | }
179 |
180 | unloadImg = () => {
181 | delete this.img.onerror;
182 | delete this.img.onload;
183 | delete this.img.src;
184 | delete this.img;
185 | }
186 |
187 | handleTouchStart = (event) => {
188 | console.info('handleTouchStart');
189 | event.preventDefault();
190 | if (this.animationID) {
191 | raf.cancel(this.animationID);
192 | }
193 | switch (event.touches.length) {
194 | case 1: {
195 | const targetEvent = event.touches[0];
196 | this.startX = targetEvent.clientX;
197 | this.startY = targetEvent.clientY;
198 | this.diffX = 0;
199 | this.diffY = 0;
200 |
201 | this.startLeft = this.state.left;
202 | this.startTop = this.state.top;
203 |
204 | console.info('handleTouchStart this.startX = %s, this.startY = %s, this.startLeft = %s, this.startTop = %s', this.startX, this.startY, this.startLeft, this.startTop);
205 |
206 | this.onTouchStartTime = (new Date()).getTime();
207 | this.haveCallMoveFn = false;
208 | break;
209 | }
210 | case 2: { // 两个手指
211 | // 设置手双指模式
212 | this.isTwoFingerMode = true;
213 |
214 | // 计算两个手指中间点屏幕上的坐标
215 | const middlePointClientLeft = Math.abs(Math.round((event.touches[0].clientX + event.touches[1].clientX) / 2));
216 | const middlePointClientTop = Math.abs(Math.round((event.touches[0].clientY + event.touches[1].clientY) / 2));
217 |
218 | // 保存图片初始位置和尺寸
219 | this.startLeft = this.state.left;
220 | this.startTop = this.state.top;
221 | this.startScale = this.state.scale;
222 |
223 | // 计算手指中间点在图片上的位置(坐标值)
224 | this.oldPointLeft = middlePointClientLeft - this.startLeft;
225 | this.oldPointTop = middlePointClientTop - this.startTop;
226 |
227 | this._touchZoomDistanceStart = getDistanceBetweenTouches(event);
228 | break;
229 | }
230 | default:
231 | break;
232 | }
233 | }
234 |
235 | handleTouchMove = (event) => {
236 | event.preventDefault();
237 |
238 | switch (event.touches.length) {
239 | case 1: {
240 | const targetEvent = event.touches[0];
241 | const diffX = targetEvent.clientX - this.startX;
242 | const diffY = targetEvent.clientY - this.startY;
243 |
244 | this.diffX = diffX;
245 | this.diffY = diffY;
246 | console.info('handleTouchMove one diffX=%s, diffY=%s', diffX, diffY);
247 | // 判断是否为点击
248 | if (Math.abs(diffX) < minTapMoveValue && Math.abs(diffY) < minTapMoveValue) {
249 | return;
250 | }
251 |
252 | const { scale, left } = this.state;
253 | const width = scale * this.originWidth;
254 | if (Math.abs(diffX) > Math.abs(diffY)) {
255 | // 水平移动
256 | if (this.state.scale === this.originScale && Math.abs(diffX) > minTapMoveValue) {
257 | this.haveCallMoveFn = true;
258 | this.callHandleMove(diffX);
259 | return;
260 | }
261 |
262 | console.info('handleMove one left=%s, this.startLeft=%s,this.originWidth=%s, width=%s', left, this.startLeft, this.originWidth, width);
263 | if (diffX < 0 && this.startLeft <= this.originWidth - width) {
264 | this.haveCallMoveFn = true;
265 | this.callHandleMove(diffX);
266 | return;
267 | }
268 |
269 | if (diffX > 0 && this.startLeft >= 0) {
270 | this.haveCallMoveFn = true;
271 | this.callHandleMove(diffX);
272 | return;
273 | }
274 | }
275 |
276 | const { screenHeight } = this.props;
277 | const height = scale * this.originHeight;
278 | let newTop = (screenHeight - height) / 2;
279 | const newLeft = this.startLeft + diffX;
280 |
281 | if (height > screenHeight || this.state.scale === this.originScale) {
282 | newTop = this.startTop + diffY;
283 | }
284 | console.info('handleTouchMove one newLeft=%s, newTop=%s', newLeft, newTop);
285 | this.setState({
286 | left: newLeft,
287 | top: newTop,
288 | });
289 |
290 | break;
291 | }
292 | case 2: { // 两个手指
293 | this._touchZoomDistanceEnd = getDistanceBetweenTouches(event);
294 |
295 | const zoom = Math.sqrt(this._touchZoomDistanceEnd / this._touchZoomDistanceStart);
296 | const scale = zoom * this.startScale;
297 |
298 | this.setState(() => {
299 | const left = this.startLeft + ((1 - zoom) * this.oldPointLeft);
300 | const top = this.startTop + ((1 - zoom) * this.oldPointTop);
301 |
302 | console.info('zoom = %s, left = %s, top = %s, scale', zoom, left, top, scale);
303 | return {
304 | left,
305 | top,
306 | scale,
307 | };
308 | });
309 | break;
310 | }
311 | default:
312 | break;
313 | }
314 | }
315 |
316 | handleTouchEnd = (event) => {
317 | console.info('handleTouchEnd', event.touches.length);
318 | event.preventDefault();
319 |
320 | if (this.isTwoFingerMode) { // 双指操作结束
321 | const touchLen = event.touches.length;
322 | this.isTwoFingerMode = false;
323 |
324 | if (touchLen === 1) {
325 | const targetEvent = event.touches[0];
326 | this.startX = targetEvent.clientX;
327 | this.startY = targetEvent.clientY;
328 | this.diffX = 0;
329 | this.diffY = 0;
330 | }
331 |
332 | this.setState((prevState, props) => {
333 | const scale = setScope(prevState.scale, 1, props.maxZoomNum);
334 | const width = scale * this.originWidth;
335 | const height = scale * this.originHeight;
336 | const zoom = scale / this.startScale;
337 | const left = setScope(this.startLeft + ((1 - zoom) * this.oldPointLeft), this.originWidth - width, 0);
338 |
339 | let top;
340 | if (height > props.screenHeight) {
341 | top = setScope(this.startTop + ((1 - zoom) * this.oldPointTop), props.screenHeight - height, 0);
342 | } else {
343 | top = (props.screenHeight - height) / 2;
344 | }
345 |
346 | if (touchLen === 1) {
347 | this.startLeft = left;
348 | this.startTop = top;
349 | this.startScale = scale;
350 | console.info('this.startX = %s, this.startY = %s, this.startLeft = %s, this.startTop = %s', this.startX, this.startY, this.startLeft, this.startTop);
351 | }
352 |
353 | console.info('zoom = %s, left = %s, top = %s, width=%s, height= %s', zoom, left, top, width, height);
354 | return {
355 | left,
356 | top,
357 | scale,
358 | };
359 | });
360 | } else { // 单指结束(ontouchend)
361 | const diffTime = (new Date()).getTime() - this.onTouchStartTime;
362 | const { diffX, diffY } = this;
363 |
364 | console.info('handleTouchEnd one diffTime = %s, diffX = %s, diffy = %s', diffTime, diffX, diffY);
365 | // 判断为点击则关闭图片浏览组件
366 | if (diffTime < maxTapTimeValue && Math.abs(diffX) < minTapMoveValue && Math.abs(diffY) < minTapMoveValue) {
367 | this.context.onClose();
368 | return;
369 | }
370 |
371 | // 水平移动
372 | if (this.haveCallMoveFn) {
373 | const isChangeImage = this.callHandleEnd(diffY < 30);
374 | if (isChangeImage) { // 如果切换图片则重置当前图片状态
375 | setTimeout(() => {
376 | this.setState({
377 | scale: this.originScale,
378 | left: 0,
379 | top: this.originTop,
380 | });
381 | }, maxAnimateTime / 3);
382 | return;
383 | }
384 | }
385 | // TODO 下拉移动距离超过屏幕高度的 1/3 则关闭
386 | // console.info(Math.abs(diffY) > (this.props.screenHeight / 2), this.startTop, this.originTop);
387 | // if (Math.abs(diffX) < Math.abs(diffY) && Math.abs(diffY) > (this.props.screenHeight / 3) && this.startTop === this.originTop) {
388 | // this.context.onClose();
389 | // return;
390 | // }
391 |
392 | let x;
393 | let y;
394 | const { scale } = this.state;
395 | // const width = scale * this.originWidth;
396 | const height = scale * this.originHeight;
397 |
398 | // 使用相同速度算法
399 | x = ((diffX * maxAnimateTime) / diffTime) + this.startLeft;
400 | y = ((diffY * maxAnimateTime) / diffTime) + this.startTop;
401 |
402 | if (this.state.scale === this.originScale) {
403 | x = 0;
404 | if (height > this.props.screenHeight) {
405 | y = setScope(y, this.props.screenHeight - height, 0);
406 | } else {
407 | y = this.originTop;
408 | }
409 | }
410 |
411 | // x = setScope(x, this.originWidth - width, 0);
412 |
413 | // if (height > this.props.screenHeight) {
414 | // y = setScope(y, this.props.screenHeight - height, 0);
415 | // } else {
416 | // y = this.state.top;
417 | // }
418 |
419 | this.animateStartValue = {
420 | x: this.state.left,
421 | y: this.state.top,
422 | };
423 | this.animateFinalValue = {
424 | x,
425 | y,
426 | };
427 | this.animateStartTime = Date.now();
428 | this.startAnimate();
429 | }
430 | }
431 |
432 | startAnimate = () => {
433 | this.animationID = raf(() => {
434 | // calculate current time
435 | const curTime = Date.now() - this.animateStartTime;
436 | let left;
437 | let top;
438 |
439 | // animate complete
440 | if (curTime > maxAnimateTime) {
441 | this.setState((prevState, props) => {
442 | const width = prevState.scale * this.originWidth;
443 | const height = prevState.scale * this.originHeight;
444 | left = setScope(prevState.left, this.originWidth - width, 0);
445 |
446 | if (height > props.screenHeight) {
447 | top = setScope(prevState.top, props.screenHeight - height, 0);
448 | } else {
449 | top = (props.screenHeight - height) / 2;
450 | }
451 | console.info('end animate left= %s, top = %s', left, top);
452 | return {
453 | left,
454 | top,
455 | };
456 | });
457 | } else {
458 | left = tween.easeOutQuart(curTime, this.animateStartValue.x, this.animateFinalValue.x, maxAnimateTime);
459 | top = tween.easeOutQuart(curTime, this.animateStartValue.y, this.animateFinalValue.y, maxAnimateTime);
460 |
461 | console.info('startAnimate left= %s, top = %s, curTime = %s', left, top, curTime);
462 | this.setState({
463 | left,
464 | top,
465 | });
466 | this.startAnimate();
467 | }
468 | });
469 | }
470 |
471 | callHandleMove = (diffX) => {
472 | if (!this.isCalledHandleStart) {
473 | this.isCalledHandleStart = true;
474 | if (this.props.handleStart) {
475 | this.props.handleStart();
476 | }
477 | }
478 | this.props.handleMove(diffX);
479 | }
480 |
481 | callHandleEnd = (isAllowChange) => {
482 | if (this.isCalledHandleStart) {
483 | this.isCalledHandleStart = false;
484 | if (this.props.handleEnd) {
485 | return this.props.handleEnd(isAllowChange);
486 | }
487 | }
488 | }
489 |
490 | render() {
491 | const {
492 | screenWidth,
493 | screenHeight,
494 | src,
495 | left: divLeft,
496 | } = this.props;
497 |
498 | const {
499 | isLoaded,
500 | left,
501 | top,
502 | scale,
503 | width,
504 | height,
505 | } = this.state;
506 |
507 | const ImageStyle = {
508 | width,
509 | height,
510 | };
511 |
512 | const translate = `translate3d(${left}px, ${top}px, 0) scale(${scale})`;
513 | ImageStyle.WebkitTransform = translate;
514 | ImageStyle.transform = translate;
515 |
516 | const defaultStyle = {
517 | left: divLeft,
518 | width: screenWidth,
519 | height: screenHeight,
520 | };
521 | // console.info('ImageContainer render');
522 | return (
523 |
530 | {
531 | isLoaded ?

:
532 | }
533 |
534 | );
535 | }
536 | }
537 |
538 | export default ImageContainer;
539 |
--------------------------------------------------------------------------------