-
Notifications
You must be signed in to change notification settings - Fork 24.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix view indices with Android LayoutAnimation #18830
Fix view indices with Android LayoutAnimation #18830
Conversation
Fixes issue #11828 that causes layout animations for removed views to remove some adjacent views as well. This happens because the animated views are still present in the ViewGroup, which throws off subsequent operations that rely on view indices having updated. This issue was addressed in #11962, which was closed in favour of a more reliable solution that addresses the issue globally since it’s difficult to account for animated views everywhere. @janicduplessis recommended[0] handling the issue through ViewManager. Since API 11, Android provides `ViewGroup#startViewTransition(View)` that can be used to keep child views visible even if they have been removed from the group. ViewGroup keeps an array of these views, which is only used for drawing. Methods such as `ViewGroup#getChildCount()` and `ViewGroup#getChildAt(int)` will ignore them. I believe relying on these framework methods within ViewManager is the most reliable way to solve this issue because it also works if callers ignore ViewManager and reach into the native view indices and counts directly. [0]: #11962 (review)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice, I didn't know about that method! Could you double check if it works well with this gist https://gist.github.com/janicduplessis/211d78c5408ab17dee7ad2e381648a48, it's what I was using when testing this issue. You can make the animation speed pretty slow and trigger multiple animations at the same time and make sure the proper number of views are there.
It seems similar to the technique I wanted to use in #16091 and that iOS uses but using a platform implementation which is way better (since my implementation had a bug left that I didn't have time to investigate 😆). |
Thanks for taking a look! I’ll try it out with that gist first thing tomorrow. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mdvacca has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
Tried it out, works fine! I managed to get master to crash due to a wrong index by removing lots of squares rapidly and then adding some, but this branch was stable. Looks like #17118. I can see how this would happen, the views are added to a certain index and ViewGroup throws if the index is greater than the number of views in the group due to extra views having been removed. There’s still a bug with view opacities not updating when interrupting an ongoing animation (compared to iOS where they get the final value) but it looks like a separate issue to me. You can see this with the tester by smashing the “remove view” button, they have random opacities on Android once the animation settles. |
@lnikkila I'm reverting this PR, as we found a java.lang.IndexOutOfBoundsException when removing views: Exception in native call java.lang.IndexOutOfBoundsException |
Hmm could be due to |
Summary: Fixes issue facebook#11828 that causes layout animations for removed views to remove some adjacent views as well. This happens because the animated views are still present in the ViewGroup, which throws off subsequent operations that rely on view indices having updated. This issue was addressed in facebook#11962, which was closed in favour of a more reliable solution that addresses the issue globally since it’s difficult to account for animated views everywhere. janicduplessis [recommended][0] handling the issue through ViewManager. Since API 11, Android provides `ViewGroup#startViewTransition(View)` that can be used to keep child views visible even if they have been removed from the group. ViewGroup keeps an array of these views, which is only used for drawing. Methods such as `ViewGroup#getChildCount()` and `ViewGroup#getChildAt(int)` will ignore them. I believe relying on these framework methods within ViewManager is the most reliable way to solve this issue because it also works if callers ignore ViewManager and reach into the native view indices and counts directly. [0]: facebook#11962 (review) I wrote a minimal test app that you can find here: <https://gist.github.com/lnikkila/87f3825442a5773f17ead433a810d53f> The expected result is that the red and green squares disappear, a blue one appears, and the black one stays in place. iOS has this behaviour, but Android removes the black square as well. We can see the bug with some breakpoint logging. Without LayoutAnimation: ``` NativeViewHierarchyOptimizer: Removing node from parent with tag 2 at index 0 NativeViewHierarchyOptimizer: Removing node from parent with tag 4 at index 1 NativeViewHierarchyManager: Removing indices [0] with tags [2] RootViewManager: Removing child view at index 0 with tag 2 NativeViewHierarchyManager: Removing indices [1] with tags [4] RootViewManager: Removing child view at index 1 with tag 4 ``` With LayoutAnimation tag 3 gets removed when it shouldn’t be: ``` NativeViewHierarchyOptimizer: Removing node from parent with tag 2 at index 0 NativeViewHierarchyOptimizer: Removing node from parent with tag 4 at index 1 NativeViewHierarchyManager: Removing indices [0] with tags [2] NativeViewHierarchyManager: Removing indices [1] with tags [4] -> RootViewManager: Removing child view at index 1 with tag 3 RootViewManager: Removing child view at index 2 with tag 4 (Animation listener kicks in here) RootViewManager: Removing child view at index 1 with tag 2 ``` Here are some GIFs to compare, click to expand: <details> <summary><b>Current master (iOS vs Android)</b></summary> <p></p> <img src="https://user-images.githubusercontent.com/1291143/38695083-fbc29cd4-3e93-11e8-9150-9b8ea75b87aa.gif" height="400" /><img src="https://user-images.githubusercontent.com/1291143/38695108-06eb73a6-3e94-11e8-867a-b95d7f926ccd.gif" height="400" /> </details><p></p> <details> <summary><b>With this patch (iOS vs Android, fixed)</b></summary> <p></p> <img src="https://user-images.githubusercontent.com/1291143/38695083-fbc29cd4-3e93-11e8-9150-9b8ea75b87aa.gif" height="400" /><img src="https://user-images.githubusercontent.com/1291143/38695137-1090f782-3e94-11e8-94c8-ce33a5d7ebdb.gif" height="400" /> </details><p></p> Previously addressed in facebook#11962, which wasn’t merged. Tangentially related to my other LayoutAnimation PR facebook#18651. No documentation changes needed. [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Closes facebook#18830 Reviewed By: achen1 Differential Revision: D7612904 Pulled By: mdvacca fbshipit-source-id: a04cf47ab80e0e813fa043125b1f907e212b1ad4
Summary: Fixes issue facebook#11828 that causes layout animations for removed views to remove some adjacent views as well. This happens because the animated views are still present in the ViewGroup, which throws off subsequent operations that rely on view indices having updated. This issue was addressed in facebook#11962, which was closed in favour of a more reliable solution that addresses the issue globally since it’s difficult to account for animated views everywhere. janicduplessis [recommended][0] handling the issue through ViewManager. Since API 11, Android provides `ViewGroup#startViewTransition(View)` that can be used to keep child views visible even if they have been removed from the group. ViewGroup keeps an array of these views, which is only used for drawing. Methods such as `ViewGroup#getChildCount()` and `ViewGroup#getChildAt(int)` will ignore them. I believe relying on these framework methods within ViewManager is the most reliable way to solve this issue because it also works if callers ignore ViewManager and reach into the native view indices and counts directly. [0]: facebook#11962 (review) I wrote a minimal test app that you can find here: <https://gist.github.com/lnikkila/87f3825442a5773f17ead433a810d53f> The expected result is that the red and green squares disappear, a blue one appears, and the black one stays in place. iOS has this behaviour, but Android removes the black square as well. We can see the bug with some breakpoint logging. Without LayoutAnimation: ``` NativeViewHierarchyOptimizer: Removing node from parent with tag 2 at index 0 NativeViewHierarchyOptimizer: Removing node from parent with tag 4 at index 1 NativeViewHierarchyManager: Removing indices [0] with tags [2] RootViewManager: Removing child view at index 0 with tag 2 NativeViewHierarchyManager: Removing indices [1] with tags [4] RootViewManager: Removing child view at index 1 with tag 4 ``` With LayoutAnimation tag 3 gets removed when it shouldn’t be: ``` NativeViewHierarchyOptimizer: Removing node from parent with tag 2 at index 0 NativeViewHierarchyOptimizer: Removing node from parent with tag 4 at index 1 NativeViewHierarchyManager: Removing indices [0] with tags [2] NativeViewHierarchyManager: Removing indices [1] with tags [4] -> RootViewManager: Removing child view at index 1 with tag 3 RootViewManager: Removing child view at index 2 with tag 4 (Animation listener kicks in here) RootViewManager: Removing child view at index 1 with tag 2 ``` Here are some GIFs to compare, click to expand: <details> <summary><b>Current master (iOS vs Android)</b></summary> <p></p> <img src="https://user-images.githubusercontent.com/1291143/38695083-fbc29cd4-3e93-11e8-9150-9b8ea75b87aa.gif" height="400" /><img src="https://user-images.githubusercontent.com/1291143/38695108-06eb73a6-3e94-11e8-867a-b95d7f926ccd.gif" height="400" /> </details><p></p> <details> <summary><b>With this patch (iOS vs Android, fixed)</b></summary> <p></p> <img src="https://user-images.githubusercontent.com/1291143/38695083-fbc29cd4-3e93-11e8-9150-9b8ea75b87aa.gif" height="400" /><img src="https://user-images.githubusercontent.com/1291143/38695137-1090f782-3e94-11e8-94c8-ce33a5d7ebdb.gif" height="400" /> </details><p></p> Previously addressed in facebook#11962, which wasn’t merged. Tangentially related to my other LayoutAnimation PR facebook#18651. No documentation changes needed. [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Closes facebook#18830 Reviewed By: achen1 Differential Revision: D7612904 Pulled By: mdvacca fbshipit-source-id: a04cf47ab80e0e813fa043125b1f907e212b1ad4
Summary: Fixes issue facebook#11828 that causes layout animations for removed views to remove some adjacent views as well. This happens because the animated views are still present in the ViewGroup, which throws off subsequent operations that rely on view indices having updated. This issue was addressed in facebook#11962, which was closed in favour of a more reliable solution that addresses the issue globally since it’s difficult to account for animated views everywhere. janicduplessis [recommended][0] handling the issue through ViewManager. Since API 11, Android provides `ViewGroup#startViewTransition(View)` that can be used to keep child views visible even if they have been removed from the group. ViewGroup keeps an array of these views, which is only used for drawing. Methods such as `ViewGroup#getChildCount()` and `ViewGroup#getChildAt(int)` will ignore them. I believe relying on these framework methods within ViewManager is the most reliable way to solve this issue because it also works if callers ignore ViewManager and reach into the native view indices and counts directly. [0]: facebook#11962 (review) I wrote a minimal test app that you can find here: <https://gist.github.com/lnikkila/87f3825442a5773f17ead433a810d53f> The expected result is that the red and green squares disappear, a blue one appears, and the black one stays in place. iOS has this behaviour, but Android removes the black square as well. We can see the bug with some breakpoint logging. Without LayoutAnimation: ``` NativeViewHierarchyOptimizer: Removing node from parent with tag 2 at index 0 NativeViewHierarchyOptimizer: Removing node from parent with tag 4 at index 1 NativeViewHierarchyManager: Removing indices [0] with tags [2] RootViewManager: Removing child view at index 0 with tag 2 NativeViewHierarchyManager: Removing indices [1] with tags [4] RootViewManager: Removing child view at index 1 with tag 4 ``` With LayoutAnimation tag 3 gets removed when it shouldn’t be: ``` NativeViewHierarchyOptimizer: Removing node from parent with tag 2 at index 0 NativeViewHierarchyOptimizer: Removing node from parent with tag 4 at index 1 NativeViewHierarchyManager: Removing indices [0] with tags [2] NativeViewHierarchyManager: Removing indices [1] with tags [4] -> RootViewManager: Removing child view at index 1 with tag 3 RootViewManager: Removing child view at index 2 with tag 4 (Animation listener kicks in here) RootViewManager: Removing child view at index 1 with tag 2 ``` Here are some GIFs to compare, click to expand: <details> <summary><b>Current master (iOS vs Android)</b></summary> <p></p> <img src="https://user-images.githubusercontent.com/1291143/38695083-fbc29cd4-3e93-11e8-9150-9b8ea75b87aa.gif" height="400" /><img src="https://user-images.githubusercontent.com/1291143/38695108-06eb73a6-3e94-11e8-867a-b95d7f926ccd.gif" height="400" /> </details><p></p> <details> <summary><b>With this patch (iOS vs Android, fixed)</b></summary> <p></p> <img src="https://user-images.githubusercontent.com/1291143/38695083-fbc29cd4-3e93-11e8-9150-9b8ea75b87aa.gif" height="400" /><img src="https://user-images.githubusercontent.com/1291143/38695137-1090f782-3e94-11e8-94c8-ce33a5d7ebdb.gif" height="400" /> </details><p></p> Previously addressed in facebook#11962, which wasn’t merged. Tangentially related to my other LayoutAnimation PR facebook#18651. No documentation changes needed. [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Closes facebook#18830 Reviewed By: achen1 Differential Revision: D7612904 Pulled By: mdvacca fbshipit-source-id: a04cf47ab80e0e813fa043125b1f907e212b1ad4
Summary: /cc janicduplessis mdvacca This addresses the same issue as #18830 which was reverted since it didn’t handle `removeClippedSubviews` properly. When subview clipping is on, ReactViewGroup keeps track of its own children directly and accounts for the offset introduced by clipped views when calling ViewGroup methods that modify children. Instead of accounting for just clipped children (views with no parent), it should account for any children that aren’t in the ViewGroup which also includes children that are being transitioned. If you look at the ViewGroup source code, [it explicitly retains the view parent until the transition finishes](https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/core/java/android/view/ViewGroup.java#L5034-L5036) which caused the `getParent()` checks to pass, even though those views should be ignored. I added a new `isChildInViewGroup` method that handles both clipped and transitioning views to fix this. I reproduced the [earlier crash](#18830 (comment)) by enabling clipping in [this test app](#18830 (review)) and adding a “clear views” button that resets the state to an empty items array with an animation. - #18830 [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Pull Request resolved: #19775 Differential Revision: D9105838 Pulled By: hramos fbshipit-source-id: 5ccb0957d1f46c36add960c0e4ef2a545cb03cbe
…9775) Summary: /cc janicduplessis mdvacca This addresses the same issue as facebook#18830 which was reverted since it didn’t handle `removeClippedSubviews` properly. When subview clipping is on, ReactViewGroup keeps track of its own children directly and accounts for the offset introduced by clipped views when calling ViewGroup methods that modify children. Instead of accounting for just clipped children (views with no parent), it should account for any children that aren’t in the ViewGroup which also includes children that are being transitioned. If you look at the ViewGroup source code, [it explicitly retains the view parent until the transition finishes](https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/core/java/android/view/ViewGroup.java#L5034-L5036) which caused the `getParent()` checks to pass, even though those views should be ignored. I added a new `isChildInViewGroup` method that handles both clipped and transitioning views to fix this. I reproduced the [earlier crash](facebook#18830 (comment)) by enabling clipping in [this test app](facebook#18830 (review)) and adding a “clear views” button that resets the state to an empty items array with an animation. - facebook#18830 [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Pull Request resolved: facebook#19775 Differential Revision: D9105838 Pulled By: hramos fbshipit-source-id: 5ccb0957d1f46c36add960c0e4ef2a545cb03cbe
…9775) Summary: /cc janicduplessis mdvacca This addresses the same issue as facebook#18830 which was reverted since it didn’t handle `removeClippedSubviews` properly. When subview clipping is on, ReactViewGroup keeps track of its own children directly and accounts for the offset introduced by clipped views when calling ViewGroup methods that modify children. Instead of accounting for just clipped children (views with no parent), it should account for any children that aren’t in the ViewGroup which also includes children that are being transitioned. If you look at the ViewGroup source code, [it explicitly retains the view parent until the transition finishes](https://github.com/aosp-mirror/platform_frameworks_base/blob/oreo-release/core/java/android/view/ViewGroup.java#L5034-L5036) which caused the `getParent()` checks to pass, even though those views should be ignored. I added a new `isChildInViewGroup` method that handles both clipped and transitioning views to fix this. I reproduced the [earlier crash](facebook#18830 (comment)) by enabling clipping in [this test app](facebook#18830 (review)) and adding a “clear views” button that resets the state to an empty items array with an animation. - facebook#18830 [ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases. Pull Request resolved: facebook#19775 Differential Revision: D9105838 Pulled By: hramos fbshipit-source-id: 5ccb0957d1f46c36add960c0e4ef2a545cb03cbe
Fixes issue #11828 that causes layout animations for removed views to
remove some adjacent views as well. This happens because the animated
views are still present in the ViewGroup, which throws off subsequent
operations that rely on view indices having updated.
This issue was addressed in #11962, which was closed in favour of a more
reliable solution that addresses the issue globally since it’s difficult
to account for animated views everywhere. @janicduplessis recommended
handling the issue through ViewManager.
Since API 11, Android provides
ViewGroup#startViewTransition(View)
that can be used to keep child views visible even if they have been
removed from the group. ViewGroup keeps an array of these views, which
is only used for drawing. Methods such as
ViewGroup#getChildCount()
and
ViewGroup#getChildAt(int)
will ignore them.I believe relying on these framework methods within ViewManager is the
most reliable way to solve this issue because it also works if callers
ignore ViewManager and reach into the native view indices and counts
directly.
Test Plan
I wrote a minimal test app that you can find here:
https://gist.github.com/lnikkila/87f3825442a5773f17ead433a810d53f
The expected result is that the red and green squares disappear, a blue
one appears, and the black one stays in place. iOS has this behaviour,
but Android removes the black square as well.
We can see the bug with some breakpoint logging.
Without LayoutAnimation:
With LayoutAnimation tag 3 gets removed when it shouldn’t be:
Here are some GIFs to compare, click to expand:
Current master (iOS vs Android)
With this patch (iOS vs Android, fixed)
Related PRs
Previously addressed in #11962, which wasn’t merged.
Tangentially related to my other LayoutAnimation PR #18651.
No documentation changes needed.
Release Notes
[ANDROID] [BUGFIX] [LayoutAnimation] - Removal LayoutAnimations no longer remove adjacent views as well in certain cases.