├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── demo
├── .babelrc
├── .flowconfig
├── .gitignore
├── .watchmanconfig
├── App.js
├── App.test.js
├── README.md
├── app.json
├── img.png
├── package-lock.json
└── package.json
├── package-lock.json
├── package.json
├── src
├── image-viewer.component.tsx
├── image-viewer.style.ts
├── image-viewer.type.ts
└── index.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | built
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | demo
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 120,
4 | "proseWrap": "never",
5 | "requirePragma": false,
6 | "semi": true,
7 | "singleQuote": true,
8 | "tabWidth": 2,
9 | "trailingComma": "none",
10 | "useTabs": false,
11 | "overrides": [
12 | {
13 | "files": "*.json",
14 | "options": {
15 | "printWidth": 200
16 | }
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 ascoders
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Show Cases
2 |
3 | **Swiper image**
4 |
5 | 
6 |
7 | **Zoom while sliding**
8 |
9 | 
10 |
11 | **Swipe down**
12 |
13 | 
14 |
15 | ## Getting Started
16 |
17 | ### Installation
18 |
19 | ```bash
20 | npm i react-native-image-zoom-viewer --save
21 | ```
22 |
23 | ### Basic Usage
24 |
25 | - Install create-react-native-app first
26 |
27 | ```bash
28 | $ npm install -g create-react-native-app
29 | ```
30 |
31 | - Initialization of a react-native project
32 |
33 | ```bash
34 | $ create-react-native-app AwesomeProject
35 | ```
36 |
37 | - Then, edit `AwesomeProject/App.js`, like this:
38 |
39 | ```typescript
40 | import { Modal } from 'react-native';
41 | import ImageViewer from 'react-native-image-zoom-viewer';
42 |
43 | const images = [{
44 | // Simplest usage.
45 | url: 'https://avatars2.githubusercontent.com/u/7970947?v=3&s=460',
46 |
47 | // width: number
48 | // height: number
49 | // Optional, if you know the image size, you can set the optimization performance
50 |
51 | // You can pass props to .
52 | props: {
53 | // headers: ...
54 | }
55 | }, {
56 | url: '',
57 | props: {
58 | // Or you can set source directory.
59 | source: require('../background.png')
60 | }
61 | }]
62 |
63 | export default class App extends React.Component {
64 | render: function() {
65 | return (
66 |
67 |
68 |
69 | )
70 | }
71 | }
72 | ```
73 |
74 | ### Props
75 |
76 | | parameter | type | required | description | default |
77 | | :--------------------- | :------------------------------------------------------------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------- |
78 | | imageUrls | array | yes | Image Source | |
79 | | enableImageZoom | boolean | no | Enable image zoom | `true` |
80 | | onShowModal | function
`(content?: JSX.Element) => void` | no | The callback for show modal | `() => {}` |
81 | | onCancel | function
`() => void` | no | The callback for cancel modal | `() => {}` |
82 | | flipThreshold | number | no | Swipe threshold of the next page | `80` |
83 | | maxOverflow | number | no | The X position maximum, that current page can slide to the next page | `300` |
84 | | index | number | no | Init index of images | `0` |
85 | | failImageSource | string, object
`{url: string}` | no | placeholder for fail | `''` |
86 | | loadingRender | function
`() => React.ReactElement` | no | placeholder for loading | `() => null` |
87 | | onSaveToCamera | function
`(index?: number => void` | no | The callback for save to camera | `() => {}` |
88 | | onChange | function
`(index?: number => void` | no | When the image changed | `() => {}` |
89 | | onMove | ( position: [IOnMove](https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts) )=>void | reports movement position data (helpful to build overlays) | ()=> {} |
90 | | saveToLocalByLongPress | boolean | no | Enable save to camera when long press | `true` |
91 | | onClick | function
`(onCancel?: function) => void` | no | Onclick | `(onCancel) => {onCancel()}` |
92 | | onDoubleClick | function
`(onCancel?: function) => void` | no | OnDoubleClick | `(onCancel) => {onCancel()}` |
93 | | onSave | function
`(url: string) => void` | no | The picture is saved to the local method, if you write this method will not call the system default method for Android does not support saveToCameraRoll remote picture, you can call this callback in Android call native interface | |
94 | | renderHeader | function
`(currentIndex?: number) => React.ReactElement` | no | Custom header | `() => null` |
95 | | renderFooter | function
`(currentIndex?: number) => React.ReactElement` | no | Custom footer | `() => null` |
96 | | renderIndicator | function
`(currentIndex?: number, allSize?) => React.ReactElement`: number | no | Custom indicator | `(currentIndex, allSize) => currentIndex + "/" + allSize` |
97 | | renderImage | function
`(props: any) => React.ReactElement` | no | Custom image component | `(props) => ` |
98 | | renderArrowLeft | function
`() => React.ReactElement` | no | Custom left arrow | `() => null` |
99 | | renderArrowRight | function
`() => React.ReactElement` | no | Custom right arrow | `() => null` |
100 | | onSwipeDown | function
`() => void` | no | Callback for swipe down | `() => null` |
101 | | footerContainerStyle | object
`{someStyle: someValue}` | no | custom style props for container that will be holding your footer that you pass | `bottom: 0, position: "absolute", zIndex: 9999` |
102 | | backgroundColor | string
`white` | no | Component background color | `black` |
103 | | enableSwipeDown | boolean | no | Enable swipe down to close image viewer. When swipe down, will trigger onCancel. | `false` |
104 | | swipeDownThreshold | number | no | Threshold for firing swipe down function | |
105 | | doubleClickInterval | number | no | Double click interval. | |
106 | | pageAnimateTime | number | no | Set the animation time for page flipping. | `100` |
107 | | enablePreload | boolean | no | Preload the next image | `false` |
108 | | useNativeDriver | boolean | no | Whether to animate using [`useNativeDriver`](https://reactnative.dev/docs/animations#using-the-native-driver) | `false` |
109 | | menus | function
`({cancel,saveToLocal}) => React.ReactElement` | no | Custom menus, with 2 methods:`cancel` to hide menus and `saveToLocal` to save image to camera
110 | | menuContext | object
`{someKey: someValue}` | no | Custom menu context. | `{ saveToLocal: 'save to the album', cancel: 'cancel' }`
111 | ## Development pattern
112 |
113 | ### Step 1, run TS listener
114 |
115 | After clone this repo, then:
116 |
117 | ```bash
118 | npm install
119 | npm start
120 | ```
121 |
122 | ### Step 2, run demo
123 |
124 | ```bash
125 | cd demo
126 | npm install
127 | npm start
128 | ```
129 |
130 | Then, scan the QR, use your [expo app](https://expo.io./).
131 |
132 | ### Dependence
133 |
134 | Depend on `react-native-image-pan-zoom`: https://github.com/ascoders/react-native-image-zoom
135 |
--------------------------------------------------------------------------------
/demo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-expo"]
3 | }
4 |
--------------------------------------------------------------------------------
/demo/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | ; We fork some components by platform
3 | .*/*[.]android.js
4 |
5 | ; Ignore templates for 'react-native init'
6 | /node_modules/react-native/local-cli/templates/.*
7 |
8 | ; Ignore RN jest
9 | /node_modules/react-native/jest/.*
10 |
11 | ; Ignore RNTester
12 | /node_modules/react-native/RNTester/.*
13 |
14 | ; Ignore the website subdir
15 | /node_modules/react-native/website/.*
16 |
17 | ; Ignore the Dangerfile
18 | /node_modules/react-native/danger/dangerfile.js
19 |
20 | ; Ignore Fbemitter
21 | /node_modules/fbemitter/.*
22 |
23 | ; Ignore "BUCK" generated dirs
24 | /node_modules/react-native/\.buckd/
25 |
26 | ; Ignore unexpected extra "@providesModule"
27 | .*/node_modules/.*/node_modules/fbjs/.*
28 |
29 | ; Ignore polyfills
30 | /node_modules/react-native/Libraries/polyfills/.*
31 |
32 | ; Ignore various node_modules
33 | /node_modules/react-native-gesture-handler/.*
34 | /node_modules/expo/.*
35 | /node_modules/react-navigation/.*
36 | /node_modules/xdl/.*
37 | /node_modules/reqwest/.*
38 | /node_modules/metro-bundler/.*
39 |
40 | [include]
41 |
42 | [libs]
43 | node_modules/react-native/Libraries/react-native/react-native-interface.js
44 | node_modules/react-native/flow/
45 | node_modules/expo/flow/
46 |
47 | [options]
48 | emoji=true
49 |
50 | module.system=haste
51 |
52 | module.file_ext=.js
53 | module.file_ext=.jsx
54 | module.file_ext=.json
55 | module.file_ext=.ios.js
56 |
57 | munge_underscores=true
58 |
59 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
60 |
61 | suppress_type=$FlowIssue
62 | suppress_type=$FlowFixMe
63 | suppress_type=$FlowFixMeProps
64 | suppress_type=$FlowFixMeState
65 | suppress_type=$FixMe
66 |
67 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)
68 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+
69 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
70 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
71 |
72 | unsafe.enable_getters_and_setters=true
73 |
74 | [version]
75 | ^0.56.0
76 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # expo
4 | .expo/
5 |
6 | # dependencies
7 | /node_modules
8 |
9 | # misc
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
--------------------------------------------------------------------------------
/demo/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/demo/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View, Modal, TouchableNativeFeedback, Text } from 'react-native';
3 | import ImageViewer from './built/index';
4 |
5 | const images = [
6 | {
7 | // Simplest usage.
8 | // url: "https://avatars2.githubusercontent.com/u/7970947?v=3&s=460",
9 | // url:
10 | // "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1527660246058&di=6f0f1b19cf05a64317cbc5d2b3713d64&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F0112a85874bd24a801219c7729e77d.jpg",
11 | // You can pass props to .
12 | props: {
13 | // headers: ...
14 | source: require('./img.png')
15 | },
16 | freeHeight: true
17 | },
18 | {
19 | // Simplest usage.
20 | // url: "https://avatars2.githubusercontent.com/u/7970947?v=3&s=460",
21 | // url:
22 | // "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1527660246058&di=6f0f1b19cf05a64317cbc5d2b3713d64&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F0112a85874bd24a801219c7729e77d.jpg",
23 | // You can pass props to .
24 | props: {
25 | // headers: ...
26 | source: require('./img.png')
27 | },
28 | freeHeight: true
29 | }
30 | ];
31 |
32 | export default class Main extends Component {
33 | state = {
34 | index: 0,
35 | modalVisible: true
36 | };
37 |
38 | render() {
39 | return (
40 |
45 | this.setState({ modalVisible: false })}
49 | >
50 | {
54 | console.log('onSwipeDown');
55 | }}
56 | onMove={data => console.log(data)}
57 | enableSwipeDown={true}
58 | />
59 |
60 |
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/demo/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App from './App';
3 |
4 | import renderer from 'react-test-renderer';
5 |
6 | it('renders without crashing', () => {
7 | const rendered = renderer.create().toJSON();
8 | expect(rendered).toBeTruthy();
9 | });
10 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React Native App](https://github.com/react-community/create-react-native-app).
2 |
3 | Below you'll find information about performing common tasks. The most recent version of this guide is available [here](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/README.md).
4 |
5 | ## Table of Contents
6 |
7 | * [Updating to New Releases](#updating-to-new-releases)
8 | * [Available Scripts](#available-scripts)
9 | * [npm start](#npm-start)
10 | * [npm test](#npm-test)
11 | * [npm run ios](#npm-run-ios)
12 | * [npm run android](#npm-run-android)
13 | * [npm run eject](#npm-run-eject)
14 | * [Writing and Running Tests](#writing-and-running-tests)
15 | * [Environment Variables](#environment-variables)
16 | * [Configuring Packager IP Address](#configuring-packager-ip-address)
17 | * [Adding Flow](#adding-flow)
18 | * [Customizing App Display Name and Icon](#customizing-app-display-name-and-icon)
19 | * [Sharing and Deployment](#sharing-and-deployment)
20 | * [Publishing to Expo's React Native Community](#publishing-to-expos-react-native-community)
21 | * [Building an Expo "standalone" app](#building-an-expo-standalone-app)
22 | * [Ejecting from Create React Native App](#ejecting-from-create-react-native-app)
23 | * [Build Dependencies (Xcode & Android Studio)](#build-dependencies-xcode-android-studio)
24 | * [Should I Use ExpoKit?](#should-i-use-expokit)
25 | * [Troubleshooting](#troubleshooting)
26 | * [Networking](#networking)
27 | * [iOS Simulator won't open](#ios-simulator-wont-open)
28 | * [QR Code does not scan](#qr-code-does-not-scan)
29 |
30 | ## Updating to New Releases
31 |
32 | You should only need to update the global installation of `create-react-native-app` very rarely, ideally never.
33 |
34 | Updating the `react-native-scripts` dependency of your app should be as simple as bumping the version number in `package.json` and reinstalling your project's dependencies.
35 |
36 | Upgrading to a new version of React Native requires updating the `react-native`, `react`, and `expo` package versions, and setting the correct `sdkVersion` in `app.json`. See the [versioning guide](https://github.com/react-community/create-react-native-app/blob/master/VERSIONS.md) for up-to-date information about package version compatibility.
37 |
38 | ## Available Scripts
39 |
40 | If Yarn was installed when the project was initialized, then dependencies will have been installed via Yarn, and you should probably use it to run these commands as well. Unlike dependency installation, command running syntax is identical for Yarn and NPM at the time of this writing.
41 |
42 | ### `npm start`
43 |
44 | Runs your app in development mode.
45 |
46 | Open it in the [Expo app](https://expo.io) on your phone to view it. It will reload if you save edits to your files, and you will see build errors and logs in the terminal.
47 |
48 | Sometimes you may need to reset or clear the React Native packager's cache. To do so, you can pass the `--reset-cache` flag to the start script:
49 |
50 | ```
51 | npm start -- --reset-cache
52 | # or
53 | yarn start -- --reset-cache
54 | ```
55 |
56 | #### `npm test`
57 |
58 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests.
59 |
60 | #### `npm run ios`
61 |
62 | Like `npm start`, but also attempts to open your app in the iOS Simulator if you're on a Mac and have it installed.
63 |
64 | #### `npm run android`
65 |
66 | Like `npm start`, but also attempts to open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). We also recommend installing Genymotion as your Android emulator. Once you've finished setting up the native build environment, there are two options for making the right copy of `adb` available to Create React Native App:
67 |
68 | ##### Using Android Studio's `adb`
69 |
70 | 1. Make sure that you can run adb from your terminal.
71 | 2. Open Genymotion and navigate to `Settings -> ADB`. Select “Use custom Android SDK tools” and update with your [Android SDK directory](https://stackoverflow.com/questions/25176594/android-sdk-location).
72 |
73 | ##### Using Genymotion's `adb`
74 |
75 | 1. Find Genymotion’s copy of adb. On macOS for example, this is normally `/Applications/Genymotion.app/Contents/MacOS/tools/`.
76 | 2. Add the Genymotion tools directory to your path (instructions for [Mac](http://osxdaily.com/2014/08/14/add-new-path-to-path-command-line/), [Linux](http://www.computerhope.com/issues/ch001647.htm), and [Windows](https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/)).
77 | 3. Make sure that you can run adb from your terminal.
78 |
79 | #### `npm run eject`
80 |
81 | This will start the process of "ejecting" from Create React Native App's build scripts. You'll be asked a couple of questions about how you'd like to build your project.
82 |
83 | **Warning:** Running eject is a permanent action (aside from whatever version control system you use). An ejected app will require you to have an [Xcode and/or Android Studio environment](https://facebook.github.io/react-native/docs/getting-started.html) set up.
84 |
85 | ## Customizing App Display Name and Icon
86 |
87 | You can edit `app.json` to include [configuration keys](https://docs.expo.io/versions/latest/guides/configuration.html) under the `expo` key.
88 |
89 | To change your app's display name, set the `expo.name` key in `app.json` to an appropriate string.
90 |
91 | To set an app icon, set the `expo.icon` key in `app.json` to be either a local path or a URL. It's recommended that you use a 512x512 png file with transparency.
92 |
93 | ## Writing and Running Tests
94 |
95 | This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test` extension to have the files loaded by jest. See the [the template project](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/App.test.js) for an example test. The [jest documentation](https://facebook.github.io/jest/docs/en/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/en/tutorial-react-native.html).
96 |
97 | ## Environment Variables
98 |
99 | You can configure some of Create React Native App's behavior using environment variables.
100 |
101 | ### Configuring Packager IP Address
102 |
103 | When starting your project, you'll see something like this for your project URL:
104 |
105 | ```
106 | exp://192.168.0.2:19000
107 | ```
108 |
109 | The "manifest" at that URL tells the Expo app how to retrieve and load your app's JavaScript bundle, so even if you load it in the app via a URL like `exp://localhost:19000`, the Expo client app will still try to retrieve your app at the IP address that the start script provides.
110 |
111 | In some cases, this is less than ideal. This might be the case if you need to run your project inside of a virtual machine and you have to access the packager via a different IP address than the one which prints by default. In order to override the IP address or hostname that is detected by Create React Native App, you can specify your own hostname via the `REACT_NATIVE_PACKAGER_HOSTNAME` environment variable:
112 |
113 | Mac and Linux:
114 |
115 | ```
116 | REACT_NATIVE_PACKAGER_HOSTNAME='my-custom-ip-address-or-hostname' npm start
117 | ```
118 |
119 | Windows:
120 | ```
121 | set REACT_NATIVE_PACKAGER_HOSTNAME='my-custom-ip-address-or-hostname'
122 | npm start
123 | ```
124 |
125 | The above example would cause the development server to listen on `exp://my-custom-ip-address-or-hostname:19000`.
126 |
127 | ## Adding Flow
128 |
129 | Flow is a static type checker that helps you write code with fewer bugs. Check out this [introduction to using static types in JavaScript](https://medium.com/@preethikasireddy/why-use-static-types-in-javascript-part-1-8382da1e0adb) if you are new to this concept.
130 |
131 | React Native works with [Flow](http://flowtype.org/) out of the box, as long as your Flow version matches the one used in the version of React Native.
132 |
133 | To add a local dependency to the correct Flow version to a Create React Native App project, follow these steps:
134 |
135 | 1. Find the Flow `[version]` at the bottom of the included [.flowconfig](.flowconfig)
136 | 2. Run `npm install --save-dev flow-bin@x.y.z` (or `yarn add --dev flow-bin@x.y.z`), where `x.y.z` is the .flowconfig version number.
137 | 3. Add `"flow": "flow"` to the `scripts` section of your `package.json`.
138 | 4. Add `// @flow` to any files you want to type check (for example, to `App.js`).
139 |
140 | Now you can run `npm run flow` (or `yarn flow`) to check the files for type errors.
141 | You can optionally use a [plugin for your IDE or editor](https://flow.org/en/docs/editors/) for a better integrated experience.
142 |
143 | To learn more about Flow, check out [its documentation](https://flow.org/).
144 |
145 | ## Sharing and Deployment
146 |
147 | Create React Native App does a lot of work to make app setup and development simple and straightforward, but it's very difficult to do the same for deploying to Apple's App Store or Google's Play Store without relying on a hosted service.
148 |
149 | ### Publishing to Expo's React Native Community
150 |
151 | Expo provides free hosting for the JS-only apps created by CRNA, allowing you to share your app through the Expo client app. This requires registration for an Expo account.
152 |
153 | Install the `exp` command-line tool, and run the publish command:
154 |
155 | ```
156 | $ npm i -g exp
157 | $ exp publish
158 | ```
159 |
160 | ### Building an Expo "standalone" app
161 |
162 | You can also use a service like [Expo's standalone builds](https://docs.expo.io/versions/latest/guides/building-standalone-apps.html) if you want to get an IPA/APK for distribution without having to build the native code yourself.
163 |
164 | ### Ejecting from Create React Native App
165 |
166 | If you want to build and deploy your app yourself, you'll need to eject from CRNA and use Xcode and Android Studio.
167 |
168 | This is usually as simple as running `npm run eject` in your project, which will walk you through the process. Make sure to install `react-native-cli` and follow the [native code getting started guide for React Native](https://facebook.github.io/react-native/docs/getting-started.html).
169 |
170 | #### Should I Use ExpoKit?
171 |
172 | If you have made use of Expo APIs while working on your project, then those API calls will stop working if you eject to a regular React Native project. If you want to continue using those APIs, you can eject to "React Native + ExpoKit" which will still allow you to build your own native code and continue using the Expo APIs. See the [ejecting guide](https://github.com/react-community/create-react-native-app/blob/master/EJECTING.md) for more details about this option.
173 |
174 | ## Troubleshooting
175 |
176 | ### Networking
177 |
178 | If you're unable to load your app on your phone due to a network timeout or a refused connection, a good first step is to verify that your phone and computer are on the same network and that they can reach each other. Create React Native App needs access to ports 19000 and 19001 so ensure that your network and firewall settings allow access from your device to your computer on both of these ports.
179 |
180 | Try opening a web browser on your phone and opening the URL that the packager script prints, replacing `exp://` with `http://`. So, for example, if underneath the QR code in your terminal you see:
181 |
182 | ```
183 | exp://192.168.0.1:19000
184 | ```
185 |
186 | Try opening Safari or Chrome on your phone and loading
187 |
188 | ```
189 | http://192.168.0.1:19000
190 | ```
191 |
192 | and
193 |
194 | ```
195 | http://192.168.0.1:19001
196 | ```
197 |
198 | If this works, but you're still unable to load your app by scanning the QR code, please open an issue on the [Create React Native App repository](https://github.com/react-community/create-react-native-app) with details about these steps and any other error messages you may have received.
199 |
200 | If you're not able to load the `http` URL in your phone's web browser, try using the tethering/mobile hotspot feature on your phone (beware of data usage, though), connecting your computer to that WiFi network, and restarting the packager.
201 |
202 | ### iOS Simulator won't open
203 |
204 | If you're on a Mac, there are a few errors that users sometimes see when attempting to `npm run ios`:
205 |
206 | * "non-zero exit code: 107"
207 | * "You may need to install Xcode" but it is already installed
208 | * and others
209 |
210 | There are a few steps you may want to take to troubleshoot these kinds of errors:
211 |
212 | 1. Make sure Xcode is installed and open it to accept the license agreement if it prompts you. You can install it from the Mac App Store.
213 | 2. Open Xcode's Preferences, the Locations tab, and make sure that the `Command Line Tools` menu option is set to something. Sometimes when the CLI tools are first installed by Homebrew this option is left blank, which can prevent Apple utilities from finding the simulator. Make sure to re-run `npm/yarn run ios` after doing so.
214 | 3. If that doesn't work, open the Simulator, and under the app menu select `Reset Contents and Settings...`. After that has finished, quit the Simulator, and re-run `npm/yarn run ios`.
215 |
216 | ### QR Code does not scan
217 |
218 | If you're not able to scan the QR code, make sure your phone's camera is focusing correctly, and also make sure that the contrast on the two colors in your terminal is high enough. For example, WebStorm's default themes may [not have enough contrast](https://github.com/react-community/create-react-native-app/issues/49) for terminal QR codes to be scannable with the system barcode scanners that the Expo app uses.
219 |
220 | If this causes problems for you, you may want to try changing your terminal's color theme to have more contrast, or running Create React Native App from a different terminal. You can also manually enter the URL printed by the packager script in the Expo app's search bar to load it manually.
221 |
--------------------------------------------------------------------------------
/demo/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "sdkVersion": "35.0.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/demo/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ascoders/react-native-image-viewer/e9130e70c816e798fedde61b773ceca3a03b2a7b/demo/img.png
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "babel-preset-expo": "^7.1.0",
7 | "jest-expo": "^35.0.0",
8 | "react-test-renderer": "16.0.0"
9 | },
10 | "main": "node_modules/expo/AppEntry.js",
11 | "scripts": {
12 | "start": "expo start -c",
13 | "eject": "expo eject",
14 | "android": "expo start --android",
15 | "ios": "expo start --ios",
16 | "test": "node node_modules/jest/bin/jest.js --watch"
17 | },
18 | "jest": {
19 | "preset": "jest-expo"
20 | },
21 | "dependencies": {
22 | "expo": "^35.0.0",
23 | "react": "16.8.3",
24 | "react-native": "0.59.8",
25 | "react-native-image-pan-zoom": "^2.1.7"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-image-zoom-viewer",
3 | "version": "3.0.1",
4 | "description": "react native image viewer,大图浏览",
5 | "main": "built/index.js",
6 | "types": "built/index.d.ts",
7 | "scripts": {
8 | "prepare": "rm -rf built && tsc",
9 | "start": "tsc -w --outDir demo/built"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/ascoders/react-native-image-viewer.git"
14 | },
15 | "keywords": [
16 | "image-viewer"
17 | ],
18 | "author": "ascoders",
19 | "license": "MIT",
20 | "dependencies": {
21 | "react-native-image-pan-zoom": "^2.1.12"
22 | },
23 | "peerDependencies": {
24 | "react": "*",
25 | "react-native": "*"
26 | },
27 | "devDependencies": {
28 | "@types/react": "*",
29 | "@types/react-native": "*",
30 | "ascoders-tslint-config": "^1.0.2",
31 | "react": "*",
32 | "react-native": "*",
33 | "tslint": "^5.8.0",
34 | "tslint-config-prettier": "^1.13.0",
35 | "typescript": "^2.9.2"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/image-viewer.component.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import {
4 | Animated,
5 | CameraRoll,
6 | Dimensions,
7 | I18nManager,
8 | Image,
9 | PanResponder,
10 | Platform,
11 | Text,
12 | TouchableHighlight,
13 | TouchableOpacity,
14 | TouchableWithoutFeedback,
15 | View,
16 | ViewStyle
17 | } from 'react-native';
18 | import ImageZoom from 'react-native-image-pan-zoom';
19 | import styles from './image-viewer.style';
20 | import { IImageInfo, IImageSize, Props, State } from './image-viewer.type';
21 |
22 | export default class ImageViewer extends React.Component {
23 | public static defaultProps = new Props();
24 | public state = new State();
25 |
26 | // 背景透明度渐变动画
27 | private fadeAnim = new Animated.Value(0);
28 |
29 | // 当前基准位置
30 | private standardPositionX = 0;
31 |
32 | // 整体位移,用来切换图片用
33 | private positionXNumber = 0;
34 | private positionX = new Animated.Value(0);
35 |
36 | private width = 0;
37 | private height = 0;
38 |
39 | private styles = styles(0, 0, 'transparent');
40 |
41 | // 是否执行过 layout. fix 安卓不断触发 onLayout 的 bug
42 | private hasLayout = false;
43 |
44 | // 记录已加载的图片 index
45 | private loadedIndex = new Map();
46 |
47 | private handleLongPressWithIndex = new Map();
48 |
49 | private imageRefs: any[] = [];
50 |
51 | public componentDidMount() {
52 | this.init(this.props);
53 | }
54 |
55 | static getDerivedStateFromProps(nextProps: Props, prevState: State) {
56 | if (nextProps.index !== prevState.prevIndexProp) {
57 | return { currentShowIndex: nextProps.index, prevIndexProp: nextProps.index };
58 | }
59 | return null;
60 | }
61 |
62 | public componentDidUpdate(prevProps: Props, prevState: State) {
63 | if (prevProps.index !== this.props.index) {
64 | // 立刻预加载要看的图
65 | this.loadImage(this.props.index || 0);
66 |
67 | this.jumpToCurrentImage();
68 |
69 | // 显示动画
70 | Animated.timing(this.fadeAnim, {
71 | toValue: 1,
72 | duration: 200,
73 | useNativeDriver: !!this.props.useNativeDriver
74 | }).start();
75 | }
76 | }
77 |
78 | /**
79 | * props 有变化时执行
80 | */
81 | public init(nextProps: Props) {
82 | if (nextProps.imageUrls.length === 0) {
83 | // 隐藏时候清空
84 | this.fadeAnim.setValue(0);
85 | return this.setState(new State());
86 | }
87 |
88 | // 给 imageSizes 塞入空数组
89 | const imageSizes: IImageSize[] = [];
90 | nextProps.imageUrls.forEach(imageUrl => {
91 | imageSizes.push({
92 | width: imageUrl.width || 0,
93 | height: imageUrl.height || 0,
94 | status: 'loading'
95 | });
96 | });
97 |
98 | this.setState(
99 | {
100 | currentShowIndex: nextProps.index,
101 | prevIndexProp: nextProps.index || 0,
102 | imageSizes
103 | },
104 | () => {
105 | // 立刻预加载要看的图
106 | this.loadImage(nextProps.index || 0);
107 |
108 | this.jumpToCurrentImage();
109 |
110 | // 显示动画
111 | Animated.timing(this.fadeAnim, {
112 | toValue: 1,
113 | duration: 200,
114 | useNativeDriver: !!nextProps.useNativeDriver
115 | }).start();
116 | }
117 | );
118 | }
119 | /**
120 | * reset Image scale and position
121 | */
122 | public resetImageByIndex = (index: number) => {
123 | this.imageRefs[index] && this.imageRefs[index].reset();
124 | };
125 | /**
126 | * 调到当前看图位置
127 | */
128 | public jumpToCurrentImage() {
129 | // 跳到当前图的位置
130 | this.positionXNumber = this.width * (this.state.currentShowIndex || 0) * (I18nManager.isRTL ? 1 : -1);
131 | this.standardPositionX = this.positionXNumber;
132 | this.positionX.setValue(this.positionXNumber);
133 | }
134 |
135 | /**
136 | * 加载图片,主要是获取图片长与宽
137 | */
138 | public loadImage(index: number) {
139 | if (!this!.state!.imageSizes![index]) {
140 | return;
141 | }
142 |
143 | if (this.loadedIndex.has(index)) {
144 | return;
145 | }
146 | this.loadedIndex.set(index, true);
147 |
148 | const image = this.props.imageUrls[index];
149 | const imageStatus = { ...this!.state!.imageSizes![index] };
150 |
151 | // 保存 imageSize
152 | const saveImageSize = () => {
153 | // 如果已经 success 了,就不做处理
154 | if (this!.state!.imageSizes![index] && this!.state!.imageSizes![index].status !== 'loading') {
155 | return;
156 | }
157 |
158 | const imageSizes = this!.state!.imageSizes!.slice();
159 | imageSizes[index] = imageStatus;
160 | this.setState({ imageSizes });
161 | };
162 |
163 | if (this!.state!.imageSizes![index].status === 'success') {
164 | // 已经加载过就不会加载了
165 | return;
166 | }
167 |
168 | // 如果已经有宽高了,直接设置为 success
169 | if (this!.state!.imageSizes![index].width > 0 && this!.state!.imageSizes![index].height > 0) {
170 | imageStatus.status = 'success';
171 | saveImageSize();
172 | return;
173 | }
174 |
175 | // 是否加载完毕了图片大小
176 | const sizeLoaded = false;
177 | // 是否加载完毕了图片
178 | let imageLoaded = false;
179 |
180 | // Tagged success if url is started with file:, or not set yet(for custom source.uri).
181 | if (!image.url || image.url.startsWith(`file:`)) {
182 | imageLoaded = true;
183 | }
184 |
185 | // 如果已知源图片宽高,直接设置为 success
186 | if (image.width && image.height) {
187 | if (this.props.enablePreload && imageLoaded === false) {
188 | Image.prefetch(image.url);
189 | }
190 | imageStatus.width = image.width;
191 | imageStatus.height = image.height;
192 | imageStatus.status = 'success';
193 | saveImageSize();
194 | return;
195 | }
196 |
197 | Image.getSize(
198 | image.url,
199 | (width: number, height: number) => {
200 | imageStatus.width = width;
201 | imageStatus.height = height;
202 | imageStatus.status = 'success';
203 | saveImageSize();
204 | },
205 | () => {
206 | try {
207 | const data = (Image as any).resolveAssetSource(image.props.source);
208 | imageStatus.width = data.width;
209 | imageStatus.height = data.height;
210 | imageStatus.status = 'success';
211 | saveImageSize();
212 | } catch (newError) {
213 | // Give up..
214 | imageStatus.status = 'fail';
215 | saveImageSize();
216 | }
217 | }
218 | );
219 | }
220 |
221 | /**
222 | * 预加载图片
223 | */
224 | public preloadImage = (index: number) => {
225 | if (index < this.state.imageSizes!.length) {
226 | this.loadImage(index + 1);
227 | }
228 | };
229 | /**
230 | * 触发溢出水平滚动
231 | */
232 | public handleHorizontalOuterRangeOffset = (offsetX: number = 0) => {
233 | this.positionXNumber = this.standardPositionX + offsetX;
234 | this.positionX.setValue(this.positionXNumber);
235 |
236 | const offsetXRTL = !I18nManager.isRTL ? offsetX : -offsetX;
237 |
238 | if (offsetXRTL < 0) {
239 | if (this!.state!.currentShowIndex || 0 < this.props.imageUrls.length - 1) {
240 | this.loadImage((this!.state!.currentShowIndex || 0) + 1);
241 | }
242 | } else if (offsetXRTL > 0) {
243 | if (this!.state!.currentShowIndex || 0 > 0) {
244 | this.loadImage((this!.state!.currentShowIndex || 0) - 1);
245 | }
246 | }
247 | };
248 |
249 | /**
250 | * 手势结束,但是没有取消浏览大图
251 | */
252 | public handleResponderRelease = (vx: number = 0) => {
253 | const vxRTL = I18nManager.isRTL ? -vx : vx;
254 | const isLeftMove = I18nManager.isRTL
255 | ? this.positionXNumber - this.standardPositionX < -(this.props.flipThreshold || 0)
256 | : this.positionXNumber - this.standardPositionX > (this.props.flipThreshold || 0);
257 | const isRightMove = I18nManager.isRTL
258 | ? this.positionXNumber - this.standardPositionX > (this.props.flipThreshold || 0)
259 | : this.positionXNumber - this.standardPositionX < -(this.props.flipThreshold || 0);
260 |
261 | if (vxRTL > 0.7) {
262 | // 上一张
263 | this.goBack.call(this);
264 |
265 | // 这里可能没有触发溢出滚动,为了防止图片不被加载,调用加载图片
266 | if (this.state.currentShowIndex || 0 > 0) {
267 | this.loadImage((this.state.currentShowIndex || 0) - 1);
268 | }
269 | return;
270 | } else if (vxRTL < -0.7) {
271 | // 下一张
272 | this.goNext.call(this);
273 | if (this.state.currentShowIndex || 0 < this.props.imageUrls.length - 1) {
274 | this.loadImage((this.state.currentShowIndex || 0) + 1);
275 | }
276 | return;
277 | }
278 |
279 | if (isLeftMove) {
280 | // 上一张
281 | this.goBack.call(this);
282 | } else if (isRightMove) {
283 | // 下一张
284 | this.goNext.call(this);
285 | return;
286 | } else {
287 | // 回到之前的位置
288 | this.resetPosition.call(this);
289 | return;
290 | }
291 | };
292 |
293 | /**
294 | * 到上一张
295 | */
296 | public goBack = () => {
297 | if (this.state.currentShowIndex === 0) {
298 | // 回到之前的位置
299 | this.resetPosition.call(this);
300 | return;
301 | }
302 |
303 | this.positionXNumber = !I18nManager.isRTL
304 | ? this.standardPositionX + this.width
305 | : this.standardPositionX - this.width;
306 | this.standardPositionX = this.positionXNumber;
307 | Animated.timing(this.positionX, {
308 | toValue: this.positionXNumber,
309 | duration: this.props.pageAnimateTime,
310 | useNativeDriver: !!this.props.useNativeDriver
311 | }).start();
312 |
313 | const nextIndex = (this.state.currentShowIndex || 0) - 1;
314 |
315 | this.setState(
316 | {
317 | currentShowIndex: nextIndex
318 | },
319 | () => {
320 | if (this.props.onChange) {
321 | this.props.onChange(this.state.currentShowIndex);
322 | }
323 | }
324 | );
325 | };
326 |
327 | /**
328 | * 到下一张
329 | */
330 | public goNext = () => {
331 | if (this.state.currentShowIndex === this.props.imageUrls.length - 1) {
332 | // 回到之前的位置
333 | this.resetPosition.call(this);
334 | return;
335 | }
336 |
337 | this.positionXNumber = !I18nManager.isRTL
338 | ? this.standardPositionX - this.width
339 | : this.standardPositionX + this.width;
340 | this.standardPositionX = this.positionXNumber;
341 | Animated.timing(this.positionX, {
342 | toValue: this.positionXNumber,
343 | duration: this.props.pageAnimateTime,
344 | useNativeDriver: !!this.props.useNativeDriver
345 | }).start();
346 |
347 | const nextIndex = (this.state.currentShowIndex || 0) + 1;
348 |
349 | this.setState(
350 | {
351 | currentShowIndex: nextIndex
352 | },
353 | () => {
354 | if (this.props.onChange) {
355 | this.props.onChange(this.state.currentShowIndex);
356 | }
357 | }
358 | );
359 | };
360 |
361 | /**
362 | * 回到原位
363 | */
364 | public resetPosition() {
365 | this.positionXNumber = this.standardPositionX;
366 | Animated.timing(this.positionX, {
367 | toValue: this.standardPositionX,
368 | duration: 150,
369 | useNativeDriver: !!this.props.useNativeDriver
370 | }).start();
371 | }
372 |
373 | /**
374 | * 长按
375 | */
376 | public handleLongPress = (image: IImageInfo) => {
377 | if (this.props.saveToLocalByLongPress) {
378 | // 出现保存到本地的操作框
379 | this.setState({ isShowMenu: true });
380 | }
381 |
382 | if (this.props.onLongPress) {
383 | this.props.onLongPress(image);
384 | }
385 | };
386 |
387 | /**
388 | * 单击
389 | */
390 | public handleClick = () => {
391 | if (this.props.onClick) {
392 | this.props.onClick(this.handleCancel, this.state.currentShowIndex);
393 | }
394 | };
395 |
396 | /**
397 | * 双击
398 | */
399 | public handleDoubleClick = () => {
400 | if (this.props.onDoubleClick) {
401 | this.props.onDoubleClick(this.handleCancel);
402 | }
403 | };
404 |
405 | /**
406 | * 退出
407 | */
408 | public handleCancel = () => {
409 | this.hasLayout = false;
410 | if (this.props.onCancel) {
411 | this.props.onCancel();
412 | }
413 | };
414 |
415 | /**
416 | * 完成布局
417 | */
418 | public handleLayout = (event: any) => {
419 | if (event.nativeEvent.layout.width !== this.width) {
420 | this.hasLayout = true;
421 |
422 | this.width = event.nativeEvent.layout.width;
423 | this.height = event.nativeEvent.layout.height;
424 | this.styles = styles(this.width, this.height, this.props.backgroundColor || 'transparent');
425 |
426 | // 强制刷新
427 | this.forceUpdate();
428 | this.jumpToCurrentImage();
429 | }
430 | };
431 |
432 | /**
433 | * 获得整体内容
434 | */
435 | public getContent() {
436 | // 获得屏幕宽高
437 | const screenWidth = this.width;
438 | const screenHeight = this.height;
439 |
440 | const ImageElements = this.props.imageUrls.map((image, index) => {
441 | if ((this.state.currentShowIndex || 0) > index + 1 || (this.state.currentShowIndex || 0) < index - 1) {
442 | return ;
443 | }
444 |
445 | if (!this.handleLongPressWithIndex.has(index)) {
446 | this.handleLongPressWithIndex.set(index, this.handleLongPress.bind(this, image));
447 | }
448 |
449 | let width = this!.state!.imageSizes![index] && this!.state!.imageSizes![index].width;
450 | let height = this.state.imageSizes![index] && this.state.imageSizes![index].height;
451 | const imageInfo = this.state.imageSizes![index];
452 |
453 | if (!imageInfo || !imageInfo.status) {
454 | return ;
455 | }
456 |
457 | // 如果宽大于屏幕宽度,整体缩放到宽度是屏幕宽度
458 | if (width > screenWidth) {
459 | const widthPixel = screenWidth / width;
460 | width *= widthPixel;
461 | height *= widthPixel;
462 | }
463 |
464 | // 如果此时高度还大于屏幕高度,整体缩放到高度是屏幕高度
465 | if (height > screenHeight) {
466 | const HeightPixel = screenHeight / height;
467 | width *= HeightPixel;
468 | height *= HeightPixel;
469 | }
470 |
471 | const Wrapper = ({ children, ...others }: any) => (
472 |
490 | {children}
491 |
492 | );
493 |
494 | switch (imageInfo.status) {
495 | case 'loading':
496 | return (
497 |
506 | {this!.props!.loadingRender!()}
507 |
508 | );
509 | case 'success':
510 | if (!image.props) {
511 | image.props = {};
512 | }
513 |
514 | if (!image.props.style) {
515 | image.props.style = {};
516 | }
517 | image.props.style = {
518 | ...this.styles.imageStyle, // User config can override above.
519 | ...image.props.style,
520 | width,
521 | height
522 | };
523 |
524 | if (typeof image.props.source === 'number') {
525 | // source = require(..), doing nothing
526 | } else {
527 | if (!image.props.source) {
528 | image.props.source = {};
529 | }
530 | image.props.source = {
531 | uri: image.url,
532 | ...image.props.source
533 | };
534 | }
535 | if (this.props.enablePreload) {
536 | this.preloadImage(this.state.currentShowIndex || 0);
537 | }
538 | return (
539 | (this.imageRefs[index] = el)}
542 | cropWidth={this.width}
543 | cropHeight={this.height}
544 | maxOverflow={this.props.maxOverflow}
545 | horizontalOuterRangeOffset={this.handleHorizontalOuterRangeOffset}
546 | responderRelease={this.handleResponderRelease}
547 | onMove={this.props.onMove}
548 | onLongPress={this.handleLongPressWithIndex.get(index)}
549 | onClick={this.handleClick}
550 | onDoubleClick={this.handleDoubleClick}
551 | imageWidth={width}
552 | imageHeight={height}
553 | enableSwipeDown={this.props.enableSwipeDown}
554 | swipeDownThreshold={this.props.swipeDownThreshold}
555 | onSwipeDown={this.handleSwipeDown}
556 | panToMove={!this.state.isShowMenu}
557 | pinchToZoom={this.props.enableImageZoom && !this.state.isShowMenu}
558 | enableDoubleClickZoom={this.props.enableImageZoom && !this.state.isShowMenu}
559 | doubleClickInterval={this.props.doubleClickInterval}
560 | minScale={this.props.minScale}
561 | maxScale={this.props.maxScale}
562 | >
563 | {this!.props!.renderImage!(image.props)}
564 |
565 | );
566 | case 'fail':
567 | return (
568 |
574 | {this.props.failImageSource &&
575 | this!.props!.renderImage!({
576 | source: {
577 | uri: this.props.failImageSource.url
578 | },
579 | style: {
580 | width: this.props.failImageSource.width,
581 | height: this.props.failImageSource.height
582 | }
583 | })}
584 |
585 | );
586 | }
587 | });
588 |
589 | return (
590 |
591 |
592 | {this!.props!.renderHeader!(this.state.currentShowIndex)}
593 |
594 |
595 |
596 | {this!.props!.renderArrowLeft!()}
597 |
598 |
599 |
600 |
601 |
602 | {this!.props!.renderArrowRight!()}
603 |
604 |
605 |
606 |
613 | {ImageElements}
614 |
615 | {this!.props!.renderIndicator!((this.state.currentShowIndex || 0) + 1, this.props.imageUrls.length)}
616 |
617 | {this.props.imageUrls[this.state.currentShowIndex || 0] &&
618 | this.props.imageUrls[this.state.currentShowIndex || 0].originSizeKb &&
619 | this.props.imageUrls[this.state.currentShowIndex || 0].originUrl && (
620 |
621 |
622 | 查看原图(2M)
623 |
624 |
625 | )}
626 |
627 | {this!.props!.renderFooter!(this.state.currentShowIndex || 0)}
628 |
629 |
630 |
631 | );
632 | }
633 |
634 | /**
635 | * 保存当前图片到本地相册
636 | */
637 | public saveToLocal = () => {
638 | if (!this.props.onSave) {
639 | CameraRoll.saveToCameraRoll(this.props.imageUrls[this.state.currentShowIndex || 0].url);
640 | this!.props!.onSaveToCamera!(this.state.currentShowIndex);
641 | } else {
642 | this.props.onSave(this.props.imageUrls[this.state.currentShowIndex || 0].url);
643 | }
644 |
645 | this.setState({ isShowMenu: false });
646 | };
647 |
648 | public getMenu() {
649 | if (!this.state.isShowMenu) {
650 | return null;
651 | }
652 |
653 | if (this.props.menus) {
654 | return (
655 |
656 | {this.props.menus({ cancel: this.handleLeaveMenu, saveToLocal: this.saveToLocal })}
657 |
658 | );
659 | }
660 |
661 | return (
662 |
663 |
664 |
665 |
666 | {this.props.menuContext.saveToLocal}
667 |
668 |
673 | {this.props.menuContext.cancel}
674 |
675 |
676 |
677 | );
678 | }
679 |
680 | public handleLeaveMenu = () => {
681 | this.setState({ isShowMenu: false });
682 | };
683 |
684 | public handleSwipeDown = () => {
685 | if (this.props.onSwipeDown) {
686 | this.props.onSwipeDown();
687 | }
688 | this.handleCancel();
689 | };
690 |
691 | public render() {
692 | let childs: React.ReactElement = null as any;
693 |
694 | childs = (
695 |
696 | {this.getContent()}
697 | {this.getMenu()}
698 |
699 | );
700 |
701 | return (
702 |
710 | {childs}
711 |
712 | );
713 | }
714 | }
715 |
--------------------------------------------------------------------------------
/src/image-viewer.style.ts:
--------------------------------------------------------------------------------
1 | import { TextStyle, ViewStyle } from 'react-native';
2 |
3 | export default (
4 | width: number,
5 | height: number,
6 | backgroundColor: string
7 | ): {
8 | [x: string]: ViewStyle | TextStyle;
9 | } => {
10 | return {
11 | modalContainer: { backgroundColor, justifyContent: 'center', alignItems: 'center', overflow: 'hidden' },
12 | watchOrigin: { position: 'absolute', width, bottom: 20, justifyContent: 'center', alignItems: 'center' },
13 | watchOriginTouchable: {
14 | paddingLeft: 10,
15 | paddingRight: 10,
16 | paddingTop: 5,
17 | paddingBottom: 5,
18 | borderRadius: 30,
19 | borderColor: 'white',
20 | borderWidth: 0.5,
21 | backgroundColor: 'rgba(0, 0, 0, 0.1)'
22 | },
23 | watchOriginText: { color: 'white', backgroundColor: 'transparent' },
24 | imageStyle: {},
25 | container: { backgroundColor }, // 多图浏览需要调整整体位置的盒子
26 | moveBox: { flexDirection: 'row', alignItems: 'center' },
27 | menuContainer: { position: 'absolute', width, height, left: 0, bottom: 0, zIndex: 12 },
28 | menuShadow: {
29 | position: 'absolute',
30 | width,
31 | height,
32 | backgroundColor: 'black',
33 | left: 0,
34 | bottom: 0,
35 | opacity: 0.2,
36 | zIndex: 10
37 | },
38 | menuContent: { position: 'absolute', width, left: 0, bottom: 0, zIndex: 11 },
39 | operateContainer: {
40 | justifyContent: 'center',
41 | alignItems: 'center',
42 | backgroundColor: 'white',
43 | height: 40,
44 | borderBottomColor: '#ccc',
45 | borderBottomWidth: 1
46 | },
47 | operateText: { color: '#333' },
48 | loadingTouchable: { width, height },
49 | loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
50 | arrowLeftContainer: { position: 'absolute', top: 0, bottom: 0, left: 0, justifyContent: 'center', zIndex: 13 },
51 | arrowRightContainer: { position: 'absolute', top: 0, bottom: 0, right: 0, justifyContent: 'center', zIndex: 13 }
52 | };
53 | };
54 |
55 | export const simpleStyle: {
56 | [x: string]: ViewStyle | TextStyle;
57 | } = {
58 | count: {
59 | position: 'absolute',
60 | left: 0,
61 | right: 0,
62 | top: 38,
63 | zIndex: 13,
64 | justifyContent: 'center',
65 | alignItems: 'center',
66 | backgroundColor: 'transparent'
67 | },
68 | countText: {
69 | color: 'white',
70 | fontSize: 16,
71 | backgroundColor: 'transparent',
72 | textShadowColor: 'rgba(0, 0, 0, 0.3)',
73 | textShadowOffset: {
74 | width: 0,
75 | height: 0.5
76 | },
77 | textShadowRadius: 0
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/src/image-viewer.type.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Image, ImageURISource, Text, View, ViewStyle } from 'react-native';
3 | import { simpleStyle } from './image-viewer.style';
4 |
5 | interface IOnMove {
6 | type: string;
7 | positionX: number;
8 | positionY: number;
9 | scale: number;
10 | zoomCurrentDistance: number;
11 | }
12 |
13 | export class Props {
14 | /**
15 | * 是否显示
16 | */
17 | public show?: boolean = false;
18 |
19 | /**
20 | * 图片数组
21 | */
22 | public imageUrls: IImageInfo[] = [];
23 |
24 | /**
25 | * 滑动到下一页的X阈值
26 | */
27 | public flipThreshold?: number = 80;
28 |
29 | /**
30 | * 当前页能滑到下一页X位置最大值
31 | */
32 | public maxOverflow?: number = 300;
33 |
34 | /**
35 | * 初始显示第几张图
36 | */
37 | public index?: number = 0;
38 |
39 | /**
40 | * 加载失败的图
41 | */
42 | public failImageSource?: IImageInfo = undefined;
43 |
44 | /**
45 | * 背景颜色
46 | */
47 | public backgroundColor?: string = 'black';
48 |
49 | /**
50 | * style props for the footer container
51 | */
52 | public footerContainerStyle?: object = {};
53 |
54 | /**
55 | * Menu Context Values
56 | */
57 | public menuContext?: any = { saveToLocal: 'save to the album', cancel: 'cancel' };
58 |
59 | /**
60 | * 是否开启长按保存到本地的功能
61 | */
62 | public saveToLocalByLongPress?: boolean = true;
63 |
64 | /**
65 | * 是否允许缩放图片
66 | */
67 | public enableImageZoom?: boolean = true;
68 |
69 | public style?: ViewStyle = {};
70 |
71 | /**
72 | * Enable swipe down to close image viewer.
73 | * When swipe down, will trigger onCancel.
74 | */
75 | public enableSwipeDown?: boolean = false;
76 |
77 | /**
78 | * threshold for firing swipe down function
79 | */
80 | public swipeDownThreshold?: number;
81 |
82 | public doubleClickInterval?: number;
83 |
84 | /**
85 | * Min and Max scale for zooming
86 | */
87 | public minScale?: number;
88 |
89 | public maxScale?: number;
90 |
91 | /**
92 | * 是否预加载图片
93 | */
94 | public enablePreload?: boolean = false;
95 |
96 | /**
97 | * 翻页时的动画时间
98 | */
99 | public pageAnimateTime?: number = 100;
100 |
101 | /**
102 | * 是否启用原生动画驱动
103 | * Whether to use the native code to perform animations.
104 | */
105 | public useNativeDriver?: boolean = false;
106 |
107 | /**
108 | * 长按图片的回调
109 | */
110 | public onLongPress?: (image?: IImageInfo) => void = () => {
111 | //
112 | };
113 |
114 | /**
115 | * 单击回调
116 | */
117 | public onClick?: (close?: () => any, currentShowIndex?: number) => void = () => {
118 | //
119 | };
120 |
121 | /**
122 | * 双击回调
123 | */
124 | public onDoubleClick?: (close?: () => any) => void = () => {
125 | //
126 | };
127 |
128 | /**
129 | * 图片保存到本地方法,如果写了这个方法,就不会调取系统默认方法
130 | * 针对安卓不支持 saveToCameraRoll 远程图片,可以在安卓调用此回调,调用安卓原生接口
131 | */
132 | public onSave?: (url: string) => void = () => {
133 | //
134 | };
135 |
136 | public onMove?: (position?: IOnMove) => void = () => {
137 | //
138 | };
139 |
140 | /**
141 | * 自定义头部
142 | */
143 | public renderHeader?: (currentIndex?: number) => React.ReactElement = () => {
144 | return null as any;
145 | };
146 |
147 | /**
148 | * 自定义尾部
149 | */
150 | public renderFooter?: (currentIndex: number) => React.ReactElement = () => {
151 | return null as any;
152 | };
153 |
154 | /**
155 | * 自定义计时器
156 | */
157 | public renderIndicator?: (currentIndex?: number, allSize?: number) => React.ReactElement = (
158 | currentIndex?: number,
159 | allSize?: number
160 | ) => {
161 | return React.createElement(
162 | View,
163 | { style: simpleStyle.count },
164 | React.createElement(Text, { style: simpleStyle.countText }, currentIndex + '/' + allSize)
165 | );
166 | };
167 |
168 | /**
169 | * Render image component
170 | */
171 | public renderImage?: (props: any) => React.ReactElement = (props: any) => {
172 | return React.createElement(Image, props);
173 | };
174 |
175 | /**
176 | * 自定义左翻页按钮
177 | */
178 | public renderArrowLeft?: () => React.ReactElement = () => {
179 | return null as any;
180 | };
181 |
182 | /**
183 | * 自定义右翻页按钮
184 | */
185 | public renderArrowRight?: () => React.ReactElement = () => {
186 | return null as any;
187 | };
188 |
189 | /**
190 | * 弹出大图的回调
191 | */
192 | public onShowModal?: (content?: any) => void = () => {
193 | //
194 | };
195 |
196 | /**
197 | * 取消看图的回调
198 | */
199 | public onCancel?: () => void = () => {
200 | //
201 | };
202 |
203 | /**
204 | * function that fires when user swipes down
205 | */
206 | public onSwipeDown?: () => void = () => {
207 | //
208 | };
209 |
210 | /**
211 | * 渲染loading元素
212 | */
213 | public loadingRender?: () => React.ReactElement = () => {
214 | return null as any;
215 | };
216 |
217 | /**
218 | * 保存到相册的回调
219 | */
220 | public onSaveToCamera?: (index?: number) => void = () => {
221 | //
222 | };
223 |
224 | /**
225 | * 当图片切换时触发
226 | */
227 | public onChange?: (index?: number) => void = () => {
228 | //
229 | };
230 |
231 | public menus?: ({ cancel, saveToLocal }: any) => React.ReactElement;
232 | }
233 |
234 | export class State {
235 | /**
236 | * 是否显示
237 | */
238 | public show?: boolean = false;
239 |
240 | /**
241 | * 当前显示第几个
242 | */
243 | public currentShowIndex?: number = 0;
244 |
245 | /**
246 | * Used to detect if parent component applied new index prop
247 | */
248 | public prevIndexProp?: number = 0;
249 |
250 | /**
251 | * 图片拉取是否完毕了
252 | */
253 | public imageLoaded?: boolean = false;
254 |
255 | /**
256 | * 图片长宽列表
257 | */
258 | public imageSizes?: IImageSize[] = [];
259 |
260 | /**
261 | * 是否出现功能菜单
262 | */
263 | public isShowMenu?: boolean = false;
264 | }
265 |
266 | export interface IImageInfo {
267 | url: string;
268 | /**
269 | * 没有的话会自动拉取
270 | */
271 | width?: number;
272 | /**
273 | * 没有的话会自动拉取
274 | */
275 | height?: number;
276 | /**
277 | * 图片字节大小(kb为单位)
278 | */
279 | sizeKb?: number;
280 | /**
281 | * 原图字节大小(kb为单位)
282 | * 如果设置了这个字段,并且有原图url,则显示查看原图按钮
283 | */
284 | originSizeKb?: number;
285 | /**
286 | * 原图url地址
287 | */
288 | originUrl?: string;
289 | /**
290 | * Pass to image props
291 | */
292 | props?: any;
293 | /**
294 | * 初始是否不超高 TODO:
295 | */
296 | freeHeight?: boolean;
297 | /**
298 | * 初始是否不超高 TODO:
299 | */
300 | freeWidth?: boolean;
301 | }
302 |
303 | export interface IImageSize {
304 | width: number;
305 | height: number;
306 | // 图片加载状态
307 | status: 'loading' | 'success' | 'fail';
308 | }
309 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import ImageViewer from "./image-viewer.component"
2 | import { Props as ImageViewerPropsDefine } from "./image-viewer.type"
3 |
4 | export { ImageViewer, ImageViewerPropsDefine }
5 | export default ImageViewer
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "module": "commonjs",
5 | "strict": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "jsx": "react-native",
9 | "lib": ["dom", "es5", "es6", "scripthost"],
10 | "target": "es5",
11 | "outDir": "built",
12 | "sourceMap": true,
13 | "skipLibCheck": true
14 | },
15 | "exclude": ["node_modules", "demo"]
16 | }
17 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "strict": true,
5 | "emitDecoratorMetadata": true,
6 | "experimentalDecorators": true,
7 | "jsx": "react-native",
8 | "lib": ["dom", "es5", "es6", "scripthost"],
9 | "target": "es5",
10 | "outDir": "built",
11 | "sourceMap": true
12 | },
13 | "exclude": ["node_modules", "demo"]
14 | }
15 |
--------------------------------------------------------------------------------