├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── package.json
├── resource
├── album.gif
└── camera.gif
└── src
├── AlbumListView.js
├── AlbumView.js
├── CameraView.js
├── PageKeys.js
├── PhotoModalPage.js
├── PreviewMultiView.js
├── images
├── arrow_right@3x.png
├── check_box@3x.png
├── flash_auto@3x.png
├── flash_close@3x.png
├── flash_open@3x.png
├── shutter@3x.png
├── switch_camera@3x.png
└── video_recording@3x.png
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /resource
2 | .travis.yml
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "10"
4 | os:
5 | - linux
6 |
7 | stages:
8 | - name: deploy
9 |
10 | jobs:
11 | include:
12 | - stage: deploy
13 | deploy:
14 | provider: npm
15 | email: gaoxiaosong06@gmail.com
16 | api_key: "$NPM_TOKEN"
17 | on:
18 | tags: true
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Xiaosong Gao
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 | # react-native-full-image-picker
2 |
3 | [](https://www.npmjs.com/package/react-native-full-image-picker)
4 | [](https://travis-ci.org/gaoxiaosong/react-native-full-image-picker)
5 |
6 | [中文说明](https://www.jianshu.com/p/4f7296753013)
7 |
8 | It is a react native UI component including a camera view and an album selection view. You can take photos, take video recording or select photo from photo library.
9 |
10 | It supports:
11 |
12 | * Take photos by camera.
13 | * Video recording.
14 | * Select photos from photo library.
15 | * Safe area for iPhone X.
16 | * Portrait and Landscape mode.
17 | * Multiple selection or capture mode.
18 | * Preview after capture or video recording.
19 | * Maximum count of photos.
20 |
21 | ## ScreenShots
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Same UI on Android.
32 |
33 | ## Install
34 |
35 | Install by Yarn:
36 |
37 | ```shell
38 | yarn add react-native-full-image-picker
39 | ```
40 |
41 | Install by NPM:
42 |
43 | ```shell
44 | npm install --save react-native-full-image-picker
45 | ```
46 |
47 | **NOTICE**: This library has no native code for iOS and Android. But you should also install native code of these libraries:
48 |
49 | * [CameraRoll](https://facebook.github.io/react-native/docs/cameraroll): Used to get all photos in camera roll or photo library.
50 | * [react-native-camera](https://github.com/react-native-community/react-native-camera): Used to show camera in view.
51 | * [react-native-video](https://github.com/react-native-community/react-native-video): Used to preview the video.
52 | * [react-native-fs](https://github.com/itinance/react-native-fs): Used to copy generated photo or video to a temporary place.
53 |
54 | ## Usage
55 |
56 | First import in the file:
57 |
58 | ```jsx
59 | import * as ImagePicker from 'react-native-full-image-picker';
60 | ```
61 |
62 | It has three method:
63 |
64 | * `ImagePicker.getCamera(options)`: Take photo from camera. (Camera Mode)
65 | * `ImagePicker.getVideo(options)`: Video recording. (Video Mode)
66 | * `ImagePicker.getAlbum(options)`: Select photo or video from photo library. (Photo Mode)
67 |
68 | `options` is a object with these settings:
69 |
70 | * `callback: (data: any[]) => void`: Callback method with photo or video array. `data` is an uri array of photo or video. Do not use `Alert` in this callback method.
71 | * `maxSize?: number`: The maximum number of photo count. Valid in camera or photo library mode.
72 | * `sideType?: RNCamera.Constants.Type`: Side of camera, back or front. Valid in camera or video.
73 | * `pictureOptions?: RNCamera.PictureOptions`: The options of RNCamera.takePictureAsync(PictureOptions)
74 | * `recordingOptions?: RNCamera.RecordingOptions`: The options of RNCamera.recordAsync(RecordingOptions)
75 | * `flashMode?: RNCamera.Constants.FlashMode`: Flash mode. Valid in camera or video.
76 |
77 | You can use [react-native-general-actionsheet](https://github.com/gaoxiaosong/react-native-general-actionsheet) to show `ActionSheet` by same API and UI with `ActionSheetIOS`.
78 |
79 | ## Change Default Property
80 |
81 | You can import page and change `defaultProps` to modify settings globally:
82 |
83 | ```jsx
84 | import * as ImagePicker from 'react-native-full-image-picker';
85 |
86 | ImagePicker.XXX.defaultProps.yyy = ...;
87 | ```
88 |
89 | The `XXX` is the export items of library. Following is the detail.
90 |
91 | ### PhotoModalPage
92 |
93 | This is the outter navigator for all modes. You can change these properties of `defaultProps`:
94 |
95 | | Name | Type | Description |
96 | | :-: | :-: | :- |
97 | | okLabel | string | OK button text |
98 | | cancelLabel | string | Cancel button text |
99 | | deleteLabel | string | Delete button text
100 | | useVideoLabel | string | UseVideo button text |
101 | | usePhotoLabel | string | UsePhoto button text |
102 | | previewLabel | string | Preview button text |
103 | | choosePhotoTitle | string | ChoosePhoto page title |
104 | | maxSizeChooseAlert | (num: number) => string | Max size limit alert message when choosing photos |
105 | | maxSizeTakeAlert | (num: number) => string | Max size limit alert message when taking photos from camera |
106 | | supportedOrientations | string[] | Supported orientations. Default is landscape and portrait |
107 |
108 | ### CameraView
109 |
110 | This is page for taking photos from camera or recording video. You can change these properties of `defaultProps`:
111 |
112 | | Name | Type | Description |
113 | | :-: | :-: | :- |
114 | | maxSize | number | Default max number limit |
115 | | sideType | RNCamera.Constants.Type | Camera side type. Default is `back` |
116 | | flashMode | RNCamera.Constants.FlashMode | Flash mode. Default is `off` |
117 |
118 | ### AlbumListView
119 |
120 | This is page for selecting photo from photo library. You can change these properties of `defaultProps`:
121 |
122 | | Name | Type | Description |
123 | | :-: | :-: | :- |
124 | | maxSize | number | Default max number limit |
125 | | autoConvertPath | boolean | Auto copy photo or not to convert file path to standard file path. Default is `false` |
126 | | assetType | string | Asset type. Please see [CameraRoll Docs](https://facebook.github.io/react-native/docs/cameraroll) |
127 | | groupTypes | string | Group type. Please see [CameraRoll Docs](https://facebook.github.io/react-native/docs/cameraroll) |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-full-image-picker",
3 | "version": "1.2.11",
4 | "private": false,
5 | "description": "Support taking photo, video recording or selecting from photo library.",
6 | "main": "src/index.js",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/gaoxiaosong/react-native-full-image-picker.git"
10 | },
11 | "keywords": [
12 | "react native",
13 | "image picker",
14 | "video record",
15 | "album select"
16 | ],
17 | "author": "Xiaosong Gao",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/gaoxiaosong/react-native-full-image-picker/issues",
21 | "email": "gaoxiaosong06@gmail.com"
22 | },
23 | "homepage": "https://github.com/gaoxiaosong/react-native-full-image-picker#readme",
24 | "dependencies": {
25 | "react-native-camera": "^1.8.0",
26 | "react-native-fs": "^2.13.3",
27 | "react-native-pure-navigation-bar": "^1.4.7",
28 | "react-native-root-siblings": "^3.1.7",
29 | "react-native-video": "^4.3.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/resource/album.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/resource/album.gif
--------------------------------------------------------------------------------
/resource/camera.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/resource/camera.gif
--------------------------------------------------------------------------------
/src/AlbumListView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CameraRoll, Image, FlatList, Platform, StyleSheet, Text, TouchableOpacity, View, Dimensions } from 'react-native';
3 | import NaviBar, { getSafeAreaInset } from 'react-native-pure-navigation-bar';
4 | import PageKeys from './PageKeys';
5 |
6 | export default class extends React.PureComponent {
7 | static defaultProps = {
8 | maxSize: 1,
9 | autoConvertPath: false,
10 | assetType: 'Photos',
11 | groupTypes: 'All',
12 | };
13 |
14 | constructor(props) {
15 | super(props);
16 | this.state = {
17 | data: [],
18 | selectedItems: [],
19 | };
20 | }
21 |
22 | componentDidMount() {
23 | Dimensions.addEventListener('change', this._onWindowChanged);
24 | CameraRoll.getPhotos({
25 | first: 1000000,
26 | groupTypes: Platform.OS === 'ios' ? this.props.groupTypes : undefined,
27 | assetType: this.props.assetType,
28 | }).then((result) => {
29 | const arr = result.edges.map(item => item.node);
30 | const dict = arr.reduce((prv, cur) => {
31 | const curValue = {
32 | type: cur.type,
33 | location: cur.location,
34 | timestamp: cur.timestamp,
35 | ...cur.image,
36 | };
37 | if (!prv[cur.group_name]) {
38 | prv[cur.group_name] = [curValue];
39 | } else {
40 | prv[cur.group_name].push(curValue);
41 | }
42 | return prv;
43 | }, {});
44 | const data = Object.keys(dict)
45 | .sort((a, b) => {
46 | const rootIndex = 'Camera Roll';
47 | if (a === rootIndex) {
48 | return -1;
49 | } else if (b === rootIndex) {
50 | return 1;
51 | } else {
52 | return a < b ? -1 : 1;
53 | }
54 | })
55 | .map(key => ({name: key, value: dict[key]}));
56 | this.setState({data});
57 | });
58 | }
59 |
60 | componentWillUnmount() {
61 | Dimensions.removeEventListener('change', this._onWindowChanged);
62 | }
63 |
64 | render() {
65 | const safeArea = getSafeAreaInset();
66 | const style = {
67 | paddingLeft: safeArea.left,
68 | paddingRight: safeArea.right,
69 | paddingBottom: safeArea.bottom,
70 | };
71 | return (
72 |
73 |
79 | item.name}
84 | extraData={this.state}
85 | />
86 |
87 | );
88 | }
89 |
90 | _renderItem = ({item}) => {
91 | const itemUris = new Set(item.value.map(i => i.uri));
92 | const selectedItems = this.state.selectedItems
93 | .filter(i => itemUris.has(i.uri));
94 | const selectedCount = selectedItems.length;
95 | return (
96 |
97 |
98 |
99 |
104 |
105 | {item.name + ' (' + item.value.length + ')'}
106 |
107 |
108 |
109 | {selectedCount > 0 && (
110 |
111 | {'' + selectedCount}
112 |
113 | )}
114 |
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | _onBackFromAlbum = (items) => {
125 | this.setState({selectedItems: [...items]});
126 | };
127 |
128 | _clickCancel = () => {
129 | this.props.callback && this.props.callback([]);
130 | };
131 |
132 | _clickRow = (item) => {
133 | this.props.navigation.navigate(PageKeys.album_view, {
134 | ...this.props,
135 | groupName: item.name,
136 | photos: item.value,
137 | selectedItems: this.state.selectedItems,
138 | onBack: this._onBackFromAlbum,
139 | });
140 | };
141 |
142 | _onWindowChanged = () => {
143 | this.forceUpdate();
144 | };
145 | }
146 |
147 | const styles = StyleSheet.create({
148 | view: {
149 | flex: 1,
150 | backgroundColor: 'white',
151 | },
152 | safeView: {
153 | flex: 1,
154 | },
155 | listView: {
156 | flex: 1,
157 | },
158 | cell: {
159 | height: 60,
160 | flex: 1,
161 | flexDirection: 'row',
162 | justifyContent: 'space-between',
163 | alignItems: 'center',
164 | paddingLeft: 16,
165 | paddingRight: 16,
166 | borderBottomWidth: StyleSheet.hairlineWidth,
167 | borderBottomColor: '#e6e6ea',
168 | },
169 | left: {
170 | flexDirection: 'row',
171 | alignItems: 'center',
172 | },
173 | image: {
174 | overflow: 'hidden',
175 | width: 44,
176 | height: 44,
177 | },
178 | text: {
179 | fontSize: 16,
180 | color: 'black',
181 | marginLeft: 10,
182 | },
183 | right: {
184 | flexDirection: 'row',
185 | alignItems: 'center',
186 | justifyContent: 'flex-end',
187 | },
188 | selectedcount: {
189 | width: 18,
190 | height: 18,
191 | ...Platform.select({
192 | ios: {lineHeight: 18},
193 | android: {textAlignVertical: 'center'},
194 | }),
195 | fontSize: 11,
196 | textAlign: 'center',
197 | color: 'white',
198 | backgroundColor: '#e15151',
199 | borderRadius: 9,
200 | overflow: 'hidden',
201 | },
202 | arrow: {
203 | width: 13,
204 | height: 16,
205 | marginLeft: 10,
206 | marginRight: 0,
207 | },
208 | });
--------------------------------------------------------------------------------
/src/AlbumView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert, Dimensions, FlatList, Image, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3 | import NaviBar, { getSafeAreaInset } from 'react-native-pure-navigation-bar';
4 | import * as RNFS from 'react-native-fs';
5 | import PageKeys from './PageKeys';
6 |
7 | export default class extends React.PureComponent {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | selectedItems: [...this.props.selectedItems],
12 | };
13 | }
14 |
15 | componentDidMount() {
16 | Dimensions.addEventListener('change', this._onWindowChanged);
17 | }
18 |
19 | componentWillUnmount() {
20 | Dimensions.removeEventListener('change', this._onWindowChanged);
21 | }
22 |
23 | render() {
24 | const safeArea = getSafeAreaInset();
25 | const style = {
26 | paddingLeft: safeArea.left,
27 | paddingRight: safeArea.right,
28 | };
29 | return (
30 |
31 |
37 | item.uri}
43 | numColumns={this._column()}
44 | extraData={this.state}
45 | />
46 | {this._renderBottomView()}
47 |
48 | );
49 | }
50 |
51 | _renderItem = ({item, index}) => {
52 | const safeArea = getSafeAreaInset();
53 | const edge = (Dimensions.get('window').width - safeArea.left - safeArea.right) / this._column() - 2;
54 | const isSelected = this.state.selectedItems.some(obj => obj.uri === item.uri);
55 | const backgroundColor = isSelected ? '#e15151' : 'transparent';
56 | const hasIcon = isSelected || this.state.selectedItems.length < this.props.maxSize;
57 | return (
58 |
59 |
60 |
66 | {hasIcon && (
67 |
68 |
69 | {isSelected && (
70 |
74 | )}
75 |
76 |
77 | )}
78 |
79 |
80 | );
81 | };
82 |
83 | _renderBottomView = () => {
84 | const previewButton = this.state.selectedItems.length > 0 ? this.props.previewLabel : '';
85 | const okButton = this.props.okLabel + ' (' + this.state.selectedItems.length + '/' + this.props.maxSize + ')';
86 | const safeArea = getSafeAreaInset();
87 | return (
88 |
89 |
90 |
91 | {previewButton}
92 |
93 |
94 |
95 |
96 | {okButton}
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | _onFinish = (data) => {
104 | if (this.props.autoConvertPath && Platform.OS === 'ios') {
105 | const promises = data.map((item, index) => {
106 | const {uri} = item;
107 | const params = uri.split('?');
108 | if (params.length < 1) {
109 | throw new Error('Unknown URI:' + uri);
110 | }
111 | const keyValues = params[1].split('&');
112 | if (keyValues.length < 2) {
113 | throw new Error('Unknown URI:' + uri);
114 | }
115 | const kvMaps = keyValues.reduce((prv, cur) => {
116 | const kv = cur.split('=');
117 | prv[kv[0]] = kv[1];
118 | return prv;
119 | }, {});
120 | const itemId = kvMaps.id;
121 | const ext = kvMaps.ext.toLowerCase();
122 | const destPath = RNFS.CachesDirectoryPath + '/' + itemId + '.' + ext;
123 | let promise;
124 | if (item.type === 'ALAssetTypePhoto') {
125 | promise = RNFS.copyAssetsFileIOS(uri, destPath, 0, 0);
126 | } else if (item.type === 'ALAssetTypeVideo') {
127 | promise = RNFS.copyAssetsVideoIOS(uri, destPath);
128 | } else {
129 | throw new Error('Unknown URI:' + uri);
130 | }
131 | return promise
132 | .then((resultUri) => {
133 | data[index].uri = resultUri;
134 | });
135 | });
136 | Promise.all(promises)
137 | .then(() => {
138 | this.props.callback && this.props.callback(data);
139 | });
140 | } else if (this.props.autoConvertPath && Platform.OS === 'android') {
141 | const promises = data.map((item, index) => {
142 | return RNFS.stat(item.uri)
143 | .then((result) => {
144 | data[index].uri = result.originalFilepath;
145 | });
146 | });
147 | Promise.all(promises)
148 | .then(() => {
149 | this.props.callback && this.props.callback(data);
150 | });
151 | } else {
152 | this.props.callback && this.props.callback(data);
153 | }
154 | };
155 |
156 | _onDeletePageFinish = (data) => {
157 | const selectedItems = this.state.selectedItems
158 | .filter(item => data.indexOf(item.uri) >= 0);
159 | this.setState({selectedItems});
160 | };
161 |
162 | _clickBack = () => {
163 | this.props.onBack && this.props.onBack(this.state.selectedItems);
164 | };
165 |
166 | _clickCell = (itemuri) => {
167 | const isSelected = this.state.selectedItems.some(item => item.uri === itemuri.uri);
168 | if (isSelected) {
169 | const selectedItems = this.state.selectedItems.filter(item => item.uri !== itemuri.uri);
170 | this.setState({
171 | selectedItems: [...selectedItems]
172 | });
173 | } else if (this.state.selectedItems.length >= this.props.maxSize) {
174 | Alert.alert('', this.props.maxSizeChooseAlert(this.props.maxSize));
175 | } else {
176 | this.setState({
177 | selectedItems: [...this.state.selectedItems, itemuri]
178 | });
179 | }
180 | };
181 |
182 | _clickPreview = () => {
183 | if (this.state.selectedItems.length > 0) {
184 | this.props.navigation.navigate(PageKeys.preview, {
185 | ...this.props,
186 | images: this.state.selectedItems.map(item => item.uri),
187 | callback: this._onDeletePageFinish,
188 | });
189 | }
190 | };
191 |
192 | _clickOk = () => {
193 | if (this.state.selectedItems.length > 0) {
194 | this._onFinish(this.state.selectedItems);
195 | }
196 | };
197 |
198 | _column = () => {
199 | const {width, height} = Dimensions.get('window');
200 | if (width < height) {
201 | return 3;
202 | } else {
203 | const safeArea = getSafeAreaInset();
204 | const edge = height * 1.0 / 3;
205 | return parseInt((width - safeArea.left - safeArea.right) / edge);
206 | }
207 | };
208 |
209 | _onWindowChanged = () => {
210 | this.forceUpdate();
211 | };
212 | }
213 |
214 | const styles = StyleSheet.create({
215 | view: {
216 | flex: 1,
217 | backgroundColor: 'white',
218 | },
219 | safeView: {
220 | flex: 1,
221 | },
222 | list: {
223 | flex: 1,
224 | },
225 | selectView: {
226 | position: 'absolute',
227 | top: 4,
228 | right: 4,
229 | width: 30,
230 | height: 30,
231 | justifyContent: 'flex-start',
232 | alignItems: 'flex-end',
233 | },
234 | selectIcon: {
235 | marginTop: 2,
236 | marginRight: 2,
237 | width: 20,
238 | height: 20,
239 | borderColor: 'white',
240 | borderWidth: 1,
241 | borderRadius: 10,
242 | overflow: 'hidden',
243 | justifyContent: 'center',
244 | alignItems: 'center',
245 | backgroundColor: 'transparent',
246 | },
247 | selectedIcon: {
248 | width: 13,
249 | height: 13,
250 | },
251 | bottom: {
252 | height: 44,
253 | flexDirection: 'row',
254 | justifyContent: 'space-between',
255 | alignItems: 'center',
256 | borderTopWidth: StyleSheet.hairlineWidth,
257 | borderTopColor: '#e6e6ea',
258 | borderBottomWidth: StyleSheet.hairlineWidth,
259 | borderBottomColor: '#e6e6ea',
260 | },
261 | previewButton: {
262 | marginLeft: 10,
263 | padding: 5,
264 | fontSize: 16,
265 | color: '#666666',
266 | },
267 | okButton: {
268 | marginRight: 15,
269 | paddingHorizontal: 15,
270 | height: 30,
271 | ...Platform.select({
272 | ios: {lineHeight: 30},
273 | android: {textAlignVertical: 'center'}
274 | }),
275 | borderRadius: 6,
276 | overflow: 'hidden',
277 | fontSize: 16,
278 | color: 'white',
279 | backgroundColor: '#e15151',
280 | },
281 | });
--------------------------------------------------------------------------------
/src/CameraView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert, Dimensions, Image, Platform, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
3 | import { RNCamera } from 'react-native-camera';
4 | import { getSafeAreaInset } from 'react-native-pure-navigation-bar';
5 | import Video from 'react-native-video';
6 | import PageKeys from './PageKeys';
7 |
8 | export default class extends React.PureComponent {
9 | static defaultProps = {
10 | maxSize: 1,
11 | sideType: RNCamera.Constants.Type.back,
12 | flashMode: 0,
13 | videoQuality: RNCamera.Constants.VideoQuality["480p"],
14 | pictureOptions: {},
15 | recordingOptions: {},
16 | };
17 |
18 | constructor(props) {
19 | super(props);
20 | this.flashModes = [
21 | RNCamera.Constants.FlashMode.auto,
22 | RNCamera.Constants.FlashMode.off,
23 | RNCamera.Constants.FlashMode.on,
24 | ];
25 | this.state = {
26 | data: [],
27 | isPreview: false,
28 | sideType: this.props.sideType,
29 | flashMode: this.props.flashMode,
30 | isRecording: false,
31 | };
32 | }
33 |
34 | componentDidMount() {
35 | Dimensions.addEventListener('change', this._onWindowChanged);
36 | }
37 |
38 | componentWillUnmount() {
39 | Dimensions.removeEventListener('change', this._onWindowChanged);
40 | }
41 |
42 | render() {
43 | return (
44 |
45 |
46 | {!this.state.isPreview ? this._renderCameraView() : this._renderPreviewView()}
47 | {!this.state.isPreview && this._renderTopView()}
48 | {this._renderBottomView()}
49 |
50 | );
51 | }
52 |
53 | _renderTopView = () => {
54 | const safeArea = getSafeAreaInset();
55 | const style = {
56 | top: safeArea.top,
57 | left: safeArea.left,
58 | right: safeArea.right,
59 | };
60 | const {flashMode} = this.state;
61 | let image;
62 | switch (flashMode) {
63 | case 1:
64 | image = require('./images/flash_close.png');
65 | break;
66 | case 2:
67 | image = require('./images/flash_open.png');
68 | break;
69 | default:
70 | image = require('./images/flash_auto.png');
71 | }
72 | return (
73 |
74 | {!this.props.isVideo && this._renderTopButton(image, this._clickFlashMode)}
75 | {this._renderTopButton(require('./images/switch_camera.png'), this._clickSwitchSide)}
76 |
77 | );
78 | };
79 |
80 | _renderTopButton = (image, onPress) => {
81 | return (
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | _renderCameraView = () => {
89 | return (
90 | this.camera = cam}
92 | type={this.state.sideType}
93 | defaultVideoQuality={this.props.videoQuality}
94 | flashMode={this.flashModes[this.state.flashMode]}
95 | style={styles.camera}
96 | captureAudio={true}
97 | fixOrientation={true}
98 | />
99 | );
100 | };
101 |
102 | _renderPreviewView = () => {
103 | const {width, height} = Dimensions.get('window');
104 | const safeArea = getSafeAreaInset();
105 | const style = {
106 | flex: 1,
107 | marginTop: safeArea.top + topHeight,
108 | marginLeft: safeArea.left,
109 | marginRight: safeArea.right,
110 | marginBottom: safeArea.bottom + bottomHeight,
111 | backgroundColor: 'black',
112 | };
113 | return (
114 |
115 | {this.props.isVideo ? (
116 |
129 | );
130 | };
131 |
132 | _renderBottomView = () => {
133 | const safeArea = getSafeAreaInset();
134 | const style = {
135 | bottom: safeArea.bottom,
136 | left: safeArea.left,
137 | right: safeArea.right,
138 | };
139 | const isMulti = this.props.maxSize > 1;
140 | const hasPhoto = this.state.data.length > 0;
141 | const inPreview = this.state.isPreview;
142 | const isRecording = this.state.isRecording;
143 | const buttonName = this.props.isVideo ? this.props.useVideoLabel : this.props.usePhotoLabel;
144 | return (
145 |
146 | {isMulti && hasPhoto ? this._renderPreviewButton() : !isRecording && this._renderBottomButton(this.props.cancelLabel, this._clickCancel)}
147 | {!inPreview && this._renderTakePhotoButton()}
148 | {isMulti ? hasPhoto && this._renderBottomButton(this.props.okLabel, this._clickOK) : inPreview && this._renderBottomButton(buttonName, this._clickOK)}
149 |
150 | );
151 | };
152 |
153 | _renderPreviewButton = () => {
154 | const text = '' + this.state.data.length + '/' + this.props.maxSize;
155 | return (
156 |
157 |
158 |
162 |
163 | {text}
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | _renderBottomButton = (text, onPress) => {
171 | return (
172 |
173 |
174 | {text}
175 |
176 |
177 | );
178 | };
179 |
180 | _renderTakePhotoButton = () => {
181 | const safeArea = getSafeAreaInset();
182 | const left = (Dimensions.get('window').width - safeArea.left - safeArea.right - 84) / 2;
183 | const icon = this.state.isRecording ?
184 | require('./images/video_recording.png') :
185 | require('./images/shutter.png');
186 | return (
187 |
191 |
192 |
193 | );
194 | };
195 |
196 | _onFinish = (data) => {
197 | this.props.callback && this.props.callback(data);
198 | };
199 |
200 | _onDeletePageFinish = (data) => {
201 | this.setState({
202 | data: [...data],
203 | });
204 | };
205 |
206 | _clickTakePicture = async () => {
207 | if (this.camera) {
208 | const item = await this.camera.takePictureAsync({
209 | mirrorImage: this.state.sideType === RNCamera.Constants.Type.front,
210 | fixOrientation: true,
211 | forceUpOrientation: true,
212 | ...this.props.pictureOptions
213 | });
214 | if (Platform.OS === 'ios') {
215 | if (item.uri.startsWith('file://')) {
216 | item.uri = item.uri.substring(7);
217 | }
218 | }
219 | if (this.props.maxSize > 1) {
220 | if (this.state.data.length >= this.props.maxSize) {
221 | Alert.alert('', this.props.maxSizeTakeAlert(this.props.maxSize));
222 | } else {
223 | this.setState({
224 | data: [...this.state.data, item],
225 | });
226 | }
227 | } else {
228 | this.setState({
229 | data: [item],
230 | isPreview: true,
231 | });
232 | }
233 | }
234 | };
235 |
236 | _clickRecordVideo = () => {
237 | if (this.camera) {
238 | if (this.state.isRecording) {
239 | this.camera.stopRecording();
240 | } else {
241 | this.setState({
242 | isRecording: true,
243 | }, this._startRecording);
244 | }
245 | }
246 | };
247 |
248 | _startRecording = () => {
249 | this.camera.recordAsync(this.props.recordingOptions)
250 | .then((item) => {
251 | if (Platform.OS === 'ios') {
252 | if (item.uri.startsWith('file://')) {
253 | item.uri = item.uri.substring(7);
254 | }
255 | }
256 | this.setState({
257 | data: [item],
258 | isRecording: false,
259 | isPreview: true,
260 | });
261 | });
262 | };
263 |
264 | _clickOK = () => {
265 | this._onFinish(this.state.data);
266 | };
267 |
268 | _clickSwitchSide = () => {
269 | const target = this.state.sideType === RNCamera.Constants.Type.back
270 | ? RNCamera.Constants.Type.front : RNCamera.Constants.Type.back;
271 | this.setState({sideType: target});
272 | };
273 |
274 | _clickFlashMode = () => {
275 | const newMode = (this.state.flashMode + 1) % this.flashModes.length;
276 | this.setState({flashMode: newMode});
277 | };
278 |
279 | _clickPreview = () => {
280 | this.props.navigation.navigate(PageKeys.preview, {
281 | ...this.props,
282 | images: this.state.data,
283 | callback: this._onDeletePageFinish,
284 | });
285 | };
286 |
287 | _clickCancel = () => {
288 | if (this.props.maxSize <= 1 && this.state.isPreview) {
289 | this.setState({
290 | data: [],
291 | isPreview: false,
292 | });
293 | } else {
294 | this._onFinish([]);
295 | }
296 | };
297 |
298 | _onWindowChanged = () => {
299 | this.forceUpdate();
300 | };
301 | }
302 |
303 | const topHeight = 60;
304 | const bottomHeight = 84;
305 |
306 | const styles = StyleSheet.create({
307 | container: {
308 | flex: 1,
309 | backgroundColor: 'black',
310 | },
311 | top: {
312 | position: 'absolute',
313 | height: topHeight,
314 | flexDirection: 'row',
315 | justifyContent: 'space-between',
316 | alignItems: 'center',
317 | backgroundColor: 'transparent',
318 | paddingHorizontal: 5,
319 | },
320 | topImage: {
321 | margin: 10,
322 | width: 27,
323 | height: 27,
324 | },
325 | camera: {
326 | flex: 1,
327 | justifyContent: 'flex-end',
328 | alignItems: 'center'
329 | },
330 | bottom: {
331 | position: 'absolute',
332 | height: 84,
333 | flexDirection: 'row',
334 | justifyContent: 'space-between',
335 | alignItems: 'center',
336 | backgroundColor: 'transparent',
337 | },
338 | takeView: {
339 | position: 'absolute',
340 | top: 0,
341 | bottom: 0,
342 | justifyContent: 'center',
343 | alignItems: 'center',
344 | },
345 | takeImage: {
346 | width: 64,
347 | height: 64,
348 | margin: 10,
349 | },
350 | buttonTouch: {
351 | marginHorizontal: 5,
352 | },
353 | buttonText: {
354 | margin: 10,
355 | height: 44,
356 | lineHeight: 44,
357 | fontSize: 16,
358 | color: 'white',
359 | backgroundColor: 'transparent',
360 | },
361 | previewTouch: {
362 | marginLeft: 15,
363 | },
364 | previewView: {
365 | flexDirection: 'row',
366 | alignItems: 'center',
367 | height: 84,
368 | },
369 | previewImage: {
370 | width: 50,
371 | height: 50,
372 | },
373 | previewText: {
374 | fontSize: 16,
375 | marginLeft: 10,
376 | color: 'white',
377 | backgroundColor: 'transparent',
378 | },
379 | });
--------------------------------------------------------------------------------
/src/PageKeys.js:
--------------------------------------------------------------------------------
1 | export default {
2 | preview: 'PreviewMultiViewPage',
3 | album_view: 'AlbumViewPage',
4 | album_list: 'AlbumListPage',
5 | camera: 'CameraViewPage',
6 | };
--------------------------------------------------------------------------------
/src/PhotoModalPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Modal, BackHandler, InteractionManager, Platform } from 'react-native';
3 | import { createStackNavigator } from 'react-navigation';
4 | import PageKeys from './PageKeys';
5 | import CameraView from './CameraView';
6 | import AlbumListView from './AlbumListView';
7 | import AlbumView from './AlbumView';
8 | import PreviewMultiView from './PreviewMultiView';
9 |
10 | export default class extends React.PureComponent {
11 | static defaultProps = {
12 | okLabel: 'OK',
13 | cancelLabel: 'Cancel',
14 | deleteLabel: 'Delete',
15 | useVideoLabel: 'Use Video',
16 | usePhotoLabel: 'Use Photo',
17 | previewLabel: 'Preview',
18 | choosePhotoTitle: 'Choose Photo',
19 | maxSizeChooseAlert: (number) => 'You can only choose ' + number + ' photos at most',
20 | maxSizeTakeAlert: (number) => 'You can only take ' + number + ' photos at most',
21 | supportedOrientations: ['portrait', 'landscape'],
22 | };
23 |
24 | componentDidMount() {
25 | BackHandler.addEventListener('hardwareBackPress', this._clickBack);
26 | }
27 |
28 | componentWillUnmount() {
29 | BackHandler.removeEventListener('hardwareBackPress', this._clickBack);
30 | }
31 |
32 | render() {
33 | const callback = (data) => {
34 | this.props.callback && this.props.callback(data);
35 | InteractionManager.runAfterInteractions(() => {
36 | this.props.onDestroy && this.props.onDestroy();
37 | });
38 | };
39 | const allscenes = {
40 | [PageKeys.camera]: CameraView,
41 | [PageKeys.album_list]: AlbumListView,
42 | [PageKeys.album_view]: AlbumView,
43 | [PageKeys.preview]: PreviewMultiView,
44 | };
45 | const withUnwrap = (WrappedComponent) => class extends React.PureComponent {
46 | render() {
47 | return (
48 |
52 | );
53 | }
54 | }
55 | const scenes = Object.keys(allscenes)
56 | .reduce((prv, cur) => {
57 | prv[cur] = {
58 | screen: withUnwrap(allscenes[cur]),
59 | navigationOptions: {
60 | gesturesEnabled: false,
61 | }
62 | };
63 | return prv;
64 | }, {});
65 | const NavigationDoor = createStackNavigator(
66 | scenes,
67 | {
68 | initialRouteName: this.props.initialRouteName,
69 | initialRouteParams: {
70 | ...this.props,
71 | callback: callback,
72 | },
73 | headerMode: 'none',
74 | });
75 | return (
76 |
80 |
81 |
82 | );
83 | }
84 |
85 | _clickBack = () => {
86 | this.props.onDestroy && this.props.onDestroy();
87 | return true;
88 | };
89 | }
--------------------------------------------------------------------------------
/src/PreviewMultiView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dimensions, Image, ScrollView, StatusBar, StyleSheet, View } from 'react-native';
3 | import NaviBar, { DEFAULT_NAVBAR_HEIGHT, getSafeAreaInset } from 'react-native-pure-navigation-bar';
4 |
5 | export default class extends React.PureComponent {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | images: this.props.images,
10 | index: 0,
11 | };
12 | }
13 |
14 | componentDidMount() {
15 | Dimensions.addEventListener('change', this._onWindowChanged);
16 | }
17 |
18 | componentWillUnmount() {
19 | Dimensions.removeEventListener('change', this._onWindowChanged);
20 | }
21 |
22 | render() {
23 | const title = '' + (this.state.index + 1) + '/' + this.state.images.length;
24 | const safeArea = getSafeAreaInset();
25 | const style = {
26 | paddingLeft: safeArea.left,
27 | paddingRight: safeArea.right,
28 | paddingBottom: safeArea.bottom,
29 | backgroundColor: 'black',
30 | };
31 | return (
32 |
33 |
34 | this._clickLeft(this.state.images)}
37 | rightElement={this.props.deleteLabel}
38 | onRight={this._clickDelete}
39 | />
40 |
48 | {this.state.images.map(this._renderItem)}
49 |
50 |
51 | );
52 | }
53 |
54 | _renderItem = ({uri: path}, index) => {
55 | const safeArea = getSafeAreaInset();
56 | const {width: screenWidth, height: screenHeight} = Dimensions.get('window');
57 | const width = screenWidth - safeArea.left - safeArea.right;
58 | const height = screenHeight - safeArea.top - safeArea.bottom - DEFAULT_NAVBAR_HEIGHT;
59 | return (
60 |
61 |
66 |
67 | );
68 | };
69 |
70 | _onScroll = ({nativeEvent: {contentOffset: {x}}}) => {
71 | const safeArea = getSafeAreaInset();
72 | const width = Dimensions.get('window').width - safeArea.left - safeArea.right;
73 | const index = Math.floor(x / width);
74 | if (index < 0 || index >= this.state.images.length) {
75 | return;
76 | }
77 | if (index !== this.state.index) {
78 | this.setState({index});
79 | }
80 | };
81 |
82 | _clickLeft = (images) => {
83 | this.props.callback && this.props.callback(images);
84 | };
85 |
86 | _clickDelete = () => {
87 | const newImages = [...this.state.images];
88 | newImages.splice(this.state.index, 1);
89 | if (newImages.length > 0) {
90 | const newIndex = this.state.index >= newImages.length ? newImages.length - 1 : this.state.index;
91 | this.setState({
92 | images: newImages,
93 | index: newIndex,
94 | });
95 | } else {
96 | this._clickLeft([]);
97 | this.props.navigation.goBack();
98 | }
99 | };
100 |
101 | _onWindowChanged = () => {
102 | this.forceUpdate();
103 | };
104 | }
105 |
106 | const styles = StyleSheet.create({
107 | view: {
108 | flex: 1,
109 | },
110 | safeView: {
111 | flex: 1,
112 | backgroundColor: 'black',
113 | },
114 | scrollView: {
115 | flex: 1,
116 | flexDirection: 'row',
117 | },
118 | });
--------------------------------------------------------------------------------
/src/images/arrow_right@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/arrow_right@3x.png
--------------------------------------------------------------------------------
/src/images/check_box@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/check_box@3x.png
--------------------------------------------------------------------------------
/src/images/flash_auto@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/flash_auto@3x.png
--------------------------------------------------------------------------------
/src/images/flash_close@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/flash_close@3x.png
--------------------------------------------------------------------------------
/src/images/flash_open@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/flash_open@3x.png
--------------------------------------------------------------------------------
/src/images/shutter@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/shutter@3x.png
--------------------------------------------------------------------------------
/src/images/switch_camera@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/switch_camera@3x.png
--------------------------------------------------------------------------------
/src/images/video_recording@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gxsshallot/react-native-full-image-picker/63df38debfa63967adf4127b165236e2ad5c487a/src/images/video_recording@3x.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import RootSiblings from 'react-native-root-siblings';
3 | import PageKeys from './PageKeys';
4 | import PhotoModalPage from './PhotoModalPage';
5 | import CameraView from './CameraView';
6 | import AlbumListView from './AlbumListView';
7 | import AlbumView from './AlbumView';
8 | import PreviewMultiView from './PreviewMultiView';
9 |
10 | /**
11 | * --OPTIONS--
12 | * maxSize?: number. Camera or Video.
13 | * sideType?: RNCamera.Constants.Type. Camera or Video.
14 | * flashMode?: RNCamera.Constants.FlashMode. Camera or Video.
15 | * pictureOptions?: RNCamera.PictureOptions. Camera.
16 | * recordingOptions?: RNCamera.RecordingOptions Video.
17 | * callback: (data: any[]) => void. Donot use Alert.
18 | */
19 |
20 | export const getCamera = (options) => showImagePicker(PageKeys.camera, {...options, isVideo: false});
21 | export const getVideo = (options) => showImagePicker(PageKeys.camera, {...options, isVideo: true});
22 | export const getAlbum = (options) => showImagePicker(PageKeys.album_list, options);
23 |
24 | let sibling = null;
25 |
26 | function showImagePicker(initialRouteName, options) {
27 | if (sibling) {
28 | return null;
29 | }
30 | sibling = new RootSiblings(
31 | {
34 | sibling && sibling.destroy();
35 | sibling = null;
36 | }}
37 | {...options}
38 | />
39 | );
40 | }
41 |
42 | export {
43 | PhotoModalPage,
44 | CameraView,
45 | PreviewMultiView,
46 | AlbumListView,
47 | AlbumView,
48 | };
--------------------------------------------------------------------------------