diff --git a/lib/android/build.gradle b/lib/android/build.gradle index 659845a2..2140dd82 100644 --- a/lib/android/build.gradle +++ b/lib/android/build.gradle @@ -33,12 +33,18 @@ android { } } +repositories { + maven { + url "../../../../react-native/android" + } +} + dependencies { compile 'com.android.support:appcompat-v7:25.2.0' compile 'com.android.support:support-annotations:25.2.0' compile 'com.android.support:design:25.2.0' - compile 'com.fasterxml.jackson.core:jackson-databind:2.8.3' - compile 'com.facebook.fresco:fresco:1.0.1' - compile 'com.facebook.fresco:imagepipeline-okhttp3:1.0.1' + provided 'com.fasterxml.jackson.core:jackson-databind:2.8.3' + compile 'com.facebook.fresco:fresco:1.3.0' + compile 'com.facebook.fresco:imagepipeline-okhttp3:1.3.0' provided 'com.facebook.react:react-native:+' } diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/DefaultNavigationImplementation.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/DefaultNavigationImplementation.java index 60c2912b..1ab7449b 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/DefaultNavigationImplementation.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/DefaultNavigationImplementation.java @@ -143,42 +143,46 @@ private void reconcileStatusBarStyleOnLollipop( ReadableMap next, boolean firstCall ) { - if (firstCall || numberHasChanged("statusBarColor", prev, next)) { + boolean nextHasColor = next.hasKey("statusBarColor"); + + if ((nextHasColor && firstCall) || numberHasChanged("statusBarColor", prev, next)) { boolean animated = false; + if (next.hasKey("statusBarAnimation")) { animated = !("none".equals(next.getString("statusBarAnimation"))); } Integer color = defaults.statusBarColor; - if (next.hasKey("statusBarColor")) { + if (nextHasColor) { color = next.getInt("statusBarColor"); } if (animated) { int curColor = activity.getWindow().getStatusBarColor(); - ValueAnimator colorAnimation = ValueAnimator.ofObject( - new ArgbEvaluator(), curColor, color); - + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), curColor, color); colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { activity.getWindow().setStatusBarColor((Integer) animator.getAnimatedValue()); } }); - colorAnimation - .setDuration(300) - .setStartDelay(0); + + colorAnimation.setDuration(300).setStartDelay(0); colorAnimation.start(); } else { activity.getWindow().setStatusBarColor(color); } } - if (firstCall || boolHasChanged("statusBarTranslucent", prev, next)) { + boolean nextHasTranslucent = next.hasKey("statusBarTranslucent"); + + if ((nextHasTranslucent && firstCall) || boolHasChanged("statusBarTranslucent", prev, next)) { boolean translucent = defaults.statusBarTranslucent; - if (next.hasKey("statusBarTranslucent")) { + + if (nextHasTranslucent) { translucent = next.getBoolean("statusBarTranslucent"); } + View decorView = activity.getWindow().getDecorView(); // If the status bar is translucent hook into the window insets calculations // and consume all the top insets so no padding will be added under the status bar. @@ -374,14 +378,22 @@ public void reconcileNavigationProperties( if (firstCall || boolHasChanged("hidden", prev, next)) { boolean hidden = false; + ViewStub viewStub = component.getViewStub(); + ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) viewStub.getLayoutParams(); if (next.hasKey("hidden")) { hidden = next.getBoolean("hidden"); } if (hidden && bar.isShowing()) { + marginLayoutParams.setMargins(0, 0, 0, 0); bar.hide(); } else if (!hidden && !bar.isShowing()) { + TypedValue value = new TypedValue(); + Resources.Theme currentTheme = viewStub.getContext().getTheme(); + boolean hasValue = currentTheme.resolveAttribute(R.attr.actionBarSize, value, true); + int marginTop = hasValue ? value.data : 0; + marginLayoutParams.setMargins(0, marginTop, 0, 0); bar.show(); } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeNavigationPackage.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeNavigationPackage.java index 6ffdd9da..8ea6b184 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeNavigationPackage.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeNavigationPackage.java @@ -17,7 +17,8 @@ public class NativeNavigationPackage implements ReactPackage { new NavigatorModule(reactContext, ReactNavigationCoordinator.sharedInstance)); } - @Override public List> createJSModules() { + // Deprecated in RN 0.47 + public List> createJSModules() { return Collections.emptyList(); } diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/NavigatorModule.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/NavigatorModule.java index 7738fd7b..c244f751 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/NavigatorModule.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/NavigatorModule.java @@ -5,6 +5,7 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; +import android.util.Log; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; @@ -26,6 +27,7 @@ class NavigatorModule extends ReactContextBaseJavaModule { private static final String CLOSE_BEHAVIOR_DISMISS = "dismiss"; private static final String RESULT_CODE = "resultCode"; + private static final String TAG = NavigatorModule.class.getSimpleName(); private final Handler handler = new Handler(Looper.getMainLooper()); private final ReactNavigationCoordinator coordinator; @@ -97,11 +99,13 @@ public void run() { if (activity == null) { return; } - ensureCoordinatorComponent(activity); - ((ScreenCoordinatorComponent) activity).getScreenCoordinator().pushScreen( - screenName, - ConversionUtil.toBundle(props), - ConversionUtil.toBundle(options)); + + if (isCoordinatorComponent(activity)) { + ((ScreenCoordinatorComponent) activity).getScreenCoordinator().pushScreen( + screenName, + ConversionUtil.toBundle(props), + ConversionUtil.toBundle(options)); + } } }); } @@ -127,12 +131,13 @@ public void run() { if (activity == null) { return; } - ensureCoordinatorComponent(activity); + if (isCoordinatorComponent(activity)) { ((ScreenCoordinatorComponent) activity).getScreenCoordinator().presentScreen( - screenName, - ConversionUtil.toBundle(props), - ConversionUtil.toBundle(options), - promise); + screenName, + ConversionUtil.toBundle(props), + ConversionUtil.toBundle(options), + promise); + } } }); } @@ -158,8 +163,10 @@ public void run() { if (activity == null) { return; } - ensureCoordinatorComponent(activity); - ((ScreenCoordinatorComponent) activity).getScreenCoordinator().dismiss(Activity.RESULT_OK, payloadToMap(payload)); + + if (isCoordinatorComponent(activity)) { + ((ScreenCoordinatorComponent) activity).getScreenCoordinator().dismiss(Activity.RESULT_OK, payloadToMap(payload)); + } } }); } @@ -175,8 +182,10 @@ public void run() { if (activity == null) { return; } - ensureCoordinatorComponent(activity); - ((ScreenCoordinatorComponent) activity).getScreenCoordinator().pop(); + + if (isCoordinatorComponent(activity)) { + ((ScreenCoordinatorComponent) activity).getScreenCoordinator().pop(); + } } }); } @@ -222,9 +231,12 @@ private void dismiss(Activity activity, ReadableMap payload) { activity.finish(); } - private void ensureCoordinatorComponent(Activity activity) { - if (!(activity instanceof ScreenCoordinatorComponent)) { - throw new IllegalStateException("Your activity must implement ScreenCoordinatorComponent."); + private boolean isCoordinatorComponent(Activity activity) { + if (activity instanceof ScreenCoordinatorComponent) { + return true; + } else { + Log.w(TAG, "Activity is not a ScreenCoordinatorComponent."); + return false; } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactAwareActivity.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactAwareActivity.java index 4bfd8db7..5d79daf7 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactAwareActivity.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactAwareActivity.java @@ -28,6 +28,12 @@ protected void onResume() { reactInstanceManager.onHostResume(this, this); } + @Override + protected void onDestroy() { + super.onDestroy(); + reactInstanceManager.onHostDestroy(this); + } + @Override public void invokeDefaultOnBackPressed() { onBackPressed(); diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactInterface.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactInterface.java index 61e2473f..97ad4df0 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactInterface.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactInterface.java @@ -1,6 +1,7 @@ package com.airbnb.android.react.navigation; import android.support.v4.app.FragmentActivity; +import android.view.ViewStub; import com.facebook.react.ReactRootView; import com.facebook.react.bridge.ReadableMap; @@ -12,6 +13,7 @@ public interface ReactInterface { String getInstanceId(); ReactRootView getReactRootView(); ReactToolbar getToolbar(); + ViewStub getViewStub(); boolean isDismissible(); void signalFirstRenderComplete(); void notifySharedElementAddition(); diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNativeFragment.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNativeFragment.java index c0c226b3..545227f6 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNativeFragment.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNativeFragment.java @@ -48,7 +48,6 @@ public class ReactNativeFragment extends Fragment implements ReactInterface, private static final String INSTANCE_ID_PROP = "nativeNavigationInstanceId"; private static final String ON_BUTTON_PRESS = "onButtonPress"; private static final String INITIAL_BAR_HEIGHT_PROP = "nativeNavigationInitialBarHeight"; - private static final int RENDER_TIMEOUT_IN_MS = 1700; // TODO(lmr): put this back down when done debugging // An incrementing ID to identify each ReactNativeActivity instance (used in `instanceId`) private static int UUID = 1; @@ -72,6 +71,7 @@ public void run() { private ReadableMap renderedConfig = ConversionUtil.EMPTY_MAP; private ReactNativeFragmentViewGroup contentContainer; private ReactRootView reactRootView; + private ViewStub viewStub = null; // private ReactInterfaceManager activityManager; private final Handler handler = new Handler(); private PermissionListener permissionListener; @@ -121,6 +121,7 @@ private void initReactNative() { if (reactRootView != null || getView() == null) { return; } + if (!isSuccessfullyInitialized()) { // TODO(lmr): need a different way of doing this // TODO(lmr): move to utils @@ -143,12 +144,7 @@ public void run() { // doing the transition. If this never happens for some reason, we are going to push // anyway in 250ms. The handler should get canceled + called sooner though (it's za race). isWaitingForRenderToFinish = true; - handler.postDelayed(new Runnable() { - @Override public void run() { - Log.d(TAG, "render timeout callback called"); - startPostponedEnterTransition(); - } - }, RENDER_TIMEOUT_IN_MS); + startPostponedEnterTransition(); } // activityManager = new ReactInterfaceManager(this); reactNavigationCoordinator.registerComponent(this, instanceId); @@ -156,9 +152,11 @@ public void run() { private void onAttachWithReactContext() { Log.d(TAG, "onCreateWithReactContext"); + if (getView() == null) { return; } + loadingView.setVisibility(View.GONE); if (!isSuccessfullyInitialized()) { @@ -166,16 +164,22 @@ private void onAttachWithReactContext() { // ReactNativeUtils.showAlertBecauseChecksFailed(getActivity(), null); return; } + String moduleName = getArguments().getString(ReactNativeIntents.EXTRA_MODULE_NAME); Bundle props = getArguments().getBundle(ReactNativeIntents.EXTRA_PROPS); + if (props == null) { props = new Bundle(); } + props.putString(INSTANCE_ID_PROP, instanceId); + if (viewStub == null || reactRootView == null) { + viewStub = (ViewStub) getView().findViewById(R.id.react_root_view_stub); + } + if (reactRootView == null) { - ViewStub reactViewStub = (ViewStub) getView().findViewById(R.id.react_root_view_stub); - reactRootView = (ReactRootView) reactViewStub.inflate(); + reactRootView = (ReactRootView) viewStub.inflate(); } getImplementation().reconcileNavigationProperties( @@ -369,6 +373,11 @@ public ReactRootView getReactRootView() { return reactRootView; } + @Override + public ViewStub getViewStub() { + return viewStub; + } + @Override public ReactToolbar getToolbar() { return toolbar; diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNavigationCoordinator.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNavigationCoordinator.java index 196b9a46..bff3910a 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNavigationCoordinator.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/ReactNavigationCoordinator.java @@ -9,6 +9,7 @@ import android.os.Looper; import android.provider.Settings; import android.widget.Toast; + import com.facebook.react.ReactInstanceManager; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableMap; @@ -178,8 +179,8 @@ public ReadableMap getInitialConfigForModuleName(String screenName) { return getOrDefault(screenName).initialConfig; } - public void start(final Application application) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(application)) { + public void start(final Application application, boolean isDebug) { + if (isDebug && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(application)) { handleOverlayPermissionsMissing(application); return; } diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinator.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinator.java index 1d89d4c7..451932a6 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinator.java +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinator.java @@ -39,7 +39,7 @@ public class ScreenCoordinator { enum PresentAnimation { Modal(R.anim.slide_up, R.anim.delay, R.anim.delay, R.anim.slide_down), - Push(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right), + Push(R.anim.slide_in_right, R.anim.no_animation, R.anim.no_animation, R.anim.slide_out_right), Fade(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out); @AnimRes int enter; @@ -75,6 +75,36 @@ public ScreenCoordinator(AppCompatActivity activity, ScreenCoordinatorLayout con this.container = container; container.setFragmentManager(activity.getSupportFragmentManager()); // TODO: restore state + + restoreStack(); + } + + /** + * If there are Fragments in the Activity stack, restore them into our backStack. We know the + * order in which to restore the Fragments by tagging them with the size of the backStack at the + * time they were added. + * + * This is useful for when the app goes to background on a device with "Don't keep activities" + * turned on. It also restores the backStack in case Android kills the app due to memory pressure. + */ + private void restoreStack() { + boolean hasFragments = true; + int fragmentIndex = 0; + + while (hasFragments) { + Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag(String.valueOf(fragmentIndex)); + if (fragment != null) { + if (getCurrentBackStack() == null) { + BackStack backStack = new BackStack(getNextStackTag(), PresentAnimation.Modal, null); + backStacks.push(backStack); + } + getCurrentBackStack().pushFragment(fragment); + + fragmentIndex++; + } else { + hasFragments = false; + } + } } void onSaveInstanceState(Bundle outState) { @@ -109,9 +139,12 @@ public void pushScreen(Fragment fragment, @Nullable Bundle options) { ft.setCustomAnimations(anim.enter, anim.exit, anim.popEnter, anim.popExit); } BackStack bsi = getCurrentBackStack(); + if (bsi == null) { + return; + } ft .detach(currentFragment) - .add(container.getId(), fragment) + .add(container.getId(), fragment, String.valueOf(bsi.getSize())) .addToBackStack(null) .commit(); bsi.pushFragment(fragment); @@ -187,8 +220,9 @@ public void presentScreen(Fragment fragment, PresentAnimation anim, @Nullable Pr container.willDetachCurrentScreen(); ft.detach(currentFragment); } + ft - .add(container.getId(), fragment) + .add(container.getId(), fragment, String.valueOf(bsi.getSize())) .addToBackStack(bsi.getTag()) .commit(); activity.getSupportFragmentManager().executePendingTransactions(); @@ -209,6 +243,9 @@ public void onBackPressed() { public void pop() { BackStack bsi = getCurrentBackStack(); + if (bsi == null) { + return; + } if (bsi.getSize() == 1) { dismiss(); return; @@ -281,7 +318,10 @@ private Fragment getCurrentFragment() { } private BackStack getCurrentBackStack() { - return backStacks.peek(); + if (backStacks.size() > 0) { + return backStacks.peek(); + } + return null; } @NonNull diff --git a/lib/android/src/main/res/anim/no_animation.xml b/lib/android/src/main/res/anim/no_animation.xml new file mode 100644 index 00000000..0cff8591 --- /dev/null +++ b/lib/android/src/main/res/anim/no_animation.xml @@ -0,0 +1,4 @@ + + diff --git a/package.json b/package.json index 3da3b833..5fb9abd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "native-navigation", - "version": "0.2.2", + "name": "@loggi/native-navigation", + "version": "0.2.14", "description": "Native Navigation for React Native", "main": "index.js", "scripts": {