Skip to content

Commit

Permalink
[native-stack] Add transitionStart and transitionEnd events (#499)
Browse files Browse the repository at this point in the history
Implement transitionStart and transitionEnd events in native stack, similar to the events available in js stack. On Android there isn't really a concept of nested stacks and it uses only one fragment manager. This means a single "transition" so we need a way to trigger this transition on all nested stacks of a screen. To do that we added a way for nested stacks to register on their parent screen so when a screen is dismissed it can also call the dismiss method on all its child stacks.
  • Loading branch information
janicduplessis authored Aug 1, 2020
1 parent df1551d commit b1078b2
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 30 deletions.
8 changes: 8 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/Screen.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public void onViewDetachedFromWindow(View view) {
private StackAnimation mStackAnimation = StackAnimation.DEFAULT;
private boolean mGestureEnabled = true;

@Override
protected void onAnimationStart() {
super.onAnimationStart();
if (mFragment != null) {
mFragment.onViewAnimationStart();
}
}

@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
private boolean mNeedUpdate;
private boolean mIsAttached;
private boolean mLayoutEnqueued = false;
private @Nullable ScreenFragment mParentScreenFragment = null;


private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
Expand Down Expand Up @@ -74,6 +75,10 @@ public void requestLayout() {
}
}

public boolean isNested() {
return mParentScreenFragment != null;
}

protected void markUpdated() {
if (!mNeedUpdate) {
mNeedUpdate = true;
Expand Down Expand Up @@ -137,8 +142,10 @@ private void setupFragmentManager() {
// If parent is of type Screen it means we are inside a nested fragment structure.
// Otherwise we expect to connect directly with root view and get root fragment manager
if (parent instanceof Screen) {
Fragment screenFragment = ((Screen) parent).getFragment();
ScreenFragment screenFragment = ((Screen) parent).getFragment();
setFragmentManager(screenFragment.getChildFragmentManager());
mParentScreenFragment = screenFragment;
mParentScreenFragment.registerChildScreenContainer(this);
return;
}

Expand Down Expand Up @@ -250,6 +257,12 @@ protected void onDetachedFromWindow() {
removeMyFragments();
mFragmentManager.executePendingTransactions();
}

if (mParentScreenFragment != null) {
mParentScreenFragment.unregisterChildScreenContainer(this);
mParentScreenFragment = null;
}

super.onDetachedFromWindow();
mIsAttached = false;
// When fragment container view is detached we force all its children to be removed.
Expand Down
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 ScreenDisappearEvent extends Event<ScreenAppearEvent> {

public static final String EVENT_NAME = "topDisappear";

public ScreenDisappearEvent(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());
}
}
87 changes: 82 additions & 5 deletions android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
import android.view.ViewParent;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

public class ScreenFragment extends Fragment {

protected static View recycleView(View view) {
Expand All @@ -33,6 +36,7 @@ protected static View recycleView(View view) {
}

protected Screen mScreenView;
private List<ScreenContainer> mChildScreenContainers = new ArrayList<>();

public ScreenFragment() {
throw new IllegalStateException("Screen fragments should never be restored");
Expand Down Expand Up @@ -60,18 +64,90 @@ public Screen getScreen() {
return mScreenView;
}

private void dispatchOnAppear() {
protected void dispatchOnWillAppear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenWillAppearEvent(mScreenView.getId()));

for (ScreenContainer sc : mChildScreenContainers) {
if (sc.getScreenCount() > 0) {
Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1);
topScreen.getFragment().dispatchOnWillAppear();
}
}
}

protected void dispatchOnAppear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenAppearEvent(mScreenView.getId()));

for (ScreenContainer sc : mChildScreenContainers) {
if (sc.getScreenCount() > 0) {
Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1);
topScreen.getFragment().dispatchOnAppear();
}
}
}

protected void dispatchOnWillDisappear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenWillDisappearEvent(mScreenView.getId()));

for (ScreenContainer sc : mChildScreenContainers) {
if (sc.getScreenCount() > 0) {
Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1);
topScreen.getFragment().dispatchOnWillDisappear();
}
}
}

protected void dispatchOnDisappear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenDisappearEvent(mScreenView.getId()));

for (ScreenContainer sc : mChildScreenContainers) {
if (sc.getScreenCount() > 0) {
Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1);
topScreen.getFragment().dispatchOnDisappear();
}
}
}

public void registerChildScreenContainer(ScreenContainer screenContainer) {
mChildScreenContainers.add(screenContainer);
}

public void unregisterChildScreenContainer(ScreenContainer screenContainer) {
mChildScreenContainers.remove(screenContainer);
}

public void onViewAnimationStart() {
// onViewAnimationStart is triggered from View#onAnimationStart method of the fragment's root view.
// We override Screen#onAnimationStart and an appropriate method of the StackFragment's root view
// in order to achieve this.
if (isResumed()) {
dispatchOnWillAppear();
} else {
dispatchOnWillDisappear();
}
}

public void onViewAnimationEnd() {
// onViewAnimationEnd is triggered from View#onAnimationEnd method of the fragment's root view.
// We override Screen#onAnimationEnd and an appropriate method of the StackFragment's root view
// in order to achieve this.
dispatchOnAppear();
if (isResumed()) {
dispatchOnAppear();
} else {
dispatchOnDisappear();
}
}

@Override
Expand All @@ -85,5 +161,6 @@ public void onDestroy() {
.getEventDispatcher()
.dispatchEvent(new ScreenDismissedEvent(mScreenView.getId()));
}
mChildScreenContainers.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.widget.LinearLayout;

import com.facebook.react.uimanager.PixelUtil;
import com.google.android.material.appbar.AppBarLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment;

import com.facebook.react.uimanager.PixelUtil;
import com.google.android.material.appbar.AppBarLayout;

public class ScreenStackFragment extends ScreenFragment {

private static class NotifyingCoordinatorLayout extends CoordinatorLayout {
Expand All @@ -31,10 +32,31 @@ public NotifyingCoordinatorLayout(@NonNull Context context, ScreenFragment fragm
mFragment = fragment;
}

private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mFragment.onViewAnimationStart();
}

@Override
public void onAnimationEnd(Animation animation) {
mFragment.onViewAnimationEnd();
}

@Override
public void onAnimationRepeat(Animation animation) {

}
};

@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
mFragment.onViewAnimationEnd();
public void startAnimation(Animation animation) {
// For some reason View##onAnimationEnd doesn't get called for
// exit transitions so we use this hack.
AnimationSet set = new AnimationSet(true);
set.addAnimation(animation);
set.setAnimationListener(mAnimationListener);
super.startAnimation(set);
}
}

Expand Down Expand Up @@ -89,12 +111,32 @@ public void onViewAnimationEnd() {

@Nullable
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (enter && transit == 0) {
// this means that the fragment will appear without transition, in this case onViewAnimationEnd
// won't be called and we need to notify stack directly from here.
notifyViewAppearTransitionEnd();
public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
// this means that the fragment will appear without transition, in this case
// onViewAnimationStart and onViewAnimationEnd won't be called and we need to notify
// stack directly from here.
// When using the Toolbar back button this is called an extra time with transit = 0 but in
// this case we don't want to notify. The way I found to detect is case is check isHidden.
if (transit == 0 && !isHidden()) {
// If the container is nested then appear events will be dispatched by their parent screen so
// they must not be triggered here.
ScreenContainer container = getScreen().getContainer();
boolean isNested = container != null && container.isNested();
if (enter) {
if (!isNested) {
dispatchOnWillAppear();
dispatchOnAppear();
}
} else {
if (!isNested) {
dispatchOnWillDisappear();
dispatchOnDisappear();
}
notifyViewAppearTransitionEnd();
}

}

return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"),
ScreenWillAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onWillAppear"),
ScreenAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onAppear"),
ScreenWillDisappearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onWillDisappear"),
ScreenDisappearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDisappear"),
StackFinishTransitioningEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onFinishTransitioning"));
}
Expand Down
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 ScreenWillAppearEvent extends Event<ScreenAppearEvent> {

public static final String EVENT_NAME = "topWillAppear";

public ScreenWillAppearEvent(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());
}
}
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 ScreenWillDisappearEvent extends Event<ScreenAppearEvent> {

public static final String EVENT_NAME = "topWillDisappear";

public ScreenWillDisappearEvent(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());
}
}
3 changes: 3 additions & 0 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
@interface RNSScreenView : RCTView

@property (nonatomic, copy) RCTDirectEventBlock onAppear;
@property (nonatomic, copy) RCTDirectEventBlock onDisappear;
@property (nonatomic, copy) RCTDirectEventBlock onDismissed;
@property (nonatomic, copy) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy) RCTDirectEventBlock onWillDisappear;
@property (weak, nonatomic) UIView<RNSScreenContainerDelegate> *reactSuperview;
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic, readonly) BOOL dismissed;
Expand Down
Loading

0 comments on commit b1078b2

Please sign in to comment.