diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6cb9d4c Binary files /dev/null and b/.DS_Store differ diff --git a/Example/PhotoBrowserExample.js b/Example/PhotoBrowserExample.js index 89a156e..dc612ad 100644 --- a/Example/PhotoBrowserExample.js +++ b/Example/PhotoBrowserExample.js @@ -11,6 +11,7 @@ import { StyleSheet, Navigator, Text, + Image, TouchableOpacity, View, Platform, @@ -24,7 +25,8 @@ const EXAMPLES = [ description: 'with caption, no grid button', enableGrid: false, media: [{ - photo: 'http://farm3.static.flickr.com/2667/4072710001_f36316ddc7_b.jpg', + // photo: 'http://farm3.static.flickr.com/2667/4072710001_f36316ddc7_b.jpg', + photo: 'http://sanantoniotourist.net/wp-content/uploads/2013/07/100_1922.jpg', caption: 'Grotto of the Madonna', }], }, { @@ -33,15 +35,23 @@ const EXAMPLES = [ displayNavArrows: true, displayActionButton: true, media: [{ - photo: 'http://farm3.static.flickr.com/2667/4072710001_f36316ddc7_b.jpg', + // photo: 'http://farm3.static.flickr.com/2667/4072710001_f36316ddc7_b.jpg', + photo: 'http://sanantoniotourist.net/wp-content/uploads/2013/07/100_1922.jpg', selected: true, caption: 'Grotto of the Madonna', }, { photo: require('./media/broadchurch_thumbnail.png'), caption: 'Broadchurch Scene', }, { - photo: 'http://farm3.static.flickr.com/2449/4052876281_6e068ac860_b.jpg', - thumb: 'http://farm3.static.flickr.com/2449/4052876281_6e068ac860_q.jpg', + photo: 'https://a1.dspncdn.com/media/692x/9c/ed/1b/9ced1b427a167ed38b0b66fe3c62f2ae.jpg', + thumb: 'https://a1.dspncdn.com/media/206x/9c/ed/1b/9ced1b427a167ed38b0b66fe3c62f2ae.jpg', + selected: false, + caption: 'rose && fire', + }, { + // photo: 'http://farm3.static.flickr.com/2449/4052876281_6e068ac860_b.jpg', + // thumb: 'http://farm3.static.flickr.com/2449/4052876281_6e068ac860_q.jpg', + photo: 'https://a1.dspncdn.com/media/692x/64/9c/53/649c5331e0f1fb645fa8d25a4ec0e53c.jpg', + thumb: 'https://a1.dspncdn.com/media/206x/64/9c/53/649c5331e0f1fb645fa8d25a4ec0e53c.jpg', selected: false, caption: 'Beautiful Eyes', }], @@ -49,6 +59,7 @@ const EXAMPLES = [ title: 'Library photos', description: 'showing grid first, custom action method', startOnGrid: true, + displaySelectionButtons: true, displayActionButton: true, }, ]; @@ -74,6 +85,7 @@ export default class PhotoBrowserExample extends Component { this._onActionButton = this._onActionButton.bind(this); this._renderRow = this._renderRow.bind(this); this._renderScene = this._renderScene.bind(this); + this._renderTopRightView = this._renderTopRightView.bind(this); const dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2, @@ -105,6 +117,18 @@ export default class PhotoBrowserExample extends Component { this.refs.nav.push(example); } + _renderTopRightView() { + return ( + + + + + ); + } + _renderRow(rowData, sectionID, rowID) { const example = EXAMPLES[rowID]; @@ -152,8 +176,12 @@ export default class PhotoBrowserExample extends Component { startOnGrid={startOnGrid} enableGrid={enableGrid} useCircleProgress + useGallery={true} onSelectionChanged={this._onSelectionChanged} onActionButton={this._onActionButton} + onTopRight={() => console.log('on top right click')} + topRightView={this._renderTopRightView()} + topRightStyle={{overflow: 'hidden'}} /> ); } diff --git a/Example/android/PhotoBrowserExample.iml b/Example/android/PhotoBrowserExample.iml new file mode 100644 index 0000000..eeca891 --- /dev/null +++ b/Example/android/PhotoBrowserExample.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Example/android/app/app.iml b/Example/android/app/app.iml new file mode 100644 index 0000000..5293d7b --- /dev/null +++ b/Example/android/app/app.iml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Example/android/app/build.gradle b/Example/android/app/build.gradle index 06d6f6e..9aef690 100644 --- a/Example/android/app/build.gradle +++ b/Example/android/app/build.gradle @@ -9,7 +9,7 @@ import com.android.build.OutputFile * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the * bundle directly from the development server. Below you can see all the possible configurations * and their defaults. If you decide to add a configuration block, make sure to add it before the - * `apply from: "react.gradle"` line. + * `apply from: "../../node_modules/react-native/react.gradle"` line. * * project.ext.react = [ * // the name of the generated asset file containing your JS bundle @@ -55,11 +55,17 @@ import com.android.build.OutputFile * // date; if you have any other folders that you want to ignore for performance reasons (gradle * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ * // for example, you might want to remove it from here. - * inputExcludes: ["android/**", "ios/**"] + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"] + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] * ] */ -apply from: "react.gradle" +apply from: "../../node_modules/react-native/react.gradle" /** * Set this to true to create two separate APKs instead of one: @@ -75,11 +81,9 @@ def enableSeparateBuildPerCPUArchitecture = false * Run Proguard to shrink the Java bytecode in release builds. */ def enableProguardInReleaseBuilds = false - android { compileSdkVersion 23 buildToolsVersion "23.0.1" - defaultConfig { applicationId "com.photobrowserexample" minSdkVersion 16 @@ -99,9 +103,29 @@ android { } } buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + debuggable true + signingConfig signingConfigs.debug + minifyEnabled true + } + } + signingConfigs { + debug { + // storeFile file("C:\\Users\\Administrator\\.android\\debug.keystore") + storeFile file("/Users/forp/Desktop/__test/debug.keystore") + keyAlias 'androiddebugkey' + keyPassword 'android' + storePassword 'android' + } release { - minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + // storeFile file("E:\\VSCodeWorkSpace\\YuanXinMobileOffice\\key.keystore") + storeFile file("/Users/forp/Desktop/__test/debug.keystore") + keyAlias 'androiddebugkey' + keyPassword 'android' + storePassword 'android' } } // applicationVariants are e.g. debug, release @@ -109,7 +133,7 @@ android { variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits - def versionCodes = ["armeabi-v7a":1, "x86":2] + def versionCodes = ["armeabi-v7a": 1, "x86": 2] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = @@ -117,10 +141,27 @@ android { } } } + productFlavors { + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } } dependencies { - compile fileTree(dir: "libs", include: ["*.jar"]) - compile "com.android.support:appcompat-v7:23.0.1" - compile "com.facebook.react:react-native:+" // From node_modules + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:appcompat-v7:23.0.1' + compile 'com.facebook.react:react-native:+' + // From node_modules +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' } + diff --git a/Example/android/app/proguard-rules.pro b/Example/android/app/proguard-rules.pro index 7d72e46..48361a9 100644 --- a/Example/android/app/proguard-rules.pro +++ b/Example/android/app/proguard-rules.pro @@ -26,11 +26,14 @@ # See http://sourceforge.net/p/proguard/bugs/466/ -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters +-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip # Do not strip any method/class that is annotated with @DoNotStrip -keep @com.facebook.proguard.annotations.DoNotStrip class * +-keep @com.facebook.common.internal.DoNotStrip class * -keepclassmembers class * { @com.facebook.proguard.annotations.DoNotStrip *; + @com.facebook.common.internal.DoNotStrip *; } -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { @@ -51,9 +54,9 @@ -keepattributes Signature -keepattributes *Annotation* --keep class com.squareup.okhttp.** { *; } --keep interface com.squareup.okhttp.** { *; } --dontwarn com.squareup.okhttp.** +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-dontwarn okhttp3.** # okio @@ -61,7 +64,3 @@ -dontwarn java.nio.file.* -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn okio.** - -# stetho - --dontwarn com.facebook.stetho.** diff --git a/Example/android/app/src/main/AndroidManifest.xml b/Example/android/app/src/main/AndroidManifest.xml index b891963..9a79830 100644 --- a/Example/android/app/src/main/AndroidManifest.xml +++ b/Example/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ getPackages() { - return Arrays.asList( - new MainReactPackage() - ); + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onHostResume(this, this); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onHostDestroy(this); + } + } + + // 如果JavaScript端不处理相应的事件,你的invokeDefaultOnBackPressed方法会被调用。默认情况,这会直接结束你的Activity。 + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } + + @Override + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onActivityResult(this, requestCode, resultCode, data); + } } } diff --git a/Example/android/app/src/main/java/com/photobrowserexample/MainApplication.java b/Example/android/app/src/main/java/com/photobrowserexample/MainApplication.java new file mode 100644 index 0000000..c113852 --- /dev/null +++ b/Example/android/app/src/main/java/com/photobrowserexample/MainApplication.java @@ -0,0 +1,52 @@ +package com.photobrowserexample; + +import android.app.Application; +import android.app.Service; +import android.os.Vibrator; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.shell.MainReactPackage; + +import java.util.Arrays; +import java.util.List; + +public class MainApplication extends Application implements ReactApplication { + + public Vibrator mVibrator; + + @Override + public void onCreate() { + super.onCreate(); + } + + private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { + + + /** + * Returns whether dev mode should be enabled. + * This enables e.g. the dev menu. + */ + @Override + protected boolean getUseDeveloperSupport() { + return BuildConfig.DEBUG; + } + + /** + * A list of packages used by the app. If the app uses additional views + * or modules besides the default ones, add more packages here. + */ + @Override + protected List getPackages() { + return Arrays.asList( + new MainReactPackage() + ); + } + }; + + @Override + public ReactNativeHost getReactNativeHost() { + return mReactNativeHost; + } +} diff --git a/Example/android/backup/build.gradle b/Example/android/backup/build.gradle new file mode 100644 index 0000000..7fc5acd --- /dev/null +++ b/Example/android/backup/build.gradle @@ -0,0 +1,142 @@ +apply plugin: "com.android.application" + +import com.android.build.OutputFile + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"] + * ] + */ + +apply from: "react.gradle" + +/** + * Set this to true to create two separate APKs instead of one: + * - An APK that only works on ARM devices + * - An APK that only works on x86 devices + * The advantage is the size of the APK is reduced by about 4MB. + * Upload all the APKs to the Play Store and people will download + * the correct one based on the CPU architecture of their device. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Run Proguard to shrink the Java bytecode in release builds. + */ +def enableProguardInReleaseBuilds = false + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.photobrowserexample" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + signingConfigs { + debug { + // storeFile file("C:\\Users\\Administrator\\.android\\debug.keystore") + storeFile file("/Users/forp/Desktop/__test/debug.keystore") + keyAlias 'androiddebugkey' + keyPassword 'android' + storePassword 'android' + } + release { + // storeFile file("E:\\VSCodeWorkSpace\\YuanXinMobileOffice\\key.keystore") + storeFile file("/Users/forp/Desktop/__test/debug.keystore") + keyAlias 'androiddebugkey' + keyPassword 'android' + storePassword 'android' + } + } + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits + def versionCodes = ["armeabi-v7a":1, "x86":2] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + } + } +} + +dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:23.0.1" + compile "com.facebook.react:react-native:+" // From node_modules +} diff --git a/Example/android/backup/proguard-rules.pro b/Example/android/backup/proguard-rules.pro new file mode 100644 index 0000000..7d72e46 --- /dev/null +++ b/Example/android/backup/proguard-rules.pro @@ -0,0 +1,67 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Disabling obfuscation is useful if you collect stack traces from production crashes +# (unless you are using a system that supports de-obfuscate the stack traces). +-dontobfuscate + +# React Native + +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip +-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters + +# Do not strip any method/class that is annotated with @DoNotStrip +-keep @com.facebook.proguard.annotations.DoNotStrip class * +-keepclassmembers class * { + @com.facebook.proguard.annotations.DoNotStrip *; +} + +-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { + void set*(***); + *** get*(); +} + +-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } +-keep class * extends com.facebook.react.bridge.NativeModule { *; } +-keepclassmembers,includedescriptorclasses class * { native ; } +-keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } + +-dontwarn com.facebook.react.** + +# okhttp + +-keepattributes Signature +-keepattributes *Annotation* +-keep class com.squareup.okhttp.** { *; } +-keep interface com.squareup.okhttp.** { *; } +-dontwarn com.squareup.okhttp.** + +# okio + +-keep class sun.misc.Unsafe { *; } +-dontwarn java.nio.file.* +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn okio.** + +# stetho + +-dontwarn com.facebook.stetho.** diff --git a/Example/android/build.gradle b/Example/android/build.gradle index 403a007..ef90680 100644 --- a/Example/android/build.gradle +++ b/Example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/Example/android/gradle/wrapper/gradle-wrapper.properties b/Example/android/gradle/wrapper/gradle-wrapper.properties index b9fbfab..f89ce5d 100644 --- a/Example/android/gradle/wrapper/gradle-wrapper.properties +++ b/Example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Dec 10 15:43:49 CST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/Example/ios/PhotoBrowserExample/AppDelegate.m b/Example/ios/PhotoBrowserExample/AppDelegate.m index a3fea98..32840e9 100644 --- a/Example/ios/PhotoBrowserExample/AppDelegate.m +++ b/Example/ios/PhotoBrowserExample/AppDelegate.m @@ -9,6 +9,8 @@ #import "AppDelegate.h" +#import "RCTBundleURLProvider.h" + #import "RCTRootView.h" @implementation AppDelegate @@ -42,6 +44,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( */ // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + +#ifdef DEBUG + jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; +#else + jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; +#endif RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"PhotoBrowserExample" diff --git a/Example/ios/PhotoBrowserExample/Info.plist b/Example/ios/PhotoBrowserExample/Info.plist index 91963b2..882b2d0 100644 --- a/Example/ios/PhotoBrowserExample/Info.plist +++ b/Example/ios/PhotoBrowserExample/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion en + NSPhotoLibraryUsageDescription + CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -38,11 +40,10 @@ NSLocationWhenInUseUsageDescription - NSAppTransportSecurity - - - NSAllowsArbitraryLoads - - + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/Example/media/ic_delete.png b/Example/media/ic_delete.png new file mode 100644 index 0000000..609c88f Binary files /dev/null and b/Example/media/ic_delete.png differ diff --git a/Example/package.json b/Example/package.json index 024144e..f6832c8 100644 --- a/Example/package.json +++ b/Example/package.json @@ -6,8 +6,8 @@ "start": "node node_modules/react-native/local-cli/cli.js start" }, "dependencies": { - "react": "^15.2.1", - "react-native": "^0.29.2", - "react-native-photo-browser": "file:../" + "react": "^15.4.0", + "react-native": "^0.39.0", + "react-native-photo-browser": "https://github.com/ksti/react-native-photo-browser.git" } } diff --git a/README.md b/README.md index 01fabbe..bf78522 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ The component has both iOS and Android support. ![](screenshots/screenshot-2.png) ### Installation -```npm install react-native-photo-browser --save``` +```npm install react-native-photo-browser --save``` + +or ```npm install react-native-photo-browser@https://github.com/ksti/react-native-photo-browser.git --save``` ### Properties @@ -41,6 +43,79 @@ const media = { }; ``` +### Usage +```js +_onTopRight = (currentMedia, currentIndex, gallery) => { + console.log('currentMedia:' + currentMedia + 'currentIndex:' + currentIndex); + gallery && gallery.deleteImageRef(currentIndex); + let initialIndex = Math.max(0, currentIndex - 1); + let images = this.state.images; + if (images.length > 1) { + images.splice(currentIndex, 1); // 删掉选中的照片 + // update state + this.setState({ + imageDataSource: this.state.imageDataSource.cloneWithRows(images), + images: images, + configPhotoBrowser: { + ...this.state.configPhotoBrowser, + initialIndex: initialIndex, + media: images.slice(0, images.length - 1), + }, + }) + } else { + this.setState({ + imageDataSource: this.state.imageDataSource.cloneWithRows(images), + images: images, + configPhotoBrowser: { + ...this.state.configPhotoBrowser, + initialIndex: initialIndex, + media: images.slice(0, images.length - 1), + }, + }) + } + + } + + _renderModalPhotoBrowser = () => { + const { + media, + initialIndex, + displayNavArrows, + displayActionButton, + displaySelectionButtons, + startOnGrid, + enableGrid, + } = this.state.configPhotoBrowser; + + return ( + + + this.setState({showPhotoBrowser: false})} + mediaList={media} + initialIndex={initialIndex} + displayNavArrows={displayNavArrows} + displaySelectionButtons={displaySelectionButtons} + displayActionButton={displayActionButton} + startOnGrid={startOnGrid} + enableGrid={enableGrid} + useCircleProgress + onSelectionChanged={this._onSelectionChanged} + onActionButton={this._onActionButton} + onTopRight={this._onTopRight} + topRightView={this._renderTopRightView()} + topRightStyle={{overflow: 'hidden'}} + useGallery={true} + /> + + + ); + } +``` ### Progress Component @@ -70,7 +145,7 @@ Follow those steps to run the example: - [x] Android support - [ ] Improve performance for bigger collections - [ ] Video support -- [ ] Photo zoom +- [x] Photo zoom - [ ] Zooming photos to fill the screen ### Licence diff --git a/lib/FullScreenContainer.js b/lib/FullScreenContainer.js index f9a5c45..6e39be8 100644 --- a/lib/FullScreenContainer.js +++ b/lib/FullScreenContainer.js @@ -15,6 +15,8 @@ import Constants from './constants'; import { BottomBar } from './bar'; import { Photo } from './media'; +import Gallery from './Gallery'; + export default class FullScreenContainer extends React.Component { static propTypes = { @@ -53,6 +55,8 @@ export default class FullScreenContainer extends React.Component { enableGrid: PropTypes.bool, useCircleProgress: PropTypes.bool, onActionButton: PropTypes.func, + forceLoadPhoto: PropTypes.bool, + notSupportedError: PropTypes.string, }; static defaultProps = { @@ -61,10 +65,12 @@ export default class FullScreenContainer extends React.Component { displaySelectionButtons: false, enableGrid: true, onGridButtonTap: () => {}, + notSupportedError: 'sorry, not supported!' }; constructor(props, context) { super(props, context); + this.mediaList = new Array().concat(props.mediaList); this._renderRow = this._renderRow.bind(this); this._toggleControls = this._toggleControls.bind(this); @@ -73,12 +79,15 @@ export default class FullScreenContainer extends React.Component { this._onNextButtonTapped = this._onNextButtonTapped.bind(this); this._onPreviousButtonTapped = this._onPreviousButtonTapped.bind(this); this._onActionButtonTapped = this._onActionButtonTapped.bind(this); + this._onGalleryPageSelected = this._onGalleryPageSelected.bind(this); + this._onSingleTapConfirmed = this._onSingleTapConfirmed.bind(this); this.photoRefs = []; this.state = { currentIndex: props.initialIndex, currentMedia: props.mediaList[props.initialIndex], controlsDisplayed: true, + forceLoadPhoto: props.forceLoadPhoto || false, }; } @@ -91,8 +100,16 @@ export default class FullScreenContainer extends React.Component { this.openPage(this.state.currentIndex, false); } + componentWillReceiveProps(nextProps) { + if (nextProps.mediaList.length !== this.mediaList.length) { + this.mediaList = new Array().concat(nextProps.mediaList); + this._updatePageIndex(nextProps.initialIndex, nextProps.forceLoadPhoto || this.state.forceLoadPhoto); + } + } + openPage(index, animated) { if (!this.scrollView) { + this._updatePageIndex(index); return; } @@ -109,22 +126,27 @@ export default class FullScreenContainer extends React.Component { this._updatePageIndex(index); } - _updatePageIndex(index) { + _updatePageIndex(index, force) { this.setState({ currentIndex: index, currentMedia: this.props.mediaList[index], }, () => { - this._triggerPhotoLoad(index); + this._triggerTopBarStatus(); + this._triggerPhotoLoad(index, force); - const newTitle = `${index + 1} of ${this.props.dataSource.getRowCount()}`; - this.props.updateTitle(newTitle); + this._updateTitle(index); }); } - _triggerPhotoLoad(index) { + _updateTitle(index) { + const newTitle = `${index + 1} of ${this.props.dataSource.getRowCount()}`; + this.props.updateTitle(newTitle); + } + + _triggerPhotoLoad(index, force) { const photo = this.photoRefs[index]; if (photo) { - photo.load(); + photo.load(force); } else { // HACK: photo might be undefined when user taps a photo from gridview // that hasn't been rendered yet. @@ -134,6 +156,17 @@ export default class FullScreenContainer extends React.Component { } } + _triggerTopBarStatus() { + const triggerTopBarStatus = this.props.triggerTopBarStatus; + + // action behaviour must be implemented by the client + // so, call the client method or simply ignore this event + if (triggerTopBarStatus) { + const { currentMedia, currentIndex } = this.state; + triggerTopBarStatus(currentMedia, currentIndex); + } + } + _toggleControls() { const { alwaysShowControls, toggleTopBar } = this.props; @@ -146,6 +179,10 @@ export default class FullScreenContainer extends React.Component { } } + _onSingleTapConfirmed() { + this._toggleControls(); + } + _onNextButtonTapped() { let nextIndex = this.state.currentIndex + 1; // go back to the first item when there is no more next item @@ -178,6 +215,10 @@ export default class FullScreenContainer extends React.Component { _onScroll(e) { const event = e.nativeEvent; const layoutWidth = event.layoutMeasurement.width; + if (layoutWidth === 0) { + return; + }; + const newIndex = Math.floor((event.contentOffset.x + 0.5 * layoutWidth) / layoutWidth); this._onPageSelected(newIndex); @@ -193,7 +234,7 @@ export default class FullScreenContainer extends React.Component { } if (currentIndex !== newIndex) { - this._updatePageIndex(newIndex); + this._updatePageIndex(newIndex, this.state.forceLoadPhoto); if (this.state.controlsDisplayed) { this._toggleControls(); @@ -201,21 +242,40 @@ export default class FullScreenContainer extends React.Component { } } - _renderRow(media: Object, sectionID: number, rowID: number) { + _onGalleryPageSelected(page) { + this._onPageSelected(page); + } + + _renderRow(media: Object, sectionID: number, rowID: number, gallery: Object) { const { displaySelectionButtons, onMediaSelection, useCircleProgress, } = this.props; + const screen = Dimensions.get('window'); + return ( - + this.photoRefs[rowID] = ref} + ref={ref => { + this.photoRefs[rowID] = ref; + if (ref) { + const transformableImage = ref.getTransformableImage(); + if (gallery && gallery.setImageRef) { + gallery.setImageRef(rowID, transformableImage); + }; + }; + }} + width={screen.width} + height={screen.height} lazyLoad useCircleProgress={useCircleProgress} + mimeTypeOrExt={media.mimeTypeOrExt} + notSupportedError={this.props.notSupportedError} uri={media.photo} + transformable={true} displaySelectionButtons={displaySelectionButtons} selected={media.selected} onSelection={(isSelected) => { @@ -228,7 +288,23 @@ export default class FullScreenContainer extends React.Component { } _renderScrollableContent() { - const { dataSource, mediaList } = this.props; + const { dataSource, mediaList, useGallery, initialIndex } = this.props; + if (useGallery) { + return ( + this.gallery = ref} + style={{flex: 1, backgroundColor: 'black'}} + onPageSelected={this._onGalleryPageSelected} + onSingleTapConfirmed={this._onSingleTapConfirmed} + // initialPage={initialIndex} + initialPage={this.state.currentIndex} + images={mediaList} + customItem={(pageData, pageId, layout, gallery) => { + return this._renderRow(pageData, 0, pageId, gallery); + }} + /> + ); + }; if (Platform.OS === 'android') { return ( diff --git a/lib/Gallery.js b/lib/Gallery.js new file mode 100644 index 0000000..db2e91c --- /dev/null +++ b/lib/Gallery.js @@ -0,0 +1,266 @@ +import React, { Component, PropTypes } from 'react'; +import { + View +} from 'react-native'; + +import Image from 'react-native-transformable-image'; +import ViewPager from '@ldn0x7dc/react-native-view-pager'; +import {createResponder} from 'react-native-gesture-responder'; + + +export default class Gallery extends Component { + + static propTypes = { + ...View.propTypes, + images: PropTypes.array, + + initialPage: PropTypes.number, + pageMargin: PropTypes.number, + onPageSelected: PropTypes.func, + onPageScrollStateChanged: PropTypes.func, + onPageScroll: PropTypes.func, + + onSingleTapConfirmed: PropTypes.func, + onGalleryStateChanged: PropTypes.func + }; + + imageRefs = new Map(); + activeResponder = undefined; + firstMove = true; + currentPage = 0; + pageCount = 0; + gestureResponder = undefined; + + constructor(props) { + super(props); + } + + componentWillMount() { + function onResponderReleaseOrTerminate(evt, gestureState) { + if (this.activeResponder) { + if (this.activeResponder === this.viewPagerResponder + && !this.shouldScrollViewPager(evt, gestureState) + && Math.abs(gestureState.vx) > 0.5) { + this.activeResponder.onEnd(evt, gestureState, true); + this.getViewPagerInstance().flingToPage(this.currentPage, gestureState.vx); + } else { + this.activeResponder.onEnd(evt, gestureState); + } + this.activeResponder = null; + } + this.firstMove = true; + this.props.onGalleryStateChanged && this.props.onGalleryStateChanged(true); + } + + this.gestureResponder = createResponder({ + onStartShouldSetResponderCapture: (evt, gestureState) => true, + onStartShouldSetResponder: (evt, gestureState) => { + return true; + }, + onResponderGrant: (evt, gestureState) => { + this.activeImageResponder(evt, gestureState); + }, + onResponderMove: (evt, gestureState) => { + if (this.firstMove) { + this.firstMove = false; + if (this.shouldScrollViewPager(evt, gestureState)) { + this.activeViewPagerResponder(evt, gestureState); + } + this.props.onGalleryStateChanged && this.props.onGalleryStateChanged(false); + } + if (this.activeResponder === this.viewPagerResponder) { + const dx = gestureState.moveX - gestureState.previousMoveX; + const offset = this.getViewPagerInstance().getScrollOffsetFromCurrentPage(); + if (dx > 0 && offset > 0 && !this.shouldScrollViewPager(evt, gestureState)) { + if (dx > offset) { // active image responder + this.getViewPagerInstance().scrollByOffset(offset); + gestureState.moveX -= offset; + this.activeImageResponder(evt, gestureState); + } + } else if (dx < 0 && offset < 0 && !this.shouldScrollViewPager(evt, gestureState)) { + if (dx < offset) { // active image responder + this.getViewPagerInstance().scrollByOffset(offset); + gestureState.moveX -= offset; + this.activeImageResponder(evt, gestureState); + } + } + } + this.activeResponder.onMove(evt, gestureState); + }, + onResponderRelease: onResponderReleaseOrTerminate.bind(this), + onResponderTerminate: onResponderReleaseOrTerminate.bind(this), + onResponderTerminationRequest: (evt, gestureState) => false, //Do not allow parent view to intercept gesture + onResponderSingleTapConfirmed: (evt, gestureState) => { + this.props.onSingleTapConfirmed && this.props.onSingleTapConfirmed(this.currentPage); + } + }); + + this.viewPagerResponder = { + onStart: (evt, gestureState) => { + this.getViewPagerInstance() && this.getViewPagerInstance().onResponderGrant(evt, gestureState); + }, + onMove: (evt, gestureState) => { + this.getViewPagerInstance() && this.getViewPagerInstance().onResponderMove(evt, gestureState); + }, + onEnd: (evt, gestureState, disableSettle) => { + this.getViewPagerInstance() && this.getViewPagerInstance().onResponderRelease(evt, gestureState, disableSettle); + } + } + + this.imageResponder = { + onStart: ((evt, gestureState) => { + this.getCurrentImageTransformer() && this.getCurrentImageTransformer().onResponderGrant(evt, gestureState); + }), + onMove: (evt, gestureState) => { + this.getCurrentImageTransformer() && this.getCurrentImageTransformer().onResponderMove(evt, gestureState); + }, + onEnd: (evt, gestureState) => { + this.getCurrentImageTransformer() && this.getCurrentImageTransformer().onResponderRelease(evt, gestureState); + } + } + } + + shouldScrollViewPager(evt, gestureState) { + if (gestureState.numberActiveTouches > 1) { + return false; + } + const viewTransformer = this.getCurrentImageTransformer(); + if (!viewTransformer) { + return false; + } + const space = viewTransformer.getAvailableTranslateSpace(); + const dx = gestureState.moveX - gestureState.previousMoveX; + + if (dx > 0 && space.left <= 0 && this.currentPage > 0) { + return true; + } + if (dx < 0 && space.right <= 0 && this.currentPage < this.pageCount - 1) { + return true; + } + return false; + } + + activeImageResponder(evt, gestureState) { + if (this.activeResponder !== this.imageResponder) { + if (this.activeResponder === this.viewPagerResponder) { + this.viewPagerResponder.onEnd(evt, gestureState, true); //pass true to disable ViewPager settle + } + this.activeResponder = this.imageResponder; + this.imageResponder.onStart(evt, gestureState); + } + } + + activeViewPagerResponder(evt, gestureState) { + if (this.activeResponder !== this.viewPagerResponder) { + if (this.activeResponder === this.imageResponder) { + this.imageResponder.onEnd(evt, gestureState); + } + this.activeResponder = this.viewPagerResponder; + this.viewPagerResponder.onStart(evt, gestureState) + } + } + + getImageTransformer(page) { + if (page >= 0 && page < this.pageCount) { + let ref = this.imageRefs.get(page + ''); + if (ref) { + return ref.getViewTransformerInstance(); + } + } + } + + getCurrentImageTransformer() { + return this.getImageTransformer(this.currentPage); + } + + getViewPagerInstance() { + return this.refs['galleryViewPager']; + } + + render() { + let gestureResponder = this.gestureResponder; + + let images = this.props.images; + if (!images) { + images = []; + } + this.pageCount = images.length; + + if (this.pageCount <= 0) { + gestureResponder = {}; + } + + return ( + + ); + } + + onPageSelected(page) { + this.currentPage = page; + this.props.onPageSelected && this.props.onPageSelected(page); + } + + onPageScrollStateChanged(state) { + if (state === 'idle') { + this.resetHistoryImageTransform(); + } + this.props.onPageScrollStateChanged && this.props.onPageScrollStateChanged(state); + } + + onPageScroll(e) { + this.props.onPageScroll && this.props.onPageScroll(e); + } + + setImageRef(pageId, ref) { + this.imageRefs.set(pageId, ref); + } + + deleteImageRef(pageId) { + this.imageRefs.delete(`${pageId}`); + } + + renderPage(pageData, pageId, layout) { + const { onViewTransformed, onTransformGestureReleased, customItem, ...other } = this.props; + if (customItem && typeof customItem === 'function') { + return customItem(pageData, pageId, layout, this); + }; + return ( + { + onViewTransformed && onViewTransformed(transform, pageId); + }).bind(this)} + onTransformGestureReleased={((transform) => { + onTransformGestureReleased && onTransformGestureReleased(transform, pageId); + }).bind(this)} + ref={((ref) => { + this.imageRefs.set(pageId, ref); + }).bind(this)} + key={'innerImage#' + pageId} + style={{width: layout.width, height: layout.height}} + source={{uri: pageData}}/> + ); + } + + resetHistoryImageTransform() { + let transformer = this.getImageTransformer(this.currentPage + 1); + if (transformer) { + transformer.forceUpdateTransform({scale: 1, translateX: 0, translateY: 0}); + } + + transformer = this.getImageTransformer(this.currentPage - 1); + if (transformer) { + transformer.forceUpdateTransform({scale: 1, translateX: 0, translateY: 0}); + } + } +} diff --git a/lib/GridContainer.js b/lib/GridContainer.js index f534079..cec2690 100644 --- a/lib/GridContainer.js +++ b/lib/GridContainer.js @@ -21,6 +21,7 @@ export default class GridContainer extends React.Component { displaySelectionButtons: PropTypes.bool, onPhotoTap: PropTypes.func, itemPerRow: PropTypes.number, + notSupportedError: PropTypes.string, /* * refresh the list to apply selection change @@ -62,6 +63,8 @@ export default class GridContainer extends React.Component { thumbnail progressImage={require('../Assets/hourglass.png')} displaySelectionButtons={displaySelectionButtons} + mimeTypeOrExt={media.mimeTypeOrExt} + notSupportedError={this.props.notSupportedError} uri={media.thumb || media.photo} selected={media.selected} onSelection={(isSelected) => { diff --git a/lib/bar/TopBar.js b/lib/bar/TopBar.js index b5e1169..67b4a71 100644 --- a/lib/bar/TopBar.js +++ b/lib/bar/TopBar.js @@ -33,6 +33,9 @@ export default class TopBar extends React.Component { title, height, onBack, + onTopRight, + topRightView, + topRightStyle, } = this.props; return ( @@ -49,6 +52,9 @@ export default class TopBar extends React.Component { } {title} + + {topRightView} + ); } @@ -75,4 +81,10 @@ const styles = StyleSheet.create({ paddingTop: 14, marginLeft: -10, }, + topRightContainer: { + position: 'absolute', + flexDirection: 'row', + right: 0, + top: 16, + }, }); diff --git a/lib/index.js b/lib/index.js index ef1c5f6..6a2f358 100644 --- a/lib/index.js +++ b/lib/index.js @@ -82,6 +82,16 @@ export default class PhotoBrowser extends React.Component { * Sets images amount in grid row, default - 3 (defined in GridContainer) */ itemPerRow: PropTypes.number, + + /* + * Whether to force load photo + */ + forceLoadPhoto: PropTypes.bool, + + /* + * not supported error + */ + notSupportedError: PropTypes.string, }; static defaultProps = { @@ -106,15 +116,17 @@ export default class PhotoBrowser extends React.Component { this._onMediaSelection = this._onMediaSelection.bind(this); this._updateTitle = this._updateTitle.bind(this); this._toggleTopBar = this._toggleTopBar.bind(this); + this._triggerTopBarStatus = this._triggerTopBarStatus.bind(this); + this._onTopRight = this._onTopRight.bind(this); const { mediaList, startOnGrid, initialIndex } = props; - this.state = { dataSource: this._createDataSource(mediaList), mediaList, isFullScreen: !startOnGrid, fullScreenAnim: new Animated.Value(startOnGrid ? 0 : 1), currentIndex: initialIndex, + currentMedia: mediaList[props.initialIndex], displayTopBar: true, }; } @@ -135,7 +147,7 @@ export default class PhotoBrowser extends React.Component { } _onGridPhotoTap(index) { - this.refs.fullScreenContainer.openPage(index, false); + this.fullScreenContainer.openPage(index, false); this._toggleFullScreen(true); } @@ -171,6 +183,12 @@ export default class PhotoBrowser extends React.Component { displayTopBar: displayed, }); } + _triggerTopBarStatus(currentMedia, currentIndex) { + this.setState({ + currentIndex: currentIndex, + currentMedia: currentMedia, + }); + } _toggleFullScreen(display: boolean) { this.setState({ @@ -185,6 +203,18 @@ export default class PhotoBrowser extends React.Component { ).start(); } + _onTopRight() { + const onTopRight = this.props.onTopRight; + const gallery = this.fullScreenContainer && this.fullScreenContainer.gallery; + + // action behaviour must be implemented by the client + // so, call the client method or simply ignore this event + if (onTopRight) { + const { currentMedia, currentIndex, isFullScreen, mediaList } = this.state; + onTopRight(currentMedia, currentIndex, gallery, isFullScreen, mediaList); + } + } + render() { const { alwaysShowControls, @@ -196,6 +226,13 @@ export default class PhotoBrowser extends React.Component { onActionButton, onBack, itemPerRow, + onTopRight, + topRightView, + topRightStyle, + useGallery, + initialIndex, + forceLoadPhoto, + notSupportedError, } = this.props; const { dataSource, @@ -218,7 +255,7 @@ export default class PhotoBrowser extends React.Component { height: screenHeight, marginTop: fullScreenAnim.interpolate({ inputRange: [0, 1], - outputRange: [0, screenHeight * -1 - TOOLBAR_HEIGHT], + outputRange: [0, screenHeight * -1], }), }} > @@ -228,6 +265,7 @@ export default class PhotoBrowser extends React.Component { onPhotoTap={this._onGridPhotoTap} onMediaSelection={this._onMediaSelection} itemPerRow={itemPerRow} + notSupportedError={notSupportedError} /> ); @@ -235,10 +273,11 @@ export default class PhotoBrowser extends React.Component { fullScreenContainer = ( this.fullScreenContainer = ref} dataSource={dataSource} mediaList={mediaList} - initialIndex={currentIndex} + initialIndex={initialIndex} alwaysShowControls={alwaysShowControls} displayNavArrows={displayNavArrows} displaySelectionButtons={displaySelectionButtons} @@ -250,13 +289,17 @@ export default class PhotoBrowser extends React.Component { onGridButtonTap={this._onGridButtonTap} updateTitle={this._updateTitle} toggleTopBar={this._toggleTopBar} + triggerTopBarStatus={this._triggerTopBarStatus} + useGallery={useGallery} + forceLoadPhoto={forceLoadPhoto} + notSupportedError={notSupportedError} /> ); } return ( {gridContainer} {fullScreenContainer} @@ -266,6 +309,9 @@ export default class PhotoBrowser extends React.Component { displayed={displayTopBar} title={isFullScreen ? title : `${mediaList.length} photos`} onBack={onBack} + onTopRight={this._onTopRight} + topRightView={topRightView} + topRightStyle={topRightStyle} /> ); diff --git a/lib/media/Photo.js b/lib/media/Photo.js index 5831e10..d85ce1a 100644 --- a/lib/media/Photo.js +++ b/lib/media/Photo.js @@ -1,15 +1,18 @@ import React, { PropTypes, Component } from 'react'; import { Dimensions, + PanResponder, Image, StyleSheet, View, + Text, TouchableWithoutFeedback, ProgressBarAndroid, Platform, } from 'react-native'; import * as Progress from 'react-native-progress'; +import TransformableImage from 'react-native-transformable-image'; export default class Photo extends Component { @@ -34,6 +37,11 @@ export default class Photo extends Component { */ resizeMode: PropTypes.string, + /* + * if transformable then photo can be zoomed + */ + transformable: PropTypes.bool, + /* * these values are set to image and it's container * screen width and height are used if those are not defined @@ -74,6 +82,16 @@ export default class Photo extends Component { * iOS only */ useCircleProgress: PropTypes.bool, + + /* + * supported mimetype + */ + mimeTypeOrExt: PropTypes.string, + + /* + * not supported error + */ + notSupportedError: PropTypes.string, }; static defaultProps = { @@ -81,6 +99,9 @@ export default class Photo extends Component { thumbnail: false, lazyLoad: false, selected: false, + transformable: false, + mimeTypeOrExt: 'jpg', + notSupportedError: 'sorry, not supported!' }; constructor(props) { @@ -91,6 +112,24 @@ export default class Photo extends Component { this._onLoad = this._onLoad.bind(this); this._toggleSelection = this._toggleSelection.bind(this); + this.supportedMimeType = [ + { + mimeType: 'image/jpeg', + ext: [ + 'jpe', + 'jpeg', + 'jpg', + ], + }, + { + mimeType: 'image/png', + ext: [ + 'png', + 'x-png', + ], + }, + ] + const { lazyLoad, uri } = props; this.state = { @@ -100,14 +139,61 @@ export default class Photo extends Component { }; } - load() { - if (!this.state.uri) { + load(force) { + if (this.transformableImage) { + const viewTransformer = this.transformableImage.getViewTransformerInstance(); + viewTransformer && viewTransformer.setState({ + scale: 1, + translateX: 0, + translateY: 0, + }); + }; + if (force === true) { this.setState({ uri: this.props.uri, }); + } else { + if (!this.state.uri) { + this.setState({ + uri: this.props.uri, + }); + } } } + getTransformableImage() { + return this.transformableImage; + } + + getSupportedMimeType() { + return this.supportedMimeType; + } + + isSupported = (mimeTypeOrExt) => { + if (!mimeTypeOrExt) return true; // default is support + if (typeof mimeTypeOrExt !== 'string') return false; + let lowercaseMimeTypeOrExt = mimeTypeOrExt.toLowerCase(); + let supported = false; + this.supportedMimeType.map((mimeTypeObject, index) => { + if (supported) { + return true; + }; + if (lowercaseMimeTypeOrExt === mimeTypeObject.mimeType) { + supported = true; + return supported; + } else { + let supportedExt = mimeTypeObject.ext; + supportedExt.map((ext, extIndex) => { + if (lowercaseMimeTypeOrExt === ext) { + supported = true; + return supported; + }; + }); + } + }); + return supported; + } + _onProgress(event) { const progress = event.nativeEvent.loaded / event.nativeEvent.total; if (!this.props.thumbnail && progress !== this.state.progress) { @@ -165,11 +251,33 @@ export default class Photo extends Component { return null; } + _renderNotSupportedType() { + let notSupportedErrorText = this.props.notSupportedError || 'sorry, not supported!'; + return ( + + + + {notSupportedErrorText} + + + ); + } + _renderErrorIcon() { return ( - + + + ); } @@ -217,7 +325,7 @@ export default class Photo extends Component { } render() { - const { resizeMode, width, height } = this.props; + const { resizeMode, width, height, transformable } = this.props; const screen = Dimensions.get('window'); const { uri, error } = this.state; @@ -236,18 +344,38 @@ export default class Photo extends Component { height: height || screen.height, }; + let errorOrProgressView = error ? this._renderErrorIcon() : this._renderProgressIndicator(); + if (!this.isSupported(this.props.mimeTypeOrExt)) { + errorOrProgressView = this._renderNotSupportedType(); + }; + return ( - {error ? this._renderErrorIcon() : this._renderProgressIndicator()} - + {errorOrProgressView} + { + transformable ? ( + this.transformableImage = ref} + style={[styles.image, sizeStyle]} + source={source} + onProgress={this._onProgress} + onError={this._onError} + onLoad={this._onLoad} + resizeMode={resizeMode} + /> + ) : ( + + ) + } {this._renderSelectionButton()} ); diff --git a/package.json b/package.json index f145205..6835bd1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "author": "Halil Bilir ", "homepage": "https://github.com/halilb/react-native-photo-browser#readme", "dependencies": { - "react-native-progress": "^3.0.0" + "react-native-progress": "^3.0.0", + "react-native-transformable-image": "0.0.18", + "@ldn0x7dc/react-native-view-pager": "https://github.com/ksti/react-native-view-pager", + "react-native-gesture-responder": "0.1.1" } }