mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-02-02 09:18:38 +08:00
This change adds new event that gets dispatched from native stack when screen transitioning is finished. The new event is used by react-navigation to update the library internal state that the triggered action has been finished. Previously we were relying solely on onAppear and onDisappear events, however those does not get triggered when new iOS 13 modals are used or when transparent modals are displayed. This is because with transparent modals the view below is still visible. We therefore needed another way of notifying the library that screen transition have finished despite the fact that disappear event couldn't be triggered. As a part of this change I also refactored invalid ref cycle-break code on iOS which was ought to remove reference cycle between view and view controller. This code have been moved to viewWillDisappear callback. Also on Android part small refactoring has been done and we removed the necessity to keep mActiveScreens array which was occasionally getting out of sync with the list of active fragments.
287 lines
10 KiB
Java
287 lines
10 KiB
Java
package com.swmansion.rnscreens;
|
|
|
|
import android.content.Context;
|
|
import android.view.View;
|
|
|
|
import androidx.fragment.app.Fragment;
|
|
import androidx.fragment.app.FragmentManager;
|
|
import androidx.fragment.app.FragmentTransaction;
|
|
|
|
import com.facebook.react.bridge.ReactContext;
|
|
import com.facebook.react.uimanager.UIManagerModule;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
|
|
|
|
private static final String BACK_STACK_TAG = "RN_SCREEN_LAST";
|
|
|
|
private final ArrayList<ScreenStackFragment> mStack = new ArrayList<>();
|
|
private final Set<ScreenStackFragment> mDismissed = new HashSet<>();
|
|
|
|
private ScreenStackFragment mTopScreen = null;
|
|
private boolean mRemovalTransitionStarted = false;
|
|
|
|
private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
|
|
@Override
|
|
public void onBackStackChanged() {
|
|
if (mFragmentManager.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);
|
|
}
|
|
}
|
|
};
|
|
|
|
private final FragmentManager.FragmentLifecycleCallbacks mLifecycleCallbacks = new FragmentManager.FragmentLifecycleCallbacks() {
|
|
@Override
|
|
public void onFragmentResumed(FragmentManager fm, Fragment f) {
|
|
if (mTopScreen == f) {
|
|
setupBackHandlerIfNeeded(mTopScreen);
|
|
}
|
|
}
|
|
};
|
|
|
|
public ScreenStack(Context context) {
|
|
super(context);
|
|
}
|
|
|
|
public void dismiss(ScreenStackFragment screenFragment) {
|
|
mDismissed.add(screenFragment);
|
|
onUpdate();
|
|
}
|
|
|
|
public Screen getTopScreen() {
|
|
return mTopScreen != null ? mTopScreen.getScreen() : null;
|
|
}
|
|
|
|
public Screen getRootScreen() {
|
|
for (int i = 0, size = getScreenCount(); i < size; i++) {
|
|
Screen screen = getScreenAt(i);
|
|
if (!mDismissed.contains(screen.getFragment())) {
|
|
return screen;
|
|
}
|
|
}
|
|
throw new IllegalStateException("Stack has no root screen set");
|
|
}
|
|
|
|
@Override
|
|
protected ScreenStackFragment adapt(Screen screen) {
|
|
return new ScreenStackFragment(screen);
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
if (mFragmentManager != null) {
|
|
mFragmentManager.removeOnBackStackChangedListener(mBackStackListener);
|
|
mFragmentManager.unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
|
|
if (!mFragmentManager.isStateSaved()) {
|
|
// state save means that the container where fragment manager was installed has been unmounted.
|
|
// This could happen as a result of dismissing nested stack. In such a case we don't need to
|
|
// reset back stack as it'd result in a crash caused by the fact the fragment manager is no
|
|
// longer attached.
|
|
mFragmentManager.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
|
}
|
|
}
|
|
super.onDetachedFromWindow();
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
|
|
}
|
|
|
|
@Override
|
|
public void startViewTransition(View view) {
|
|
super.startViewTransition(view);
|
|
mRemovalTransitionStarted = true;
|
|
}
|
|
|
|
@Override
|
|
public void endViewTransition(View view) {
|
|
super.endViewTransition(view);
|
|
if (mRemovalTransitionStarted) {
|
|
mRemovalTransitionStarted = false;
|
|
dispatchOnFinishTransitioning();
|
|
}
|
|
}
|
|
|
|
public void onViewAppearTransitionEnd() {
|
|
if (!mRemovalTransitionStarted) {
|
|
dispatchOnFinishTransitioning();
|
|
}
|
|
}
|
|
|
|
private void dispatchOnFinishTransitioning() {
|
|
((ReactContext) getContext())
|
|
.getNativeModule(UIManagerModule.class)
|
|
.getEventDispatcher()
|
|
.dispatchEvent(new StackFinishTransitioningEvent(getId()));
|
|
}
|
|
|
|
@Override
|
|
protected void removeScreenAt(int index) {
|
|
Screen toBeRemoved = getScreenAt(index);
|
|
mDismissed.remove(toBeRemoved);
|
|
super.removeScreenAt(index);
|
|
}
|
|
|
|
@Override
|
|
protected void removeAllScreens() {
|
|
mDismissed.clear();
|
|
super.removeAllScreens();
|
|
}
|
|
|
|
@Override
|
|
protected boolean hasScreen(ScreenFragment screenFragment) {
|
|
return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment);
|
|
}
|
|
|
|
@Override
|
|
protected void onUpdate() {
|
|
// remove all screens previously on stack
|
|
for (ScreenStackFragment screen : mStack) {
|
|
if (!mScreenFragments.contains(screen) || mDismissed.contains(screen)) {
|
|
getOrCreateTransaction().remove(screen);
|
|
}
|
|
}
|
|
|
|
// When going back from a nested stack with a single screen on it, we may hit an edge case
|
|
// when all screens are dismissed and no screen is to be displayed on top. We need to gracefully
|
|
// handle the case of newTop being NULL, which happens in several places below
|
|
ScreenStackFragment newTop = null; // newTop is nullable, see the above comment ^
|
|
ScreenStackFragment belowTop = null; // this is only set if newTop has TRANSPARENT_MODAL presentation mode
|
|
|
|
for (int i = mScreenFragments.size() - 1; i >= 0; i--) {
|
|
ScreenStackFragment screen = mScreenFragments.get(i);
|
|
if (!mDismissed.contains(screen)) {
|
|
if (newTop == null) {
|
|
newTop = screen;
|
|
if (newTop.getScreen().getStackPresentation() != Screen.StackPresentation.TRANSPARENT_MODAL) {
|
|
break;
|
|
}
|
|
} else {
|
|
belowTop = screen;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (ScreenStackFragment screen : mScreenFragments) {
|
|
// detach all screens that should not be visible
|
|
if (screen != newTop && screen != belowTop && !mDismissed.contains(screen)) {
|
|
getOrCreateTransaction().remove(screen);
|
|
}
|
|
}
|
|
// attach "below top" screen if set
|
|
if (belowTop != null && !belowTop.isAdded()) {
|
|
final ScreenStackFragment top = newTop;
|
|
getOrCreateTransaction().add(getId(), belowTop).runOnCommit(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
top.getScreen().bringToFront();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (newTop != null && !newTop.isAdded()) {
|
|
getOrCreateTransaction().add(getId(), newTop);
|
|
}
|
|
|
|
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.getScreen().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.getScreen().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(mScreenFragments);
|
|
|
|
tryCommitTransaction();
|
|
|
|
if (mTopScreen != null) {
|
|
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) {
|
|
if (!mTopScreen.isResumed()) {
|
|
// if the top fragment is not in a resumed state, adding back stack transaction would throw.
|
|
// In such a case we skip installing back handler and use FragmentLifecycleCallbacks to get
|
|
// notified when it gets resumed so that we can install the handler.
|
|
return;
|
|
}
|
|
mFragmentManager.removeOnBackStackChangedListener(mBackStackListener);
|
|
mFragmentManager.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()) {
|
|
mFragmentManager
|
|
.beginTransaction()
|
|
.show(topScreen)
|
|
.addToBackStack(BACK_STACK_TAG)
|
|
.setPrimaryNavigationFragment(topScreen)
|
|
.commitAllowingStateLoss();
|
|
mFragmentManager.addOnBackStackChangedListener(mBackStackListener);
|
|
}
|
|
}
|
|
}
|