Skip to content

Commit

Permalink
Try to import some of the functionalities from PR facebook#29466
Browse files Browse the repository at this point in the history
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 4ba2671
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 48 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
24 changes: 6 additions & 18 deletions packages/rn-tester/js/examples/FlatList/FlatList-inverted.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function NestedFlatList(props) {
const [items, setItems] = useState(DATA);
const [disabled, setDisabled] = useState(false);
const [index, setIndex] = useState(DATA.length + 1);
const [minIndex, setMinIndex] = useState(DATA.length - 1);
const [counter, setCounter] = useState(0);
const getNewItems = startIndex => {
let newItems = [];
Expand Down Expand Up @@ -141,33 +142,20 @@ 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: minIndex,
}}
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
enabledTalkbackCompatibleInvertedList
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 4ba2671

Please sign in to comment.