mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-31 09:18:29 +08:00
This change fixes two crashes related to fragment library. The first issue was a crash caused by return transaction started while the previous transaction was ongoing (e.g., quickly adding new screen on top of the stack and immediately dismissing it). The main fix was applied in the fragment library and therefore as a part of this change we update the dependency to fragment:1.2.1 which is the current latest stable version. As a result of the fragment library change we started observing other issue. The second issue was caused by the fact that under certain circumstances the view associated with a fragment couldn't been added despite it still being attached to a parent. This was resulting in a crash. This change adds a cleanup code that properly detaches the view: we do it in onCreateView but also when the fragment destroys its view (in onViewDestroy callback). The latter is necessary because when fragments are restored the order of onCreateView calls is reversed which causes inner views to attach despite their fragment managers not being initialized.
320 lines
9.0 KiB
Java
320 lines
9.0 KiB
Java
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.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.appcompat.app.ActionBar;
|
|
import androidx.appcompat.app.AppCompatActivity;
|
|
import androidx.appcompat.widget.Toolbar;
|
|
import androidx.fragment.app.Fragment;
|
|
|
|
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
|
import com.facebook.react.views.text.ReactFontManager;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
public class ScreenStackHeaderConfig extends ViewGroup {
|
|
|
|
private final ArrayList<ScreenStackHeaderSubview> mConfigSubviews = new ArrayList<>(3);
|
|
private String mTitle;
|
|
private int mTitleColor;
|
|
private String mTitleFontFamily;
|
|
private float mTitleFontSize;
|
|
private int mBackgroundColor;
|
|
private boolean mIsHidden;
|
|
private boolean mIsBackButtonHidden;
|
|
private boolean mIsShadowHidden;
|
|
private boolean mDestroyed;
|
|
private int mTintColor;
|
|
private final Toolbar mToolbar;
|
|
|
|
private boolean mIsAttachedToWindow = false;
|
|
|
|
private OnClickListener mBackClickListener = new OnClickListener() {
|
|
@Override
|
|
public void onClick(View view) {
|
|
ScreenStack stack = getScreenStack();
|
|
ScreenStackFragment fragment = getScreenFragment();
|
|
if (stack.getRootScreen() == fragment.getScreen()) {
|
|
Fragment parentFragment = fragment.getParentFragment();
|
|
if (parentFragment instanceof ScreenStackFragment) {
|
|
((ScreenStackFragment) parentFragment).dismiss();
|
|
}
|
|
} else {
|
|
fragment.dismiss();
|
|
}
|
|
}
|
|
};
|
|
|
|
public ScreenStackHeaderConfig(Context context) {
|
|
super(context);
|
|
setVisibility(View.GONE);
|
|
|
|
mToolbar = new Toolbar(context);
|
|
// reset content insets to be 0 to allow react position custom navbar views. Note that this does
|
|
// not affect platform native back button as toolbar does not apply left inset when navigation
|
|
// button is specified
|
|
mToolbar.setContentInsetsAbsolute(0, 0);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
// no-op
|
|
}
|
|
|
|
public void destroy() {
|
|
mDestroyed = true;
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
mIsAttachedToWindow = true;
|
|
onUpdate();
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
mIsAttachedToWindow = false;
|
|
}
|
|
|
|
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 ScreenStackFragment getScreenFragment() {
|
|
ViewParent screen = getParent();
|
|
if (screen instanceof Screen) {
|
|
Fragment fragment = ((Screen) screen).getFragment();
|
|
if (fragment instanceof ScreenStackFragment) {
|
|
return (ScreenStackFragment) fragment;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void onUpdate() {
|
|
Screen parent = (Screen) getParent();
|
|
final ScreenStack stack = getScreenStack();
|
|
boolean isTop = stack == null ? true : stack.getTopScreen() == parent;
|
|
|
|
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
|
|
return;
|
|
}
|
|
|
|
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
|
|
if (activity == null) {
|
|
return;
|
|
}
|
|
|
|
if (mIsHidden) {
|
|
if (mToolbar.getParent() != null) {
|
|
getScreenFragment().removeToolbar();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mToolbar.getParent() == null) {
|
|
getScreenFragment().setToolbar(mToolbar);
|
|
}
|
|
|
|
activity.setSupportActionBar(mToolbar);
|
|
ActionBar actionBar = activity.getSupportActionBar();
|
|
|
|
// hide back button
|
|
actionBar.setDisplayHomeAsUpEnabled(getScreenFragment().canNavigateBack() ? !mIsBackButtonHidden : false);
|
|
|
|
// 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
|
|
getScreenFragment().setToolbarShadowHidden(mIsShadowHidden);
|
|
|
|
// 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 = mToolbar.getChildCount() - 1; i >= 0; i--) {
|
|
if (mToolbar.getChildAt(i) instanceof ScreenStackHeaderSubview) {
|
|
mToolbar.removeViewAt(i);
|
|
}
|
|
}
|
|
for (int i = 0, size = mConfigSubviews.size(); i < size; i++) {
|
|
ScreenStackHeaderSubview view = mConfigSubviews.get(i);
|
|
ScreenStackHeaderSubview.Type type = view.getType();
|
|
|
|
if (type == ScreenStackHeaderSubview.Type.BACK) {
|
|
// we special case BACK button header config type as we don't add it as a view into toolbar
|
|
// but instead just copy the drawable from imageview that's added as a first child to it.
|
|
View firstChild = view.getChildAt(0);
|
|
if (!(firstChild instanceof ImageView)) {
|
|
throw new JSApplicationIllegalArgumentException("Back button header config view should have Image as first child");
|
|
}
|
|
actionBar.setHomeAsUpIndicator(((ImageView) firstChild).getDrawable());
|
|
continue;
|
|
}
|
|
|
|
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 CENTER:
|
|
params.width = LayoutParams.MATCH_PARENT;
|
|
params.gravity = Gravity.CENTER_HORIZONTAL;
|
|
mToolbar.setTitle(null);
|
|
break;
|
|
}
|
|
|
|
view.setLayoutParams(params);
|
|
mToolbar.addView(view);
|
|
}
|
|
}
|
|
|
|
private void maybeUpdate() {
|
|
if (getParent() != null && !mDestroyed) {
|
|
onUpdate();
|
|
}
|
|
}
|
|
|
|
public ScreenStackHeaderSubview getConfigSubview(int index) {
|
|
return mConfigSubviews.get(index);
|
|
}
|
|
|
|
public int getConfigSubviewsCount() {
|
|
return mConfigSubviews.size();
|
|
}
|
|
|
|
public void removeConfigSubview(int index) {
|
|
mConfigSubviews.remove(index);
|
|
maybeUpdate();
|
|
}
|
|
|
|
public void removeAllConfigSubviews() {
|
|
mConfigSubviews.clear();
|
|
maybeUpdate();
|
|
}
|
|
|
|
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
|
|
mConfigSubviews.add(index, child);
|
|
maybeUpdate();
|
|
}
|
|
|
|
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(float 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;
|
|
}
|
|
}
|