diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java index 30d9ba9d..ecbfc1f7 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java @@ -7,6 +7,7 @@ import android.view.ViewParent; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import com.facebook.react.ReactRootView; @@ -109,9 +110,13 @@ public class ScreenContainer extends ViewGroup { return (FragmentActivity) context; } + protected FragmentManager getFragmentManager() { + return findRootFragmentActivity().getSupportFragmentManager(); + } + protected FragmentTransaction getOrCreateTransaction() { if (mCurrentTransaction == null) { - mCurrentTransaction = findRootFragmentActivity().getSupportFragmentManager().beginTransaction(); + mCurrentTransaction = getFragmentManager().beginTransaction(); mCurrentTransaction.setReorderingAllowed(true); } return mCurrentTransaction; diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java index af68298d..41bc5da0 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java @@ -2,6 +2,7 @@ package com.swmansion.rnscreens; import android.content.Context; +import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import java.util.ArrayList; @@ -10,12 +11,26 @@ import java.util.Set; public class ScreenStack extends ScreenContainer { + private static final String BACK_STACK_TAG = "RN_SCREEN_LAST"; + private final ArrayList mStack = new ArrayList<>(); private final Set mDismissed = new HashSet<>(); private ScreenStackFragment mTopScreen = null; private boolean mLayoutEnqueued = false; + private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() { + @Override + public void onBackStackChanged() { + if (getFragmentManager().getBackStackEntryCount() == 0) { + // when back stack entry count hits 0 it means the user's navigated back using hw back + // button. As the "fake" transaction we installed on the back stack does nothing we need + // to handle back navigation on our own. + dismiss(mTopScreen); + } + } + }; + public ScreenStack(Context context) { super(context); } @@ -59,6 +74,12 @@ public class ScreenStack extends ScreenContainer { } } + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getFragmentManager().removeOnBackStackChangedListener(mBackStackListener); + } + private final Runnable mLayoutRunnable = new Runnable() { @Override public void run() { @@ -172,8 +193,51 @@ public class ScreenStack extends ScreenContainer { tryCommitTransaction(); + setupBackHandlerIfNeeded(mTopScreen); + for (ScreenStackFragment screen : mStack) { screen.onStackUpdate(); } } + + /** + * The below method sets up fragment manager's back stack in a way that it'd trigger our back + * stack change listener when hw back button is clicked. + * + * Because back stack by default rolls back the transaction the stack entry is associated with we + * generate a "fake" transaction that hides and shows the top fragment. As a result when back + * stack entry is rolled back nothing happens and we are free to handle back navigation on our + * own in `mBackStackListener`. + * + * We pop that "fake" transaction each time we update stack and we add a new one in case the top + * screen is allowed to be dismised using hw back button. This way in the listener we can tell + * if back button was pressed based on the count of the items on back stack. We expect 0 items + * in case hw back is pressed becakse we try to keep the number of items at 1 by always resetting + * and adding new items. In case we don't add a new item to back stack we remove listener so that + * it does not get triggered. + * + * It is important that we don't install back handler when stack contains a single screen as in + * that case we want the parent navigator or activity handler to take over. + */ + private void setupBackHandlerIfNeeded(ScreenStackFragment topScreen) { + getFragmentManager().removeOnBackStackChangedListener(mBackStackListener); + getFragmentManager().popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE); + ScreenStackFragment firstScreen = null; + for (int i = 0, size = mStack.size(); i < size; i++) { + ScreenStackFragment screen = mStack.get(i); + if (!mDismissed.contains(screen)) { + firstScreen = screen; + break; + } + } + if (topScreen != firstScreen && topScreen.isDismissable()) { + getFragmentManager().addOnBackStackChangedListener(mBackStackListener); + getFragmentManager() + .beginTransaction() + .hide(topScreen) + .show(topScreen) + .addToBackStack(BACK_STACK_TAG) + .commit(); + } + } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java index ea6ab79c..c38906d6 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java @@ -86,4 +86,12 @@ public class ScreenStackFragment extends ScreenFragment { return view; } + + public boolean isDismissable() { + View child = mScreenView.getChildAt(0); + if (child instanceof ScreenStackHeaderConfig) { + return ((ScreenStackHeaderConfig) child).isDismissable(); + } + return true; + } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java index 4a6695c4..0e3fa9fb 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.java @@ -10,7 +10,6 @@ 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; @@ -36,18 +35,6 @@ public class ScreenStackHeaderConfig extends ViewGroup { private boolean mIsAttachedToWindow = false; - private OnBackPressedCallback mBackCallback = new OnBackPressedCallback(false) { - @Override - public void handleOnBackPressed() { - ScreenStack stack = getScreenStack(); - Screen current = getScreen(); - if (stack.getTopScreen() == current) { - stack.dismiss(getScreenFragment()); - } - mBackCallback.remove(); - } - }; - private OnClickListener mBackClickListener = new OnClickListener() { @Override public void onClick(View view) { @@ -116,27 +103,20 @@ public class ScreenStackHeaderConfig extends ViewGroup { return null; } + public boolean isDismissable() { + return mGestureEnabled; + } + public void onUpdate() { Screen parent = (Screen) getParent(); final ScreenStack stack = getScreenStack(); boolean isRoot = stack == null ? true : stack.getRootScreen() == parent; boolean isTop = stack == null ? true : stack.getTopScreen() == parent; - // we need to clean up back handler especially in the case given screen is no longer on top - // because we don't want it to capture back event if it is not responsible for handling it - // as that would block other handlers from running - mBackCallback.remove(); - if (!mIsAttachedToWindow || !isTop) { return; } - if (!isRoot && isTop && mGestureEnabled) { - Fragment fragment = getScreenFragment(); - fragment.requireActivity().getOnBackPressedDispatcher().addCallback(fragment, mBackCallback); - mBackCallback.setEnabled(true); - } - if (mIsHidden) { if (mToolbar.getParent() != null) { getScreenFragment().removeToolbar();