Skip to content

fix(Android): drawing order for multiple disappearing screens#2806

Merged
kkafar merged 15 commits intomainfrom
@kligarski/drawing-order-for-multiple-disappearing-screens-2
Apr 2, 2025
Merged

fix(Android): drawing order for multiple disappearing screens#2806
kkafar merged 15 commits intomainfrom
@kligarski/drawing-order-for-multiple-disappearing-screens-2

Conversation

@kligarski
Copy link
Copy Markdown
Contributor

Description

Fixes drawing order to account for multiple disappearing screens (e.g. formSheets).

Changes

  • move ScreensCoordinatorLayout, ScreensAnimation to seperate package
  • move ChildDrawingOrderStrategy interface to seperate file
  • track disappearing transitioning children & disable ChildDrawingOrderStrategy when the transition is over
  • add new, more general ReverseOrderStartingFrom strategy to replace SwapLastTwo and use it in ScreenStack's onUpdate

Test code and steps to reproduce

Currently, the transition from a formSheet to push is not working properly (two animation mechanisms don't work correctly together) so it is difficult to verify drawing order. To temporarily disable formSheet animation, you can return null at the beginning of ScreenStackFragment's onCreateAnimator method. Then go to TestFormSheet example sheet, open the sheet and then click Open second. New screen will be drawn over disappearing screens during the transition.

Checklist

  • Included code example that can be used to test this change
  • Ensured that CI passes

Copy link
Copy Markdown
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

This looks good!
I'll test it later in the day & we'll merge it after 4.10.0 release.

Comment on lines +177 to +191
val notDismissedWrappers =
screenWrappers
.asReversed()
.asSequence()
.filter {
!dismissedWrappers.contains(it) &&
it.screen.activityState !== Screen.ActivityState.INACTIVE
}

newTop = notDismissedWrappers.firstOrNull()
visibleBottom =
notDismissedWrappers
.dropWhile { it.screen.isTransparent() }
.firstOrNull()
?.takeUnless { it === newTop }
}
newTop = notDismissedWrappers.firstOrNull()
visibleBottom =
notDismissedWrappers
.dropWhile { it.screen.isTransparent() }
.firstOrNull()
?.takeUnless { it === newTop }
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.

You moved it out from run scope to fix this compiler warnings, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, I used notDismissedWrappers sequence to count disappearing views later in the code (line 244) and I didn't want to repeat the same logic there with reversing and filtering the sequence. Was the run scope important?

Copy link
Copy Markdown
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

Left some notes for future us

) {
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.

Comment on lines +243 to +247
val disappearingCount =
notDismissedWrappers
.drop(1)
.takeWhile { it.screen.isTransparent() }
.count() + 1
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.

I'm reading the code once again now & my initial impression is that it won't work in general case. I need to come up with counter example -- will try to do that next week.

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.

The problem is that notDismissedWrappers sequence is computed basing on screenWrappers - these represent current react view state. We don't have guarantees that this is consistent with currently HostTree state represented by stack list.

I think we need to find some time in upcoming weeks & design this algorithm properly from the ground up.

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.

Notation:

  • uppercase letters, e.g. A, B - opaque screens,
  • lowercase letters, e.g. a, b - translucent screens.

stack: Abc
screenWrappers: AD

This case will have issue, where the count of dismissed screens will be 0 -> therefore we won't change drawing order, while we should.

One thing to note that current solution might be good enough to work in simple stack operations (push, pop, replace). The one I've just described is not a stack operation.

My aspiration is to make the screen stack work with arbitrary state changes, but I guess we can postpone this plant until we have more robust update algorithm.

Copy link
Copy Markdown
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

I've also spotted problem while testing on Example app.

Screen.Recording.2025-03-28.at.19.06.15.mov

I do not know whether this is a regression or not. We must check before proceeding.

@kkafar
Copy link
Copy Markdown
Member

kkafar commented Mar 28, 2025

Yeah, this is a regression. Works fine on main.

Don't know what the culprit is yet. Will look into it when I have a moment next week.

@kkafar kkafar self-requested a review April 2, 2025 12:38
Copy link
Copy Markdown
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

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

Okay, I think that current approach is reasonable.

When we decide to swap:

  1. we want to draw entering screen after exiting ones -> therefore we need to put entering screen at the end of the list,
  2. when we have multiple exiting screens we want to reverse the order among them, because framework draws them reversed (disappearing child with greater index in parent is drawn before one with smaller index).

Therefore reversing whole drawing operation list seems 🆗

@kkafar
Copy link
Copy Markdown
Member

kkafar commented Apr 2, 2025

I'll merge it once we manage to convince the CI to pass

@kkafar
Copy link
Copy Markdown
Member

kkafar commented Apr 2, 2025

No idea why CI fails. It passes locally.

image

@kkafar kkafar merged commit 172935e into main Apr 2, 2025
4 of 6 checks passed
@kkafar kkafar deleted the @kligarski/drawing-order-for-multiple-disappearing-screens-2 branch April 2, 2025 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants