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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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.

### 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"
}
}