Skip to content
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

Merged
merged 1 commit into from
Aug 4, 2022

Conversation

janicduplessis
Copy link

@janicduplessis janicduplessis commented Jul 25, 2022

Add android support for maintainVisibleContentPosition

@janicduplessis janicduplessis changed the title Expensify mvcp Android support for maintainVisibleContentPosition v2 Jul 25, 2022
if (prevChild != null && prevChild.getGlobalVisibleRect(new Rect())) {
max = mid;
continue;
int minIndex = mMaintainVisibleContentPositionData.minIndexForVisible;
Copy link
Author

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;

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 :)

Copy link
Author

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;
Copy link

@Julesssss Julesssss Jul 26, 2022

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?

Copy link
Author

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.

Copy link
Author

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

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);

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?

Copy link
Author

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.

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));
Copy link

@Julesssss Julesssss Jul 26, 2022

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.

Copy link
Author

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;

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.

Copy link
Author

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.

Copy link
Author

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();

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.

Copy link
Author

@janicduplessis janicduplessis Jul 26, 2022

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.

@janicduplessis
Copy link
Author

This should work for our use case, but missing a few things to upstream:

  • Support for horizontal scroll view
  • Support autoscrollToTopThreshold

@janicduplessis janicduplessis changed the base branch from Rory-latest to 0.69-stable July 26, 2022 20:05
@janicduplessis
Copy link
Author

Updated to target 0.69-stable

@janicduplessis janicduplessis changed the title Android support for maintainVisibleContentPosition v2 Android support for maintainVisibleContentPosition Jul 27, 2022
@roryabraham
Copy link

roryabraham commented Jul 29, 2022

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 ScrollView components would not scroll. Looking for a pattern now.

Edit: Ignore this ... I was already testing on 0.69-stable so this is a serious fabric bug but not one caused by any of this code

Edit 2: Just needed to add the nestedScrollEnabled prop 🤦 After adding that this is working great!

@roryabraham
Copy link

We'll want to move the example up so that it renders on iOS and Android

Copy link

@roryabraham roryabraham left a 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:

  1. Include the maintainVisibleContentPosition example on Android as well as iOS
  2. Pass the nestedScrollEnabled prop to child scroll views in rn-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);

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.

Copy link
Author

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(
Copy link

@roryabraham roryabraham Jul 30, 2022

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();
});

@roryabraham
Copy link

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.

Copy link

@roryabraham roryabraham 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 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.

@roryabraham roryabraham merged commit fae196a into Expensify:0.69-stable Aug 4, 2022
@roryabraham
Copy link

I renamed the file, tested, and just pushed the change directly to the 0.69-stable branch in 807d6e7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants