Skip to content

Commit

Permalink
Dispatch appear event for screens. (#248)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kmagiera authored Dec 11, 2019
1 parent 1958cf3 commit 656e82d
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -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());
}
}
51 changes: 51 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.content.Context;
import android.content.ContextWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;

Expand All @@ -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() {
Expand Down Expand Up @@ -101,6 +105,36 @@ protected void removeScreenAt(int index) {
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();
}
Expand Down Expand Up @@ -159,6 +193,19 @@ protected FragmentTransaction getOrCreateTransaction() {

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;
}
Expand All @@ -184,6 +231,10 @@ protected boolean isScreenActive(ScreenFragment screenFragment) {
return screenFragment.getScreen().isActive();
}

protected boolean hasScreen(ScreenFragment screenFragment) {
return mScreenFragments.contains(screenFragment);
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Expand Down
37 changes: 31 additions & 6 deletions android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
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 {

Expand All @@ -39,12 +37,39 @@ public Screen getScreen() {
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()));
}
}
}
19 changes: 10 additions & 9 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ protected void removeScreenAt(int index) {
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
Expand Down Expand Up @@ -128,28 +133,24 @@ protected void onUpdate() {
}

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();
}
});
}

if (newTop != null) {
getOrCreateTransaction().show(newTop);
if (newTop != null && !newTop.isAdded()) {
getOrCreateTransaction().add(getId(), newTop);
}

if (!mStack.contains(newTop)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -59,10 +60,7 @@ public void onStackUpdate() {
}
}

@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);
Expand All @@ -87,6 +85,17 @@ public View onCreateView(LayoutInflater inflater,
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public void setStackAnimation(Screen view, String animation) {
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"));
MapBuilder.of("registrationName", "onDismissed"),
ScreenAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onAppear"));
}
}
17 changes: 8 additions & 9 deletions createNativeStackNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
StackRouter,
SceneView,
StackActions,
NavigationActions,
createNavigator,
} from '@react-navigation/core';
import { createKeyboardAwareNavigator } from '@react-navigation/native';
Expand All @@ -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) => {
Expand Down Expand Up @@ -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';
}
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions ios/RNSScreen.m
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ - (void)notifyDismissed
}
}

- (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) {
Expand Down Expand Up @@ -235,6 +246,12 @@ - (void)viewDidDisappear:(BOOL)animated
}
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[((RNSScreenView *)self.view) notifyAppear];
}

- (void)notifyFinishTransitioning
{
[_previousFirstResponder becomeFirstResponder];
Expand All @@ -258,6 +275,7 @@ @implementation RNSScreenManager
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
Expand Down

0 comments on commit 656e82d

Please sign in to comment.