├── .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 | [![npm version](https://img.shields.io/npm/v/react-native-full-image-picker.svg?style=flat)](https://www.npmjs.com/package/react-native-full-image-picker) 4 | [![Build Status](https://travis-ci.org/gaoxiaosong/react-native-full-image-picker.svg?branch=master)](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 | 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 | 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 | }; --------------------------------------------------------------------------------