Skip to content

Commit

Permalink
Using initialIndex instead of scrollToBottom (disable warning for Tal…
Browse files Browse the repository at this point in the history
…kBack).

The logic was moved to ReactScrollView onLayoutChange.
Try to import some of the functionalities from PR facebook#29466
Use maintainVisibleContentPosition instead of existing functionality in FlatList implementation

PR 29466 facebook#29466
  • Loading branch information
fabOnReact committed Sep 23, 2022
1 parent c98b369 commit 1eea5b9
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 53 deletions.
28 changes: 28 additions & 0 deletions Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,34 @@ export type Props = $ReadOnly<{|
* - `true`, deprecated, use 'always' instead
*/
keyboardShouldPersistTaps?: ?('always' | 'never' | 'handled' | true | false),

/**
* When set, the scroll view will adjust the scroll position so that the first child that is
* currently visible and at or beyond `minIndexForVisible` will not change position. This is
* useful for lists that are loading content in both directions, e.g. a chat thread, where new
* messages coming in might otherwise cause the scroll position to jump. A value of 0 is common,
* but other values such as 1 can be used to skip loading spinners or other content that should
* not maintain position.
*
* The optional `autoscrollToTopThreshold` can be used to make the content automatically scroll
* to the top after making the adjustment if the user was within the threshold of the top before
* the adjustment was made. This is also useful for chat-like applications where you want to see
* new messages scroll into place, but not if the user has scrolled up a ways and it would be
* disruptive to scroll a bunch.
*
* Caveat 1: Reordering elements in the scrollview with this enabled will probably cause
* jumpiness and jank. It can be fixed, but there are currently no plans to do so. For now,
* don't re-order the content of any ScrollViews or Lists that use this feature.
*
* Caveat 2: This simply uses `contentOffset` and `frame.origin` in native code to compute
* visibility. Occlusion, transforms, and other complexity won't be taken into account as to
* whether content is "visible" or not.
*/
maintainVisibleContentPosition?: ?$ReadOnly<{|
minIndexForVisible: number,
autoscrollToTopThreshold?: ?number,
|}>,

/**
* Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop).
*/
Expand Down
28 changes: 0 additions & 28 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -1477,23 +1477,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
// The root cause of this issue may have been
// adding contentLength to state.
// Try to move this to another callback, for example componentDidUpdate
setTimeout(
(flatlist, contentLength, lastItem) => {
if (
contentLength != undefined &&
contentLength >= this._lastScrollPosition
) {
flatlist.scrollToOffset({
offset: contentLength,
animated: false,
});
}
},
1,
this,
this._scrollMetrics.contentLength,
lastItem,
);
this._lastItem = lastItem;
this._lastScrollPosition = this._scrollMetrics.contentLength;
this._hasTriggeredInitialScrollToIndex = true;
Expand All @@ -1515,17 +1498,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
} else {
newBottomHeight = height - this._lastOffsetFromBottomOfScreen;
}
this._scrollToOffsetTimeout = setTimeout(
(flatlist, bottomHeight) => {
flatlist.scrollToOffset({
offset: bottomHeight,
animated: false,
});
},
1,
this,
newBottomHeight,
);
}
this._maybeCallOnEndReached();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
Expand All @@ -47,6 +48,8 @@
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasScrollState;
import com.facebook.react.views.scroll.ReactScrollViewHelper.ReactScrollViewScrollState;
import com.facebook.react.views.view.ReactViewBackgroundManager;
import com.facebook.react.views.view.ReactViewGroup;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.List;

Expand Down Expand Up @@ -109,11 +112,34 @@ public class ReactScrollView extends ScrollView
private PointerEvents mPointerEvents = PointerEvents.AUTO;
private long mLastScrollDispatchTime = 0;
private int mScrollEventThrottle = 0;
private boolean mEnabledTalkbackCompatibleInvertedList = false;
private @Nullable ReactScrollViewMaintainVisibleContentPositionData
mMaintainVisibleContentPositionData;
private @Nullable WeakReference<View> firstVisibleViewForMaintainVisibleContentPosition = null;
private @Nullable Rect prevFirstVisibleFrameForMaintainVisibleContentPosition = null;
private boolean mInitialScrollTriggered = false;

private final Handler mHandler = new Handler();
private final Runnable mComputeFirstVisibleViewRunnable =
new Runnable() {
@Override
public void run() {
computeFirstVisibleItemForMaintainVisibleContentPosition();
}
};

public ReactScrollView(Context context) {
this(context, null);
}

public void setMaintainVisibleContentPosition(
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData) {
mMaintainVisibleContentPositionData = maintainVisibleContentPositionData;
if (maintainVisibleContentPositionData != null) {
computeFirstVisibleItemForMaintainVisibleContentPosition();
}
}

public ReactScrollView(Context context, @Nullable FpsListener fpsListener) {
super(context);
mFpsListener = fpsListener;
Expand Down Expand Up @@ -183,6 +209,11 @@ public void setDisableIntervalMomentum(boolean disableIntervalMomentum) {
mDisableIntervalMomentum = disableIntervalMomentum;
}

public void setEnabledTalkbackCompatibleInvertedList(
boolean enabledTalkbackCompatibleInvertedList) {
mEnabledTalkbackCompatibleInvertedList = enabledTalkbackCompatibleInvertedList;
}

public void setSendMomentumEvents(boolean sendMomentumEvents) {
mSendMomentumEvents = sendMomentumEvents;
}
Expand Down Expand Up @@ -350,6 +381,14 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) {
mOnScrollDispatchHelper.getXFlingVelocity(),
mOnScrollDispatchHelper.getYFlingVelocity());
}

if (mMaintainVisibleContentPositionData != null) {
// We don't want to compute the first visible view everytime onScrollChanged gets called (can
// be multiple times per second).
// The following logic debounces the computation by 100ms (arbitrary value).
mHandler.removeCallbacks(mComputeFirstVisibleViewRunnable);
mHandler.postDelayed(mComputeFirstVisibleViewRunnable, 100);
}
}

@Override
Expand Down Expand Up @@ -1073,8 +1112,83 @@ public void onLayoutChange(

int currentScrollY = getScrollY();
int maxScrollY = getMaxScrollY();
if (currentScrollY > maxScrollY) {
scrollTo(getScrollX(), maxScrollY);
if (mEnabledTalkbackCompatibleInvertedList) {
if (mInitialScrollTriggered) {
scrollMaintainVisibleContentPosition();
} else {
scrollTo(getScrollX(), getMaxScrollY());
mInitialScrollTriggered = true;
}
} else {
if (currentScrollY > maxScrollY) {
scrollTo(getScrollX(), maxScrollY);
}
}
}

/**
* Called when maintainVisibleContentPosition is used and after a scroll. Finds the first
* completely visible view in the ScrollView and stores it for later use.
*/
private void computeFirstVisibleItemForMaintainVisibleContentPosition() {
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData =
mMaintainVisibleContentPositionData;
if (maintainVisibleContentPositionData == null) return;

int currentScrollY = getScrollY();
int minIdx = maintainVisibleContentPositionData.minIndexForVisible;

ReactViewGroup contentView = (ReactViewGroup) mContentView;
if (contentView == null) return;

for (int i = minIdx; i < contentView.getChildCount(); i++) {
// Find the first entirely visible view. This must be done after we update the content offset
// or it will tend to grab rows that were made visible by the shift in position
View child = contentView.getChildAt(i);
if (child.getY() >= currentScrollY || i == contentView.getChildCount() - 1) {
firstVisibleViewForMaintainVisibleContentPosition = new WeakReference<>(child);
Rect frame = new Rect();
child.getHitRect(frame);
prevFirstVisibleFrameForMaintainVisibleContentPosition = frame;
break;
}
}
}

/**
* Called when maintainVisibleContentPosition is used and after a layout change. Detects if the
* layout change impacts the scroll position and corrects it if needed.
*/
private void scrollMaintainVisibleContentPosition() {
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData =
this.mMaintainVisibleContentPositionData;
// if (maintainVisibleContentPositionData == null) return;

int currentScrollY = getScrollY();

View firstVisibleView =
firstVisibleViewForMaintainVisibleContentPosition != null
? firstVisibleViewForMaintainVisibleContentPosition.get()
: null;
if (firstVisibleView == null) return;
Rect prevFirstVisibleFrame = this.prevFirstVisibleFrameForMaintainVisibleContentPosition;
if (prevFirstVisibleFrame == null) return;

Rect newFrame = new Rect();
firstVisibleView.getHitRect(newFrame);
int deltaY = newFrame.top - prevFirstVisibleFrame.top;
if (Math.abs(deltaY) > 1) {
int scrollYTo = getScrollY() + deltaY;

scrollTo(getScrollX(), scrollYTo);

Integer autoScrollThreshold = maintainVisibleContentPositionData.autoScrollToTopThreshold;
if (autoScrollThreshold != null) {
// If the offset WAS within the threshold of the start, animate to the start.
if (currentScrollY - deltaY <= autoScrollThreshold) {
reactSmoothScrollTo(getScrollX(), 0);
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.facebook.react.views.scroll;

import androidx.annotation.Nullable;

public class ReactScrollViewMaintainVisibleContentPositionData {
public final int minIndexForVisible;

public final @Nullable Integer autoScrollToTopThreshold;

ReactScrollViewMaintainVisibleContentPositionData(
int minIndexForVisible, @Nullable Integer autoScrollToTopThreshold) {
this.minIndexForVisible = minIndexForVisible;
this.autoScrollToTopThreshold = autoScrollToTopThreshold;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,29 @@ public void setDisableIntervalMomentum(ReactScrollView view, boolean disbaleInte
view.setDisableIntervalMomentum(disbaleIntervalMomentum);
}

@ReactProp(name = "enabledTalkbackCompatibleInvertedList")
public void setEnabledTalkbackCompatibleInvertedList(
ReactScrollView view, boolean enabledTalkbackCompatibleInvertedList) {
view.setEnabledTalkbackCompatibleInvertedList(true);
}

@ReactProp(name = "maintainVisibleContentPosition")
public void setMaintainVisibleContentPosition(ReactScrollView view, ReadableMap value) {
if (value != null) {
int minIndexForVisible = value.getInt("minIndexForVisible");
Integer autoScrollToTopThreshold =
value.hasKey("autoscrollToTopThreshold")
? value.getInt("autoscrollToTopThreshold")
: null;
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData =
new ReactScrollViewMaintainVisibleContentPositionData(
minIndexForVisible, autoScrollToTopThreshold);
view.setMaintainVisibleContentPosition(maintainVisibleContentPositionData);
} else {
view.setMaintainVisibleContentPosition(null);
}
}

@ReactProp(name = "snapToInterval")
public void setSnapToInterval(ReactScrollView view, float snapToInterval) {
// snapToInterval needs to be exposed as a float because of the Javascript interface.
Expand Down
25 changes: 2 additions & 23 deletions packages/rn-tester/js/examples/FlatList/FlatList-inverted.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,33 +141,12 @@ function NestedFlatList(props) {
/>
<Text>Flatlist</Text>
<FlatList
onScrollToIndexFailed={e => console.log(e)}
// initialScrollIndex is not supported with enableTalkbackCompatibleInvertedList
// initialScrollIndex={5}
ref={ref => {
// $FlowFixMe
flatlist = ref;
maintainVisibleContentPosition={{
minIndexForVisible: 4,
}}
enabledTalkbackCompatibleInvertedList
accessibilityRole="list"
ListFooterComponent={
<Text style={{height: 50, width: 100, backgroundColor: 'yellow'}}>
Footer Component
</Text>
}
ListHeaderComponent={
<Text style={{height: 100, width: 100, backgroundColor: 'yellow'}}>
Header Component
</Text>
}
inverted
renderItem={renderItem}
data={items}
onEndReached={() => {
console.log('TESTING:: ' + 'callback called');
setItems(items => [...items, ...getNewItems(index)]);
setIndex(index => index + 11);
}}
/>
</View>
);
Expand Down

0 comments on commit 1eea5b9

Please sign in to comment.