-
Notifications
You must be signed in to change notification settings - Fork 20
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
Android support for maintainVisibleContentPosition #14
Android support for maintainVisibleContentPosition #14
Conversation
if (prevChild != null && prevChild.getGlobalVisibleRect(new Rect())) { | ||
max = mid; | ||
continue; | ||
int minIndex = mMaintainVisibleContentPositionData.minIndexForVisible; |
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.
I had some issues with binary search infinite looping so I instead of debugging it I just ported the iOS logic pretty much line for line. I think this should be ok.
@@ -20,6 +20,7 @@ | |||
import android.graphics.Rect; | |||
import android.graphics.drawable.ColorDrawable; | |||
import android.graphics.drawable.Drawable; | |||
import android.util.Log; |
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.
I guess this is still WIP, but leaving a note that we should remove this :)
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.
👍
@@ -103,6 +114,9 @@ public class ReactScrollView extends ScrollView | |||
new ReactScrollViewScrollState(ViewCompat.LAYOUT_DIRECTION_LTR); | |||
private final ValueAnimator DEFAULT_FLING_ANIMATOR = ObjectAnimator.ofInt(this, "scrollY", 0, 0); | |||
private PointerEvents mPointerEvents = PointerEvents.AUTO; | |||
private @Nullable ReactScrollViewMaintainVisibleContentPositionData mMaintainVisibleContentPositionData; |
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.
Any reason to not initialize null?
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.
This is correct, this holds the value of the maintainVisibleContent prop, which is null by default.
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.
Oh I read this wrong, by default it will be initialized to null, so it isn’t needed, although we could add it to be more explicit
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.
Not a big deal either way, might look a bit more consistent though.
@@ -228,6 +242,16 @@ public void setOverflow(String overflow) { | |||
invalidate(); | |||
} | |||
|
|||
public void setMaintainVisibleContentPosition(ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData) { | |||
if (maintainVisibleContentPositionData != null && mMaintainVisibleContentPositionData == null) { | |||
getUIManagerModule().addUIManagerEventListener(this); |
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.
Just to clarify, we're using the value of maintainVisibleContentPositionData
to register/unregister the listener? I'm curious why this is preferred instead of tying it to the class/lifecycle? (i.e class initialisation).
Is this because the invoked callback is expensive?
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.
We want to avoid the overhead of tge listener if the prop is not set, iOS had this logic so that is mainly why I added it too.
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.
Ah I see, that makes sense 👍
@@ -991,6 +1015,80 @@ private void setPendingContentOffsets(int x, int y) { | |||
} | |||
} | |||
|
|||
private UIManagerModule getUIManagerModule() { | |||
ReactContext reactContext = UIManagerHelper.getReactContext(this); | |||
return Assertions.assertNotNull(reactContext.getNativeModule(UIManagerModule.class)); |
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.
I'm not familiar with ReactSoftExceptionLogger
, so wanted to check that our assertion is treated as soft exception, instead of crashing the app?
This logs an assertion with ReactSoftExceptionLogger, which decides whether or not to actually throw.
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.
Hard crash should be fine as I think it can never be null when this is called while setting props.
return; | ||
} | ||
|
||
ReactViewGroup contentView = (ReactViewGroup) mContentView; |
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.
Again a minor point. But I wonder if we could improve this by casting once and holding a reference to ReactViewGroup
, instead of casting every time we compute.
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.
I might be able to change mContentView to ReactViewGroup
. Also wanna double check if the cast is actually needed.
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.
I think I would rather keep the cast here, to avoid having to change mContentView and having to cast it somewhere else. Just to keep the diff as small as possible. I don't the cast should really have any impact in this case.
@@ -1011,6 +1109,8 @@ public void onLayoutChange( | |||
return; | |||
} | |||
|
|||
updateScrollPositionForMaintainVisibleContentPosition(); |
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.
Asking mainly for my understanding, but I wanted to check that the following lines should be invoked if updateScrollPositionForMaintainVisibleContentPosition
has already initiated a scrollTo event.
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.
Yes, there are cases when removing views where even after adjustments we want to do the other adjustment (in the following lines) to make sure scroll position is not outside of the content view. Since we call getScrollY
again it should already use the updated value from updateScrollPositionForMaintainVisibleContentPosition
.
Of course in this case the scroll position is no longer maintained, but this is correct behavior. Basically whenever there's less content than the scroll view height, position is not maintained.
This should work for our use case, but missing a few things to upstream:
|
113f2f5
to
c4b4a83
Compare
Updated to target 0.69-stable |
So without diving too much into the code – I tested this on a physical Pixel 4a with Fabric enabled, and found that many of the vertical Edit: Ignore this ... I was already testing on Edit 2: Just needed to add the |
We'll want to move the example up so that it renders on iOS and Android |
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.
Overall works great, just a few things we should do before we merge it:
- Include the
maintainVisibleContentPosition
example on Android as well as iOS - Pass the
nestedScrollEnabled
prop to child scroll views inrn-tester/js/examples/ScrollView.js
as needed
I also think we should work on an implementation of autoScrollToTopThreshold
ASAP, because there's a good chance we might want to use that in our app. We'll need that and horizontal scrolling implemented when we go to upstream, anyways.
for (int i = minIndex; i < contentView.getChildCount(); i++) { | ||
View child = contentView.getChildAt(i); | ||
if (child.getY() > currentScrollY || i == contentView.getChildCount() - 1) { | ||
mFirstVisibleViewForMaintainVisibleContentPosition = new WeakReference<>(child); |
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.
Do you know why a WeakReference is needed here? I included it in my draft basically copying from an existing draft implementation in an open RN PR, but @Julesssss and I weren't sure if it was actually needed.
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.
Not sure, I also kept it because it was there in your implementation. I think this is good to avoid retaining the first visible view in the case it gets removed from the scrollview.
|
||
@Override | ||
public void willDispatchViewUpdates(final UIManager uiManager) { | ||
UiThreadUtil.runOnUiThread( |
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.
NAB but I think we could use a lambda here to be a bit more concise:
UiThreadUtil.runOnUiThread(() -> {
if (mContentView == null) {
return;
}
computeTargetViewForMaintainVisibleContentPosition();
});
According to my testing, this unfortunately does not work with Fabric enabled (though, I don't think the iOS implementation does either). No worries here though, that's a problem for our future selves. |
2779082
to
deff396
Compare
deff396
to
5d49268
Compare
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.
This looks and tests great! I don't love how much duplication there is between ReactScrollView
and ReachHorizontalScrollView
, but that's not on us and I won't ask you to DRY this up any further. Nice job creating the MaintainVisibleScrollPositionHelper
.
My only comment is that it should probably be called MaintainVisibleContentPositionHelper
to match the name of the prop. We can always adjust that with a follow-up and in the upstream PR though.
I renamed the file, tested, and just pushed the change directly to the 0.69-stable branch in 807d6e7 |
Add android support for maintainVisibleContentPosition