diff --git a/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainActivity.java b/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainActivity.java index d0f0365f..dc169d49 100644 --- a/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainActivity.java +++ b/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainActivity.java @@ -3,12 +3,13 @@ import android.os.Bundle; import android.support.annotation.Nullable; -import com.airbnb.android.react.navigation.ScreenCoordinatorLayout; import com.airbnb.android.react.navigation.ReactAwareActivity; import com.airbnb.android.react.navigation.ScreenCoordinator; import com.airbnb.android.react.navigation.ScreenCoordinatorComponent; +import com.airbnb.android.react.navigation.ScreenCoordinatorLayout; public class MainActivity extends ReactAwareActivity implements ScreenCoordinatorComponent { + private static final String TAG = MainActivity.class.getSimpleName(); private ScreenCoordinator screenCoordinator; @@ -19,12 +20,25 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_main); ScreenCoordinatorLayout container = (ScreenCoordinatorLayout) findViewById(R.id.content); screenCoordinator = new ScreenCoordinator(this, container, savedInstanceState); + screenCoordinator.registerScreen("NativeFragment2", Native2Fragment.FACTORY); if (savedInstanceState == null) { screenCoordinator.presentScreen(MainFragment.newInstance()); } } + @Override + protected void onResume() { + super.onResume(); + screenCoordinator.onResume(); + } + + @Override + protected void onPause() { + screenCoordinator.onPause(); + super.onPause(); + } + @Override public ScreenCoordinator getScreenCoordinator() { return screenCoordinator; diff --git a/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainApplication.java b/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainApplication.java index ce7fe5ce..69abec7f 100644 --- a/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainApplication.java +++ b/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/MainApplication.java @@ -1,6 +1,7 @@ package com.airbnb.android.react.navigation.example; import android.app.Application; + import com.airbnb.android.react.navigation.NativeNavigationPackage; import com.airbnb.android.react.navigation.ReactNavigationCoordinator; import com.facebook.react.ReactApplication; @@ -22,7 +23,7 @@ public boolean getUseDeveloperSupport() { @Override protected List getPackages() { - return Arrays.asList( + return Arrays.asList( new MainReactPackage(), new NativeNavigationPackage() ); @@ -48,5 +49,4 @@ public void onCreate() { coordinator.injectReactInstanceManager(mReactNativeHost.getReactInstanceManager()); coordinator.start(this); } - } diff --git a/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/Native2Fragment.java b/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/Native2Fragment.java new file mode 100644 index 00000000..ad28ef68 --- /dev/null +++ b/example/android/app/src/main/java/com/airbnb/android/react/navigation/example/Native2Fragment.java @@ -0,0 +1,46 @@ +package com.airbnb.android.react.navigation.example; + + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.airbnb.android.react.navigation.NativeScreenFactory; + +public class Native2Fragment extends Fragment { + static final NativeScreenFactory FACTORY = new NativeScreenFactory() { + @NonNull + @Override + public Fragment newScreen(@Nullable Bundle props) { + return new Native2Fragment(); + } + }; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_native_2, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar); + ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); + toolbar.setTitle("Native Fragment"); + toolbar.setNavigationIcon(R.drawable.n2_ic_arrow_back_white); + toolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + getActivity().onBackPressed(); + } + }); + } +} diff --git a/example/android/app/src/main/res/layout/fragment_native_2.xml b/example/android/app/src/main/res/layout/fragment_native_2.xml new file mode 100644 index 00000000..5ed1dcba --- /dev/null +++ b/example/android/app/src/main/res/layout/fragment_native_2.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/example/screens/NavigationExampleScreen.js b/example/screens/NavigationExampleScreen.js index 7941d61c..be01fab5 100644 --- a/example/screens/NavigationExampleScreen.js +++ b/example/screens/NavigationExampleScreen.js @@ -56,6 +56,10 @@ export default class NavigationExampleScreen extends Component { title="Navigation bar customisation" onPress={() => Navigator.push('NavigationBar')} /> + Navigator.push('NativeFragment2')} + /> ); } diff --git a/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeScreenFactory.java b/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeScreenFactory.java new file mode 100644 index 00000000..19a7dfd9 --- /dev/null +++ b/lib/android/src/main/java/com/airbnb/android/react/navigation/NativeScreenFactory.java @@ -0,0 +1,14 @@ +package com.airbnb.android.react.navigation; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +public interface NativeScreenFactory { + /** + * Creates a new {@linkplain Fragment native screen} with {@code props}. + */ + @NonNull + Fragment newScreen(@Nullable Bundle props); +} 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 7bcd6a79..290d460c 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 @@ -18,8 +18,8 @@ import java.util.Map; import static com.airbnb.android.react.navigation.ReactNativeIntents.EXTRA_IS_DISMISS; -import static com.airbnb.android.react.navigation.ScreenCoordinator.EXTRA_PAYLOAD; import static com.airbnb.android.react.navigation.ReactNativeUtils.VERSION_CONSTANT_KEY; +import static com.airbnb.android.react.navigation.ScreenCoordinator.EXTRA_PAYLOAD; class NavigatorModule extends ReactContextBaseJavaModule { private static final int VERSION = 2; @@ -113,8 +113,13 @@ public void pushNative(String name, ReadableMap props, ReadableMap options, Prom if (activity == null) { return; } - Intent intent = coordinator.intentForKey(activity.getBaseContext(), name, props); - startActivityWithPromise(activity, intent, promise, options); + + boolean startedFragment = coordinator.startFragmentForKey(name, props, options); + + if (!startedFragment) { + Intent intent = coordinator.intentForKey(activity.getBaseContext(), name, props); + startActivityWithPromise(activity, intent, promise, options); + } } @SuppressWarnings("UnusedParameters") 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..066e9434 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 @@ -8,7 +8,10 @@ import android.os.Handler; import android.os.Looper; import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.widget.Toast; + import com.facebook.react.ReactInstanceManager; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableMap; @@ -49,6 +52,7 @@ public class ReactNavigationCoordinator { private boolean isSuccessfullyInitialized = false; private static final int APP_INITIALIZE_TOAST_DELAY = 3000; + @Nullable ScreenCoordinator screenCoordinator; private ReactNavigationCoordinator() { } @@ -71,6 +75,7 @@ public void onReactContextInitialized(ReactContext context) { } }); } + public void injectImplementation(NavigationImplementation implementation) { if (this.navigationImplementation != null) { // TODO: throw error. can only initialize once. @@ -125,12 +130,17 @@ public void unregisterComponent(String name) { * * @see ReactExposedActivityParams#toIntent(Context, ReadableMap) */ - Intent intentForKey(Context context, String key, ReadableMap arguments) { + @NonNull Intent intentForKey(Context context, String key, ReadableMap arguments) { + if (exposedActivities == null) { + throw new IllegalArgumentException("No Activities registered."); + } + for (ReactExposedActivityParams exposedActivity : exposedActivities) { if (exposedActivity.key().equals(key)) { return exposedActivity.toIntent(context, arguments); } } + throw new IllegalArgumentException( String.format("Tried to push Activity with key '%s', but it could not be found", key)); } @@ -202,4 +212,11 @@ public void run() { } }, APP_INITIALIZE_TOAST_DELAY); } + + boolean startFragmentForKey(String name, ReadableMap props, ReadableMap options) { + if (screenCoordinator == null) { + throw new IllegalStateException("screenCoordinator == null"); + } + return screenCoordinator.pushNativeScreen(name, ConversionUtil.toBundle(props), ConversionUtil.toBundle(options)); + } } 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..afffdaa0 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 @@ -6,6 +6,8 @@ import android.os.Build; import android.os.Bundle; import android.support.annotation.AnimRes; +import android.support.annotation.CallSuper; +import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -24,6 +26,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Stack; @@ -34,9 +37,10 @@ */ public class ScreenCoordinator { private static final String TAG = ScreenCoordinator.class.getSimpleName(); - static final String EXTRA_PAYLOAD = "payload"; private static final String TRANSITION_GROUP = "transitionGroup"; + static final String EXTRA_PAYLOAD = "payload"; + 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), @@ -61,6 +65,7 @@ enum PresentAnimation { private ReactNavigationCoordinator reactNavigationCoordinator = ReactNavigationCoordinator.sharedInstance; private int stackId = 0; + /** * When we dismiss a back stack, the fragment manager would normally execute the latest fragment's * pop exit animation. However, if we present A as a modal, push, B, then dismiss(), the latest @@ -69,6 +74,8 @@ enum PresentAnimation { */ @AnimRes private int nextPopExitAnim; + private Map factories = new LinkedHashMap<>(); + public ScreenCoordinator(AppCompatActivity activity, ScreenCoordinatorLayout container, @Nullable Bundle savedInstanceState) { this.activity = activity; @@ -81,6 +88,38 @@ void onSaveInstanceState(Bundle outState) { // TODO } + @CallSuper + public void onResume() { + reactNavigationCoordinator.screenCoordinator = this; + } + + @CallSuper + public void onPause() { + reactNavigationCoordinator.screenCoordinator = null; + } + + /** + * Register a {@linkplain NativeScreenFactory} for {@code moduleName}. + */ + public void registerScreen(String moduleName, @NonNull NativeScreenFactory nativeScreenFactory) { + factories.put(moduleName, nativeScreenFactory); + } + + /** + * Will try to push a native screen if a {@link NativeScreenFactory factory} is available for {@code moduleName}. + * Will return {@code true} if a screen was pushed, otherwise false. + */ + @CheckResult + boolean pushNativeScreen(String moduleName, @Nullable Bundle props, @Nullable Bundle options) { + NativeScreenFactory nativeScreenFactory = factories.get(moduleName); + if (nativeScreenFactory != null) { + pushScreen(nativeScreenFactory.newScreen(props), options); + return true; + } + + return false; + } + public void pushScreen(String moduleName) { pushScreen(moduleName, null, null); }