Dispatch appear event for screens. (#248)

Appear event is used by react-navigation to properly dispatch focus. It is important that appear is dispatched after dismissed event. The reverse order of actions would result in getting react-navigation stack in a weird state.

It is relatively streightforward to implement onAppear event on iOS where we hook into didAppear callback from UIViewController. It gets dispatched in the right moment, that is when the transition is fully over.

On Android however it is much more tricky. There is no standard way to be notified from the fragment level that fragment transition finished. One way that is frequently recommended is to override Fragment.onCreateAnimation. However, this only works when custom transitions are provided (e.g. if we set the transition to use fade animation). As we want the platform native transition to be run by default we had to look for other ways. The current approach relies on fragment container's callbacks startViewTransition and endViewTransition, with the latter being triggered once the animation is over. We also need to take into account that a good starting point for the transition is when we call commit on fragment transaction. We use these two methods to determine if the fragment is instantiated (onCreate) within a running transaction and if so we schedule event dispatch at the moment when endViewTransition is called.

Another change this commit introduces on the Android side is that we no longer rely on show/hide for replacing fragments on stack and we now use add/remove transaction methods. Due to this change we had to make our fragments reusable and make onCreateView indempotent.
This commit is contained in:
Krzysztof Magiera
2019-12-11 22:28:19 +01:00
committed by GitHub
parent 1958cf37ea
commit 656e82de9f
9 changed files with 165 additions and 29 deletions

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 ScreenAppearEvent extends Event<ScreenAppearEvent> {
public static final String EVENT_NAME = "topAppear";
public ScreenAppearEvent(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

@@ -2,6 +2,7 @@ package com.swmansion.rnscreens;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
@@ -22,11 +23,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected final ArrayList<T> mScreenFragments = new ArrayList<>();
private final Set<ScreenFragment> mActiveScreenFragments = new HashSet<>();
private final ArrayList<Runnable> mAfterTransitionRunnables = new ArrayList<>(1);
private @Nullable FragmentManager mFragmentManager;
private @Nullable FragmentTransaction mCurrentTransaction;
private @Nullable FragmentTransaction mProcessingTransaction;
private boolean mNeedUpdate;
private boolean mIsAttached;
private boolean mIsTransitioning;
private boolean mLayoutEnqueued = false;
private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
@@ -101,6 +105,36 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
markUpdated();
}
@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mIsTransitioning = true;
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mIsTransitioning) {
mIsTransitioning = false;
notifyTransitionFinished();
}
}
public boolean isTransitioning() {
return mIsTransitioning || mProcessingTransaction != null;
}
public void postAfterTransition(Runnable runnable) {
mAfterTransitionRunnables.add(runnable);
}
protected void notifyTransitionFinished() {
for (int i = 0, size = mAfterTransitionRunnables.size(); i < size; i++) {
mAfterTransitionRunnables.get(i).run();
}
mAfterTransitionRunnables.clear();
}
protected int getScreenCount() {
return mScreenFragments.size();
}
@@ -159,6 +193,19 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected void tryCommitTransaction() {
if (mCurrentTransaction != null) {
final FragmentTransaction transaction = mCurrentTransaction;
mProcessingTransaction = transaction;
mProcessingTransaction.runOnCommit(new Runnable() {
@Override
public void run() {
if (mProcessingTransaction == transaction) {
// we need to take into account that commit is initiated with some other transaction while
// the previous one is still processing. In this case mProcessingTransaction gets overwritten
// and we don't want to set it to null until the second transaction is finished.
mProcessingTransaction = null;
}
}
});
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
}
@@ -184,6 +231,10 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
return screenFragment.getScreen().isActive();
}
protected boolean hasScreen(ScreenFragment screenFragment) {
return mScreenFragments.contains(screenFragment);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();

View File

@@ -7,12 +7,10 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
public class ScreenFragment extends Fragment {
@@ -39,12 +37,39 @@ public class ScreenFragment extends Fragment {
return mScreenView;
}
@Override
public void onDestroy() {
super.onDestroy();
private void dispatchOnAppear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenDismissedEvent(mScreenView.getId()));
.dispatchEvent(new ScreenAppearEvent(mScreenView.getId()));
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ScreenContainer container = mScreenView.getContainer();
if (container.isTransitioning()) {
container.postAfterTransition(new Runnable() {
@Override
public void run() {
dispatchOnAppear();
}
});
} else {
dispatchOnAppear();
}
}
@Override
public void onDestroy() {
super.onDestroy();
ScreenContainer container = mScreenView.getContainer();
if (container == null || !container.hasScreen(this)) {
// we only send dismissed even when the screen has been removed from its container
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenDismissedEvent(mScreenView.getId()));
}
}
}

View File

@@ -97,6 +97,11 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
super.removeScreenAt(index);
}
@Override
protected boolean hasScreen(ScreenFragment screenFragment) {
return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment);
}
@Override
protected void onUpdate() {
// remove all screens previously on stack
@@ -128,19 +133,15 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
for (ScreenStackFragment screen : mScreenFragments) {
// add all new views that weren't on stack before
if (!mStack.contains(screen) && !mDismissed.contains(screen)) {
getOrCreateTransaction().add(getId(), screen);
}
// detach all screens that should not be visible
if (screen != newTop && screen != belowTop && !mDismissed.contains(screen)) {
getOrCreateTransaction().hide(screen);
getOrCreateTransaction().remove(screen);
}
}
// attach "below top" screen if set
if (belowTop != null) {
if (belowTop != null && !belowTop.isAdded()) {
final ScreenStackFragment top = newTop;
getOrCreateTransaction().show(belowTop).runOnCommit(new Runnable() {
getOrCreateTransaction().add(getId(), belowTop).runOnCommit(new Runnable() {
@Override
public void run() {
top.getScreen().bringToFront();
@@ -148,8 +149,8 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
});
}
if (newTop != null) {
getOrCreateTransaction().show(newTop);
if (newTop != null && !newTop.isAdded()) {
getOrCreateTransaction().add(getId(), newTop);
}
if (!mStack.contains(newTop)) {

View File

@@ -22,6 +22,7 @@ public class ScreenStackFragment extends ScreenFragment {
private AppBarLayout mAppBarLayout;
private Toolbar mToolbar;
private boolean mShadowHidden;
private CoordinatorLayout mScreenRootView;
@SuppressLint("ValidFragment")
public ScreenStackFragment(Screen screenView) {
@@ -59,10 +60,7 @@ public class ScreenStackFragment extends ScreenFragment {
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
private CoordinatorLayout configureView() {
CoordinatorLayout view = new CoordinatorLayout(getContext());
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
@@ -87,6 +85,17 @@ public class ScreenStackFragment extends ScreenFragment {
return view;
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (mScreenRootView == null) {
mScreenRootView = configureView();
}
return mScreenRootView;
}
public boolean isDismissable() {
View child = mScreenView.getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {

View File

@@ -62,6 +62,8 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"));
MapBuilder.of("registrationName", "onDismissed"),
ScreenAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onAppear"));
}
}

View File

@@ -4,7 +4,6 @@ import {
StackRouter,
SceneView,
StackActions,
NavigationActions,
createNavigator,
} from '@react-navigation/core';
import { createKeyboardAwareNavigator } from '@react-navigation/native';
@@ -27,14 +26,13 @@ function renderComponentOrThunk(componentOrThunk, props) {
class StackView extends React.Component {
_removeScene = route => {
const { navigation } = this.props;
navigation.dispatch(
NavigationActions.back({
key: route.key,
immediate: true,
})
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
};
_onSceneFocus = route => {
this.props.navigation.dispatch(
StackActions.completeTransition({ toChildKey: route.key })
);
navigation.dispatch(StackActions.completeTransition());
};
_renderHeaderConfig = (index, route, descriptor) => {
@@ -165,7 +163,7 @@ class StackView extends React.Component {
transparentCard || options.cardTransparent ? 'transparentModal' : mode;
}
let stackAnimation = undefined;
let stackAnimation;
if (options.animationEnabled === false) {
stackAnimation = 'none';
}
@@ -177,6 +175,7 @@ class StackView extends React.Component {
style={options.cardStyle}
stackAnimation={stackAnimation}
stackPresentation={stackPresentation}
onAppear={() => this._onSceneFocus(route)}
onDismissed={() => this._removeScene(route)}>
{this._renderHeaderConfig(index, route, descriptor)}
<SceneView

View File

@@ -39,6 +39,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
@interface RNSScreenView : RCTView
@property (nonatomic, copy) RCTDirectEventBlock onAppear;
@property (nonatomic, copy) RCTDirectEventBlock onDismissed;
@property (weak, nonatomic) UIView<RNSScreenContainerDelegate> *reactSuperview;
@property (nonatomic, retain) UIViewController *controller;

View File

@@ -135,6 +135,17 @@
}
}
- (void)notifyAppear
{
if (self.onAppear) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onAppear) {
self.onAppear(nil);
}
});
}
}
- (BOOL)isMountedUnderScreenOrReactRoot
{
for (UIView *parent = self.superview; parent != nil; parent = parent.superview) {
@@ -235,6 +246,12 @@
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[((RNSScreenView *)self.view) notifyAppear];
}
- (void)notifyFinishTransitioning
{
[_previousFirstResponder becomeFirstResponder];
@@ -258,6 +275,7 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
- (UIView *)view