Skip to content

Commit

Permalink
[Android] Allow to start a native fragment via Navigator.startNative().
Browse files Browse the repository at this point in the history
* Introduces NativeScreenFactory for android.support.v4.app.Fragment.
* Changes NavigatorModule.pushNative() to check for native screen availability.
* Adds Native2Fragment to example app to show case usage of the new api.
* ReactNavigationCoordinator throws exceptions early if no activity where registered.
  • Loading branch information
serj-lotutovici committed Jul 20, 2017
1 parent 3f91a95 commit a0c3879
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,7 +23,7 @@ public boolean getUseDeveloperSupport() {

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
return Arrays.asList(
new MainReactPackage(),
new NativeNavigationPackage()
);
Expand All @@ -48,5 +49,4 @@ public void onCreate() {
coordinator.injectReactInstanceManager(mReactNativeHost.getReactInstanceManager());
coordinator.start(this);
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
});
}
}
23 changes: 23 additions & 0 deletions example/android/app/src/main/res/layout/fragment_native_2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_primary"
android:minHeight="?attr/actionBarSize"
app:theme="@style/ToolbarTheme" />

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:text="Second Native Fragment" />
</LinearLayout>
4 changes: 4 additions & 0 deletions example/screens/NavigationExampleScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export default class NavigationExampleScreen extends Component {
title="Navigation bar customisation"
onPress={() => Navigator.push('NavigationBar')}
/>
<Row
title="Start Native Screen 2"
onPress={() => Navigator.push('NativeFragment2')}
/>
</Screen>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,7 +79,8 @@ public void signalFirstRenderComplete(String id) {
final ReactInterface component = coordinator.componentFromId(id);
if (component != null) {
handler.post(new Runnable() {
@Override public void run() {
@Override
public void run() {
component.signalFirstRenderComplete();
}
});
Expand Down Expand Up @@ -113,8 +114,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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
}
Expand All @@ -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.
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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),
Expand All @@ -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
Expand All @@ -69,6 +74,8 @@ enum PresentAnimation {
*/
@AnimRes private int nextPopExitAnim;

private Map<String, NativeScreenFactory> factories = new LinkedHashMap<>();

public ScreenCoordinator(AppCompatActivity activity, ScreenCoordinatorLayout container,
@Nullable Bundle savedInstanceState) {
this.activity = activity;
Expand All @@ -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);
}
Expand Down

0 comments on commit a0c3879

Please sign in to comment.