Navigation stack native primitives (#139)

Adds support for stack navigation primitives (UINavigationViewController and Android fragment container with back button support)
This commit is contained in:
Krzysztof Magiera
2019-09-05 13:55:14 +02:00
committed by GitHub
parent 4c2aded426
commit 80a466970e
48 changed files with 4303 additions and 985 deletions

View File

@@ -1,12 +1,12 @@
package com.swmansion.rnscreens;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.fragment.app.Fragment;
import android.view.View;
import android.view.ViewParent;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import java.util.HashMap;
import java.util.Map;

View File

@@ -19,7 +19,10 @@ public class RNScreensPackage implements ReactPackage {
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ScreenContainerViewManager(),
new ScreenViewManager()
new ScreenViewManager(),
new ScreenStackViewManager(),
new ScreenStackHeaderConfigViewManager(),
new ScreenStackHeaderSubviewManager()
);
}
}

View File

@@ -4,18 +4,37 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
public class Screen extends ViewGroup implements ReactPointerEventsView {
public enum StackPresentation {
PUSH,
MODAL,
TRANSPARENT_MODAL
}
public enum StackAnimation {
DEFAULT,
NONE,
FADE
}
public static class ScreenFragment extends Fragment {
private Screen mScreenView;
@@ -36,23 +55,39 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
@Nullable Bundle savedInstanceState) {
return mScreenView;
}
@Override
public void onDestroy() {
super.onDestroy();
mScreenView.mEventDispatcher.dispatchEvent(new ScreenDismissedEvent(mScreenView.getId()));
}
}
private final Fragment mFragment;
private final EventDispatcher mEventDispatcher;
private @Nullable ScreenContainer mContainer;
private boolean mActive;
private boolean mTransitioning;
private StackPresentation mStackPresentation = StackPresentation.PUSH;
private StackAnimation mStackAnimation = StackAnimation.DEFAULT;
public Screen(Context context) {
public Screen(ReactContext context) {
super(context);
mFragment = new ScreenFragment(this);
mEventDispatcher = context.getNativeModule(UIManagerModule.class).getEventDispatcher();
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
protected void onLayout(boolean changed, int l, int t, int b, int r) {
// no-op
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
clearDisappearingChildren();
}
/**
* While transitioning this property allows to optimize rendering behavior on Android and provide
* a correct blending options for the animated screen. It is turned on automatically by the container
@@ -66,9 +101,20 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
super.setLayerType(transitioning ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE, null);
}
@Override
public boolean hasOverlappingRendering() {
return mTransitioning;
public void setStackPresentation(StackPresentation stackPresentation) {
mStackPresentation = stackPresentation;
}
public void setStackAnimation(StackAnimation stackAnimation) {
mStackAnimation = stackAnimation;
}
public StackAnimation getStackAnimation() {
return mStackAnimation;
}
public StackPresentation getStackPresentation() {
return mStackPresentation;
}
@Override
@@ -81,12 +127,8 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
// ignore - layer type is controlled by `transitioning` prop
}
public void setNeedsOffscreenAlphaCompositing(boolean needsOffscreenAlphaCompositing) {
// ignore - offscreen alpha is controlled by `transitioning` prop
}
protected void setContainer(@Nullable ScreenContainer mContainer) {
this.mContainer = mContainer;
protected void setContainer(@Nullable ScreenContainer container) {
mContainer = container;
}
protected @Nullable ScreenContainer getContainer() {

View File

@@ -1,15 +1,15 @@
package com.swmansion.rnscreens;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import com.facebook.react.ReactRootView;
import com.facebook.react.modules.core.ChoreographerCompat;
import com.facebook.react.modules.core.ReactChoreographer;
@@ -21,7 +21,7 @@ import java.util.Set;
public class ScreenContainer extends ViewGroup {
private final ArrayList<Screen> mScreens = new ArrayList<>();
protected final ArrayList<Screen> mScreens = new ArrayList<>();
private final Set<Screen> mActiveScreens = new HashSet<>();
private @Nullable FragmentTransaction mCurrentTransaction;
@@ -79,7 +79,7 @@ public class ScreenContainer extends ViewGroup {
return mScreens.get(index);
}
private FragmentActivity findRootFragmentActivity() {
protected final FragmentActivity findRootFragmentActivity() {
ViewParent parent = this;
while (!(parent instanceof ReactRootView) && parent.getParent() != null) {
parent = parent.getParent();
@@ -105,7 +105,7 @@ public class ScreenContainer extends ViewGroup {
return (FragmentActivity) context;
}
private FragmentTransaction getOrCreateTransaction() {
protected FragmentTransaction getOrCreateTransaction() {
if (mCurrentTransaction == null) {
mCurrentTransaction = findRootFragmentActivity().getSupportFragmentManager().beginTransaction();
mCurrentTransaction.setReorderingAllowed(true);
@@ -113,7 +113,7 @@ public class ScreenContainer extends ViewGroup {
return mCurrentTransaction;
}
private void tryCommitTransaction() {
protected void tryCommitTransaction() {
if (mCurrentTransaction != null) {
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
@@ -159,7 +159,10 @@ public class ScreenContainer extends ViewGroup {
return;
}
mNeedUpdate = false;
onUpdate();
}
protected void onUpdate() {
// detach screens that are no longer active
Set<Screen> orphaned = new HashSet<>(mActiveScreens);
for (int i = 0, size = mScreens.size(); i < size; i++) {

View File

@@ -0,0 +1,30 @@
package com.swmansion.rnscreens;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class ScreenDismissedEvent extends Event<ScreenDismissedEvent> {
public static final String EVENT_NAME = "topDismissed";
public ScreenDismissedEvent(int viewId) {
super(viewId);
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public short getCoalescingKey() {
// All events for a given view can be coalesced.
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap());
}
}

View File

@@ -0,0 +1,141 @@
package com.swmansion.rnscreens;
import android.content.Context;
import androidx.fragment.app.FragmentTransaction;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import static com.swmansion.rnscreens.Screen.StackAnimation.*;
public class ScreenStack extends ScreenContainer {
private final ArrayList<Screen> mStack = new ArrayList<>();
private final Set<Screen> mDismissed = new HashSet<>();
private Screen mTopScreen = null;
public ScreenStack(Context context) {
super(context);
}
public void dismiss(Screen screen) {
mDismissed.add(screen);
onUpdate();
}
public Screen getTopScreen() {
for (int i = getScreenCount() - 1; i >= 0; i--) {
Screen screen = getScreenAt(i);
if (!mDismissed.contains(screen)) {
return screen;
}
}
throw new IllegalStateException("Stack is empty");
}
public Screen getRootScreen() {
for (int i = 0, size = getScreenCount(); i < size; i++) {
Screen screen = getScreenAt(i);
if (!mDismissed.contains(screen)) {
return screen;
}
}
throw new IllegalStateException("Stack has no root screen set");
}
@Override
protected void removeScreenAt(int index) {
Screen toBeRemoved = getScreenAt(index);
mDismissed.remove(toBeRemoved);
super.removeScreenAt(index);
}
@Override
protected void onUpdate() {
// remove all screens previously on stack
for (Screen screen : mStack) {
if (!mScreens.contains(screen) || mDismissed.contains(screen)) {
getOrCreateTransaction().remove(screen.getFragment());
}
}
Screen newTop = null;
Screen belowTop = null; // this is only set if newTop has TRANSPARENT_MODAL presentation mode
for (int i = mScreens.size() - 1; i >= 0; i--) {
Screen screen = mScreens.get(i);
if (!mDismissed.contains(screen)) {
if (newTop == null) {
newTop = screen;
if (newTop.getStackPresentation() != Screen.StackPresentation.TRANSPARENT_MODAL) {
break;
}
} else {
belowTop = screen;
break;
}
}
}
for (Screen screen : mScreens) {
// add all new views that weren't on stack before
if (!mStack.contains(screen) && !mDismissed.contains(screen)) {
getOrCreateTransaction().add(getId(), screen.getFragment());
}
// detach all screens that should not be visible
if (screen != newTop && screen != belowTop && !mDismissed.contains(screen)) {
getOrCreateTransaction().hide(screen.getFragment());
}
}
// attach "below top" screen if set
if (belowTop != null) {
final Screen top = newTop;
getOrCreateTransaction().show(belowTop.getFragment()).runOnCommit(new Runnable() {
@Override
public void run() {
top.bringToFront();
}
});
}
getOrCreateTransaction().show(newTop.getFragment());
if (!mStack.contains(newTop)) {
// if new top screen wasn't on stack we do "open animation" so long it is not the very first screen on stack
if (mTopScreen != null) {
// there was some other screen attached before
int transition = FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
switch (mTopScreen.getStackAnimation()) {
case NONE:
transition = FragmentTransaction.TRANSIT_NONE;
break;
case FADE:
transition = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
break;
}
getOrCreateTransaction().setTransition(transition);
}
} else if (mTopScreen != null && !mTopScreen.equals(newTop)) {
// otherwise if we are performing top screen change we do "back animation"
int transition = FragmentTransaction.TRANSIT_FRAGMENT_CLOSE;
switch (mTopScreen.getStackAnimation()) {
case NONE:
transition = FragmentTransaction.TRANSIT_NONE;
break;
case FADE:
transition = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
break;
}
getOrCreateTransaction().setTransition(transition);
}
mTopScreen = newTop;
mStack.clear();
mStack.addAll(mScreens);
tryCommitTransaction();
}
}

View File

@@ -0,0 +1,326 @@
package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.views.text.ReactFontManager;
public class ScreenStackHeaderConfig extends ViewGroup {
private static final float TOOLBAR_ELEVATION = PixelUtil.toPixelFromDIP(4);
private static final class ToolbarWithLayoutLoop extends Toolbar {
private final Runnable mLayoutRunnable = new Runnable() {
@Override
public void run() {
mLayoutEnqueued = false;
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
private boolean mLayoutEnqueued = false;
public ToolbarWithLayoutLoop(Context context) {
super(context);
}
@Override
public void requestLayout() {
super.requestLayout();
if (!mLayoutEnqueued) {
mLayoutEnqueued = true;
post(mLayoutRunnable);
}
}
}
private final ScreenStackHeaderSubview mConfigSubviews[] = new ScreenStackHeaderSubview[3];
private int mSubviewsCount = 0;
private String mTitle;
private int mTitleColor;
private String mTitleFontFamily;
private int mTitleFontSize;
private int mBackgroundColor;
private boolean mIsHidden;
private boolean mIsBackButtonHidden;
private boolean mIsShadowHidden;
private int mTintColor;
private int mWidth;
private int mHeight;
private final Toolbar mToolbar;
private OnBackPressedCallback mBackCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
getScreenStack().dismiss(getScreen());
}
};
private OnClickListener mBackClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
getScreenStack().dismiss(getScreen());
}
};
public ScreenStackHeaderConfig(Context context) {
super(context);
setVisibility(View.GONE);
mToolbar = new ToolbarWithLayoutLoop(context);
// set primary color as background by default
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(android.R.attr.colorPrimary, tv, true)) {
mToolbar.setBackgroundColor(tv.data);
}
mWidth = 0;
mHeight = 0;
if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
mHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
}
}
private void updateToolbarLayout() {
mToolbar.measure(
View.MeasureSpec.makeMeasureSpec(mWidth, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(mHeight, View.MeasureSpec.EXACTLY));
mToolbar.layout(0, 0, mWidth, mHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mWidth != (r - l)) {
mWidth = (r - l);
updateToolbarLayout();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
update();
}
private Screen getScreen() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
return (Screen) screen;
}
return null;
}
private ScreenStack getScreenStack() {
Screen screen = getScreen();
if (screen != null) {
ScreenContainer container = screen.getContainer();
if (container instanceof ScreenStack) {
return (ScreenStack) container;
}
}
return null;
}
private Fragment getScreenFragment() {
ViewParent screen = getParent();
if (screen instanceof Screen) {
return ((Screen) screen).getFragment();
}
return null;
}
private void installBackCallback() {
mBackCallback.remove();
Fragment fragment = getScreenFragment();
fragment.requireActivity().getOnBackPressedDispatcher().addCallback(fragment, mBackCallback);
}
private void update() {
Screen parent = (Screen) getParent();
if (mIsHidden) {
if (mToolbar.getParent() != null) {
parent.removeView(mToolbar);
}
return;
}
if (mToolbar.getParent() == null) {
parent.addView(mToolbar);
}
AppCompatActivity activity = (AppCompatActivity) parent.getFragment().getActivity();
activity.setSupportActionBar(mToolbar);
ActionBar actionBar = activity.getSupportActionBar();
// hide back button
final ScreenStack stack = getScreenStack();
boolean isRoot = stack == null ? true : stack.getRootScreen() == parent;
actionBar.setDisplayHomeAsUpEnabled(isRoot ? false : !mIsBackButtonHidden);
if (!isRoot) {
installBackCallback();
}
mBackCallback.setEnabled(!isRoot);
// when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
// navigation click listener. The default behavior set in the wrapper is to call into
// menu options handlers, but we prefer the back handling logic to stay here instead.
mToolbar.setNavigationOnClickListener(mBackClickListener);
// shadow
actionBar.setElevation(mIsShadowHidden ? 0 : TOOLBAR_ELEVATION);
// title
actionBar.setTitle(mTitle);
TextView titleTextView = getTitleTextView();
if (mTitleColor != 0) {
mToolbar.setTitleTextColor(mTitleColor);
}
if (titleTextView != null) {
if (mTitleFontFamily != null) {
titleTextView.setTypeface(ReactFontManager.getInstance().getTypeface(
mTitleFontFamily, 0, getContext().getAssets()));
}
if (mTitleFontSize > 0) {
titleTextView.setTextSize(mTitleFontSize);
}
}
// background
if (mBackgroundColor != 0) {
mToolbar.setBackgroundColor(mBackgroundColor);
}
// color
if (mTintColor != 0) {
Drawable navigationIcon = mToolbar.getNavigationIcon();
if (navigationIcon != null) {
navigationIcon.setColorFilter(mTintColor, PorterDuff.Mode.SRC_ATOP);
}
}
// subviews
for (int i = 0; i < mSubviewsCount; i++) {
ScreenStackHeaderSubview view = mConfigSubviews[i];
ScreenStackHeaderSubview.Type type = view.getType();
Toolbar.LayoutParams params =
new Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
switch (type) {
case LEFT:
// when there is a left item we need to disable navigation icon
// we also hide title as there is no other way to display left side items
mToolbar.setNavigationIcon(null);
mToolbar.setTitle(null);
params.gravity = Gravity.LEFT;
break;
case RIGHT:
params.gravity = Gravity.RIGHT;
break;
case TITLE:
params.width = LayoutParams.MATCH_PARENT;
mToolbar.setTitle(null);
case CENTER:
params.gravity = Gravity.CENTER_HORIZONTAL;
break;
}
view.setLayoutParams(params);
if (view.getParent() == null) {
mToolbar.addView(view);
}
}
}
public ScreenStackHeaderSubview getConfigSubview(int index) {
return mConfigSubviews[index];
}
public int getConfigSubviewsCount() {
return mSubviewsCount;
}
public void removeConfigSubview(int index) {
if (mConfigSubviews[index] != null) {
mSubviewsCount--;
}
mConfigSubviews[index] = null;
}
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
if (mConfigSubviews[index] == null) {
mSubviewsCount++;
}
mConfigSubviews[index] = child;
}
private TextView getTitleTextView() {
for (int i = 0, size = mToolbar.getChildCount(); i < size; i++) {
View view = mToolbar.getChildAt(i);
if (view instanceof TextView) {
TextView tv = (TextView) view;
if (tv.getText().equals(mToolbar.getTitle())) {
return tv;
}
}
}
return null;
}
public void setTitle(String title) {
mTitle = title;
}
public void setTitleFontFamily(String titleFontFamily) {
mTitleFontFamily = titleFontFamily;
}
public void setTitleFontSize(int titleFontSize) {
mTitleFontSize = titleFontSize;
}
public void setTitleColor(int color) {
mTitleColor = color;
}
public void setTintColor(int color) {
mTintColor = color;
}
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
public void setHideShadow(boolean hideShadow) {
mIsShadowHidden = hideShadow;
}
public void setHideBackButton(boolean hideBackButton) {
mIsBackButtonHidden = hideBackButton;
}
public void setHidden(boolean hidden) {
mIsHidden = hidden;
}
}

View File

@@ -0,0 +1,106 @@
package com.swmansion.rnscreens;
import android.view.View;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
@ReactModule(name = ScreenStackHeaderConfigViewManager.REACT_CLASS)
public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenStackHeaderConfig> {
protected static final String REACT_CLASS = "RNSScreenStackHeaderConfig";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ScreenStackHeaderConfig createViewInstance(ThemedReactContext reactContext) {
return new ScreenStackHeaderConfig(reactContext);
}
@Override
public void addView(ScreenStackHeaderConfig parent, View child, int index) {
if (!(child instanceof ScreenStackHeaderSubview)) {
throw new JSApplicationCausedNativeException("Config children should be of type " + ScreenStackHeaderSubviewManager.REACT_CLASS);
}
parent.addConfigSubview((ScreenStackHeaderSubview) child, index);
}
@Override
public void removeViewAt(ScreenStackHeaderConfig parent, int index) {
parent.removeConfigSubview(index);
}
@Override
public int getChildCount(ScreenStackHeaderConfig parent) {
return parent.getConfigSubviewsCount();
}
@Override
public View getChildAt(ScreenStackHeaderConfig parent, int index) {
return parent.getConfigSubview(index);
}
@Override
public boolean needsCustomLayoutForChildren() {
return true;
}
@ReactProp(name = "title")
public void setTitle(ScreenStackHeaderConfig config, String title) {
config.setTitle(title);
}
@ReactProp(name = "titleFontFamily")
public void setTitleFontFamily(ScreenStackHeaderConfig config, String titleFontFamily) {
config.setTitleFontFamily(titleFontFamily);
}
@ReactProp(name = "titleFontSize")
public void setTitleFontSize(ScreenStackHeaderConfig config, double titleFontSizeSP) {
config.setTitleFontSize((int) PixelUtil.toPixelFromSP(titleFontSizeSP));
}
@ReactProp(name = "titleColor", customType = "Color")
public void setTitleColor(ScreenStackHeaderConfig config, int titleColor) {
config.setTitleColor(titleColor);
}
@ReactProp(name = "backgroundColor", customType = "Color")
public void setBackgroundColor(ScreenStackHeaderConfig config, int titleColor) {
config.setBackgroundColor(titleColor);
}
@ReactProp(name = "hideShadow")
public void setHideShadow(ScreenStackHeaderConfig config, boolean hideShadow) {
config.setHideShadow(hideShadow);
}
@ReactProp(name = "hideBackButton")
public void setHideBackButton(ScreenStackHeaderConfig config, boolean hideBackButton) {
config.setHideBackButton(hideBackButton);
}
@ReactProp(name = "color", customType = "Color")
public void setColor(ScreenStackHeaderConfig config, int color) {
config.setTintColor(color);
}
@ReactProp(name = "hidden")
public void setHidden(ScreenStackHeaderConfig config, boolean hidden) {
config.setHidden(hidden);
}
// RCT_EXPORT_VIEW_PROPERTY(backTitle, NSString)
// RCT_EXPORT_VIEW_PROPERTY(backTitleFontFamily, NSString)
// RCT_EXPORT_VIEW_PROPERTY(backTitleFontSize, NSString)
// // `hidden` is an UIView property, we need to use different name internally
// RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL)
}

View File

@@ -0,0 +1,77 @@
package com.swmansion.rnscreens;
import android.view.View;
import android.view.ViewParent;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.views.view.ReactViewGroup;
public class ScreenStackHeaderSubview extends ReactViewGroup {
public class Measurements {
public int width;
public int height;
}
public enum Type {
LEFT,
CENTER,
TITLE,
RIGHT
}
private int mReactWidth, mReactHeight;
private final UIManagerModule mUIManager;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
// dimensions provided by react
mReactWidth = MeasureSpec.getSize(widthMeasureSpec);
mReactHeight = MeasureSpec.getSize(heightMeasureSpec);
ViewParent parent = getParent();
if (parent != null) {
forceLayout();
((View) parent).requestLayout();
}
}
setMeasuredDimension(mReactWidth, mReactHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed && (mType == Type.CENTER || mType == Type.TITLE)) {
Measurements measurements = new Measurements();
measurements.width = right - left;
if (mType == Type.CENTER) {
// if we want the view to be centered we need to account for the fact that right and left
// paddings may not be equal.
View parent = (View) getParent();
int parentWidth = parent.getWidth();
int rightPadding = parentWidth - right;
int leftPadding = left;
measurements.width = Math.max(0, parentWidth - 2 * Math.max(rightPadding, leftPadding));
}
measurements.height = bottom - top;
mUIManager.setViewLocalData(getId(), measurements);
}
super.onLayout(changed, left, top, right, bottom);
}
private Type mType = Type.RIGHT;
public ScreenStackHeaderSubview(ReactContext context) {
super(context);
mUIManager = context.getNativeModule(UIManagerModule.class);
}
public void setType(Type type) {
mType = type;
}
public Type getType() {
return mType;
}
}

View File

@@ -0,0 +1,52 @@
package com.swmansion.rnscreens;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.views.view.ReactViewManager;
@ReactModule(name = ScreenStackHeaderSubviewManager.REACT_CLASS)
public class ScreenStackHeaderSubviewManager extends ReactViewManager {
private static class SubviewShadowNode extends LayoutShadowNode {
@Override
public void setLocalData(Object data) {
ScreenStackHeaderSubview.Measurements measurements = (ScreenStackHeaderSubview.Measurements) data;
setStyleWidth(measurements.width);
setStyleHeight(measurements.height);
}
}
protected static final String REACT_CLASS = "RNSScreenStackHeaderSubview";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public ReactViewGroup createViewInstance(ThemedReactContext context) {
return new ScreenStackHeaderSubview(context);
}
@Override
public LayoutShadowNode createShadowNodeInstance(ReactApplicationContext context) {
return new SubviewShadowNode();
}
@ReactProp(name = "type")
public void setType(ScreenStackHeaderSubview view, String type) {
if ("left".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.LEFT);
} else if ("center".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.CENTER);
} else if ("title".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.TITLE);
} else if ("right".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.RIGHT);
}
}
}

View File

@@ -0,0 +1,62 @@
package com.swmansion.rnscreens;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
@ReactModule(name = ScreenStackViewManager.REACT_CLASS)
public class ScreenStackViewManager extends ViewGroupManager<ScreenStack> {
protected static final String REACT_CLASS = "RNSScreenStack";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ScreenStack createViewInstance(ThemedReactContext reactContext) {
return new ScreenStack(reactContext);
}
@Override
public void addView(ScreenStack parent, View child, int index) {
if (!(child instanceof Screen)) {
throw new IllegalArgumentException("Attempt attach child that is not of type RNScreen");
}
parent.addScreen((Screen) child, index);
}
@Override
public void removeViewAt(ScreenStack parent, int index) {
prepareOutTransition(parent.getScreenAt(index));
parent.removeScreenAt(index);
}
private void prepareOutTransition(Screen screen) {
startTransitionRecursive(screen);
}
private void startTransitionRecursive(ViewGroup parent) {
for (int i = 0, size = parent.getChildCount(); i < size; i++) {
View child = parent.getChildAt(i);
parent.startViewTransition(child);
if (child instanceof ViewGroup) {
startTransitionRecursive((ViewGroup) child);
}
}
}
@Override
public int getChildCount(ScreenStack parent) {
return parent.getScreenCount();
}
@Override
public View getChildAt(ScreenStack parent, int index) {
return parent.getScreenAt(index);
}
}

View File

@@ -1,10 +1,16 @@
package com.swmansion.rnscreens;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.Map;
import javax.annotation.Nullable;
@ReactModule(name = ScreenViewManager.REACT_CLASS)
public class ScreenViewManager extends ViewGroupManager<Screen> {
@@ -24,4 +30,36 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
public void setActive(Screen view, float active) {
view.setActive(active != 0);
}
@ReactProp(name = "stackPresentation")
public void setStackPresentation(Screen view, String presentation) {
if ("push".equals(presentation)) {
view.setStackPresentation(Screen.StackPresentation.PUSH);
} else if ("modal".equals(presentation)) {
view.setStackPresentation(Screen.StackPresentation.MODAL);
} else if ("transparentModal".equals(presentation)) {
view.setStackPresentation(Screen.StackPresentation.TRANSPARENT_MODAL);
} else {
throw new JSApplicationIllegalArgumentException("Unknown presentation type " + presentation);
}
}
@ReactProp(name = "stackAnimation")
public void setStackAnimation(Screen view, String animation) {
if (animation == null || "default".equals(animation)) {
view.setStackAnimation(Screen.StackAnimation.DEFAULT);
} else if ("none".equals(animation)) {
view.setStackAnimation(Screen.StackAnimation.NONE);
} else if ("fade".equals(animation)) {
view.setStackAnimation(Screen.StackAnimation.FADE);
}
}
@Nullable
@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"));
}
}