Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 33 additions & 87 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,76 +9,14 @@ import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.rnscreens.Screen.StackAnimation
import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
import com.swmansion.rnscreens.events.StackFinishTransitioningEvent
import com.swmansion.rnscreens.stack.views.ChildrenDrawingOrderStrategy
import com.swmansion.rnscreens.stack.views.ReverseFromIndex
import com.swmansion.rnscreens.stack.views.ReverseOrder
import com.swmansion.rnscreens.stack.views.ScreensCoordinatorLayout
import com.swmansion.rnscreens.utils.setTweenAnimations
import java.util.Collections
import kotlin.collections.ArrayList
import kotlin.math.max

internal interface ChildDrawingOrderStrategy {
/**
* Mutates the list of draw operations **in-place**.
*/
fun apply(drawingOperations: MutableList<ScreenStack.DrawingOp>)

/**
* Enables the given strategy. When enabled - the strategy **might** mutate the operations
* list passed to `apply` method.
*/
fun enable()

/**
* Disables the given strategy - even when `apply` is called it **must not** produce
* any side effect (it must not manipulate the drawing operations list passed to `apply` method).
*/
fun disable()

fun isEnabled(): Boolean
}

internal abstract class ChildDrawingOrderStrategyBase(
var enabled: Boolean = false,
) : ChildDrawingOrderStrategy {
override fun enable() {
enabled = true
}

override fun disable() {
enabled = false
}

override fun isEnabled() = enabled
}

internal class SwapLastTwo : ChildDrawingOrderStrategyBase() {
override fun apply(drawingOperations: MutableList<ScreenStack.DrawingOp>) {
if (!isEnabled()) {
return
}
if (drawingOperations.size >= 2) {
Collections.swap(drawingOperations, drawingOperations.lastIndex, drawingOperations.lastIndex - 1)
}
}
}

internal class ReverseOrderInRange(
val range: IntRange,
) : ChildDrawingOrderStrategyBase() {
override fun apply(drawingOperations: MutableList<ScreenStack.DrawingOp>) {
if (!isEnabled()) {
return
}

var startRange = range.start
var endRange = range.endInclusive

while (startRange < endRange) {
Collections.swap(drawingOperations, startRange, endRange)
startRange += 1
endRange -= 1
}
}
}

class ScreenStack(
context: Context?,
) : ScreenContainer(context) {
Expand All @@ -88,9 +26,9 @@ class ScreenStack(
private var drawingOps: MutableList<DrawingOp> = ArrayList()
private var topScreenWrapper: ScreenStackFragmentWrapper? = null
private var removalTransitionStarted = false
private var previousChildrenCount = 0

private var childDrawingOrderStrategy: ChildDrawingOrderStrategy? = null
private var childrenDrawingOrderStrategy: ChildrenDrawingOrderStrategy? = null
private var disappearingTransitioningChildren: MutableList<View> = ArrayList()

var goingForward = false

Expand Down Expand Up @@ -122,14 +60,25 @@ class ScreenStack(
}

override fun startViewTransition(view: View) {
check(view is ScreensCoordinatorLayout) { "[RNScreens] Unexpected type of ScreenStack direct subview ${view.javaClass}" }
super.startViewTransition(view)
childDrawingOrderStrategy?.enable()
if (view.fragment.isRemoving) {
disappearingTransitioningChildren.add(view)
}
if (disappearingTransitioningChildren.isNotEmpty()) {
childrenDrawingOrderStrategy?.enable()
}
removalTransitionStarted = true
}

override fun endViewTransition(view: View) {
super.endViewTransition(view)
childDrawingOrderStrategy?.disable()

disappearingTransitioningChildren.remove(view)

if (disappearingTransitioningChildren.isEmpty()) {
childrenDrawingOrderStrategy?.disable()
}
if (removalTransitionStarted) {
removalTransitionStarted = false
dispatchOnFinishTransitioning()
Expand Down Expand Up @@ -172,14 +121,17 @@ class ScreenStack(
var visibleBottom: ScreenFragmentWrapper? = null

// reset, to not use previously set strategy by mistake
childDrawingOrderStrategy = null
childrenDrawingOrderStrategy = null

// Determine new first & last visible screens.
val notDismissedWrappers =
screenWrappers
.asReversed()
.asSequence()
.filter { !dismissedWrappers.contains(it) && it.screen.activityState !== Screen.ActivityState.INACTIVE }
.filter {
!dismissedWrappers.contains(it) &&
it.screen.activityState !== Screen.ActivityState.INACTIVE
}

newTop = notDismissedWrappers.firstOrNull()
visibleBottom =
Expand Down Expand Up @@ -226,16 +178,16 @@ class ScreenStack(
needsDrawReordering(newTop, stackAnimation) &&
visibleBottom == null
) {
// When using an open animation in which two screens overlap (eg. fade_from_bottom or
// slide_from_bottom), we want to draw the previous screen under the new one,
// which is apparently not the default option. Android always draws the disappearing view
// When using an open animation in which screens overlap (eg. fade_from_bottom or
// slide_from_bottom), we want to draw the previous screens under the new one,
// which is apparently not the default option. Android always draws the disappearing views
// on top of the appearing one. We then reverse the order of the views so the new screen
// appears on top of the previous one. You can read more about in the comment
// appears on top of the previous ones. You can read more about in the comment
// for the code we use to change that behavior:
// https://github.com/airbnb/native-navigation/blob/9cf50bf9b751b40778f473f3b19fcfe2c4d40599/lib/android/src/main/java/com/airbnb/android/react/navigation/ScreenCoordinatorLayout.java#L18
// Note: This should not be set in case there is only a single screen in stack or animation `none` is used.
// Atm needsDrawReordering implementation guards that assuming that first screen on stack uses `NONE` animation.
childDrawingOrderStrategy = SwapLastTwo()
childrenDrawingOrderStrategy = ReverseOrder()
} else if (newTop != null &&
newTopAlreadyInStack &&
topScreenWrapper?.isTranslucent() == true &&
Expand All @@ -253,8 +205,8 @@ class ScreenStack(
it.isTranslucent()
}.count()
if (dismissedTransparentScreenApproxCount > 1) {
childDrawingOrderStrategy =
ReverseOrderInRange(max(stack.lastIndex - dismissedTransparentScreenApproxCount + 1, 0)..stack.lastIndex)
childrenDrawingOrderStrategy =
ReverseFromIndex(max(stack.lastIndex - dismissedTransparentScreenApproxCount + 1, 0))
}
}

Expand Down Expand Up @@ -351,13 +303,7 @@ class ScreenStack(
override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas)

// check the view removal is completed (by comparing the previous children count)
if (drawingOps.size < previousChildrenCount) {
childDrawingOrderStrategy = null
}
previousChildrenCount = drawingOps.size

childDrawingOrderStrategy?.apply(drawingOps)
childrenDrawingOrderStrategy?.apply(drawingOps)

drawAndRelease()
}
Expand Down Expand Up @@ -407,7 +353,7 @@ class ScreenStack(
fragmentWrapper: ScreenFragmentWrapper,
resolvedStackAnimation: StackAnimation?,
): Boolean {
val stackAnimation = if (resolvedStackAnimation != null) resolvedStackAnimation else fragmentWrapper.screen.stackAnimation
val stackAnimation = resolvedStackAnimation ?: fragmentWrapper.screen.stackAnimation
// On Android sdk 33 and above the animation is different and requires draw reordering.
// For React Native 0.70 and lower versions, `Build.VERSION_CODES.TIRAMISU` is not defined yet.
// Hence, we're comparing numerical version here.
Expand Down
124 changes: 1 addition & 123 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
Expand All @@ -14,15 +13,11 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.Transformation
import android.widget.LinearLayout
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.ReactPointerEventsView
import com.facebook.react.uimanager.UIManagerHelper
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
Expand All @@ -37,6 +32,7 @@ import com.swmansion.rnscreens.events.ScreenAnimationDelegate
import com.swmansion.rnscreens.events.ScreenDismissedEvent
import com.swmansion.rnscreens.events.ScreenEventEmitter
import com.swmansion.rnscreens.ext.recycle
import com.swmansion.rnscreens.stack.views.ScreensCoordinatorLayout
import com.swmansion.rnscreens.transition.ExternalBoundaryValuesEvaluator
import com.swmansion.rnscreens.utils.DeviceUtils
import com.swmansion.rnscreens.utils.resolveBackgroundColor
Expand Down Expand Up @@ -471,122 +467,4 @@ class ScreenStackFragment :
}
return sheetDelegate!!
}

private class ScreensCoordinatorLayout(
context: Context,
private val fragment: ScreenStackFragment,
private val pointerEventsImpl: ReactPointerEventsView,
// ) : CoordinatorLayout(context), ReactCompoundViewGroup, ReactHitSlopView {
) : CoordinatorLayout(context),
ReactPointerEventsView by pointerEventsImpl {
constructor(context: Context, fragment: ScreenStackFragment) : this(
context,
fragment,
PointerEventsBoxNoneImpl(),
)

override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets = super.onApplyWindowInsets(insets)

private val animationListener: Animation.AnimationListener =
object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
fragment.onViewAnimationStart()
}

override fun onAnimationEnd(animation: Animation) {
fragment.onViewAnimationEnd()
}

override fun onAnimationRepeat(animation: Animation) {}
}

override fun startAnimation(animation: Animation) {
// For some reason View##onAnimationEnd doesn't get called for
// exit transitions so we explicitly attach animation listener.
// We also have some animations that are an AnimationSet, so we don't wrap them
// in another set since it causes some visual glitches when going forward.
// We also set the listener only when going forward, since when going back,
// there is already a listener for dismiss action added, which would be overridden
// and also this is not necessary when going back since the lifecycle methods
// are correctly dispatched then.
// We also add fakeAnimation to the set of animations, which sends the progress of animation
val fakeAnimation = ScreensAnimation(fragment).apply { duration = animation.duration }

if (animation is AnimationSet && !fragment.isRemoving) {
animation
.apply {
addAnimation(fakeAnimation)
setAnimationListener(animationListener)
}.also {
super.startAnimation(it)
}
} else {
AnimationSet(true)
.apply {
addAnimation(animation)
addAnimation(fakeAnimation)
setAnimationListener(animationListener)
}.also {
super.startAnimation(it)
}
}
}

/**
* This method implements a workaround for RN's autoFocus functionality. Because of the way
* autoFocus is implemented it dismisses soft keyboard in fragment transition
* due to change of visibility of the view at the start of the transition. Here we override the
* call to `clearFocus` when the visibility of view is `INVISIBLE` since `clearFocus` triggers the
* hiding of the keyboard in `ReactEditText.java`.
*/
override fun clearFocus() {
if (visibility != INVISIBLE) {
super.clearFocus()
}
}

override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int,
) {
super.onLayout(changed, l, t, r, b)

if (fragment.screen.usesFormSheetPresentation()) {
fragment.screen.onBottomSheetBehaviorDidLayout(changed)
}
}

// override fun reactTagForTouch(touchX: Float, touchY: Float): Int {
// throw IllegalStateException("Screen wrapper should never be asked for the view tag")
// }
//
// override fun interceptsTouchEvent(touchX: Float, touchY: Float): Boolean {
// return false
// }
//
// override fun getHitSlopRect(): Rect? {
// val screen: Screen = fragment.screen
// // left – The X coordinate of the left side of the rectangle
// // top – The Y coordinate of the top of the rectangle i
// // right – The X coordinate of the right side of the rectangle
// // bottom – The Y coordinate of the bottom of the rectangle
// return Rect(screen.x.toInt(), -screen.y.toInt(), screen.x.toInt() + screen.width, screen.y.toInt() + screen.height)
// }
}

private class ScreensAnimation(
private val mFragment: ScreenFragment,
) : Animation() {
override fun applyTransformation(
interpolatedTime: Float,
t: Transformation,
) {
super.applyTransformation(interpolatedTime, t)
// interpolated time should be the progress of the current transition
mFragment.dispatchTransitionProgressEvent(interpolatedTime, !mFragment.isResumed)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.swmansion.rnscreens.stack.anim

import android.view.animation.Animation
import android.view.animation.Transformation
import com.swmansion.rnscreens.ScreenFragment

internal class ScreensAnimation(
private val mFragment: ScreenFragment,
) : Animation() {
override fun applyTransformation(
interpolatedTime: Float,
t: Transformation,
) {
super.applyTransformation(interpolatedTime, t)
// interpolated time should be the progress of the current transition
mFragment.dispatchTransitionProgressEvent(interpolatedTime, !mFragment.isResumed)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action point for the future: Fragment ScreenFragment should have onTransitionProgress callback & it should be called here. This class should not dispatch the events directly.

}
}
Loading
Loading