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

[DF] feat(#547): add experimentalScrollPositionManagement support to FlashList #824

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
711dd4f
[DF] feat(#547): `experimentalMaintainTopContentPosition` works verti…
friyiajr Feb 10, 2023
f8409a1
[DF] feat(#547): `experimentalMaintainTopContentPosition` works horiz…
friyiajr Feb 23, 2023
8a9d280
[DF] feat(#547): `experimentalMaintainTopContentPosition` prop added …
friyiajr Feb 25, 2023
1b8a62a
[DF] feat(#547): update documentation
friyiajr Feb 25, 2023
9aa5c26
[DF] feat(#547): final cleanups
friyiajr Feb 25, 2023
eed666f
[DF] feat(#547): update podfile.lock
friyiajr Mar 30, 2023
564660b
[DF] feat(#547): `experimentalMaintainTopContentPosition` works verti…
friyiajr Feb 10, 2023
193bf0d
[DF] feat(#547): final cleanups
friyiajr Feb 25, 2023
285063b
[DF] feat(#547): `experimentalMaintainTopContentPosition` works verti…
friyiajr Feb 10, 2023
903ba11
[DF] feat(#547): `experimentalMaintainTopContentPosition` works horiz…
friyiajr Feb 23, 2023
56987b5
[DF] feat(#547): `experimentalMaintainTopContentPosition` prop added …
friyiajr Feb 25, 2023
46599e2
[DF] feat(#547): final cleanups
friyiajr Feb 25, 2023
b9d1f85
[DF] feat(#547): retrieve diff value on android
friyiajr Mar 3, 2023
814d74d
[DF] feat(#547): maintain android list position
friyiajr Mar 29, 2023
07122e9
Fix rebase
friyiajr Mar 30, 2023
bdafd16
Fix Rebase
friyiajr Mar 30, 2023
a3718f2
Fix rebase
friyiajr Mar 30, 2023
f650e56
[DF] feat(#547): clean up kotlin code
friyiajr Apr 6, 2023
a37e431
[DF] feat(#547): cleanup typescript code
friyiajr Apr 6, 2023
6cc3be7
[DF] feat(#547): rename some variables
friyiajr Apr 18, 2023
c044882
[DF] feat(#547): fix kotlin pr requests
friyiajr Aug 16, 2023
788eb40
[DF] feat(#547): fix typescript pr requests
friyiajr Aug 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

- Update types to match `react-native@0.72` view types.
- https://github.com/Shopify/flash-list/pull/890
- Add support for `experimentalMaintainTopContentPosition`
- https://github.com/Shopify/flash-list/issues/547

## [1.5.0] - 2023-07-12

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.shopify.reactnative.flash_list

import android.widget.ScrollView

class AutoLayoutShadow {
var horizontal: Boolean = false
var scrollOffset: Int = 0
Expand All @@ -11,17 +13,25 @@ class AutoLayoutShadow {
var blankOffsetAtEnd = 0 // Tracks blank area from the bottom

var lastMaxBoundOverall = 0 // Tracks where the last pixel is drawn in the overall
var maintainTopContentPosition = false

private var lastMaxBound = 0 // Tracks where the last pixel is drawn in the visible window
private var lastMinBound = 0 // Tracks where first pixel is drawn in the visible window

private var anchorStableId = ""
private var anchorOffset = 0

/** Checks for overlaps or gaps between adjacent items and then applies a correction (Only Grid layouts with varying spans)
* Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on Android side. Not expecting any major perf issue. */
fun clearGapsAndOverlaps(sortedItems: Array<CellContainer>) {
fun clearGapsAndOverlaps(sortedItems: Array<CellContainer>, scrollView: ScrollView) {
var maxBound = 0
var minBound = Int.MAX_VALUE
var maxBoundNeighbour = 0

lastMaxBoundOverall = 0
var nextAnchorStableId = ""
var nextAnchorOffset = 0

for (i in 0 until sortedItems.size - 1) {
val cell = sortedItems[i]
val neighbour = sortedItems[i + 1]
Expand Down Expand Up @@ -74,9 +84,31 @@ class AutoLayoutShadow {
}
}
}
val isAnchorFound = nextAnchorStableId == "" || neighbour.stableId == anchorStableId

if(isAnchorFound) {
nextAnchorOffset = neighbour.top
nextAnchorStableId = neighbour.stableId
}
lastMaxBoundOverall = kotlin.math.max(lastMaxBoundOverall, if (horizontal) cell.right else cell.bottom)
lastMaxBoundOverall = kotlin.math.max(lastMaxBoundOverall, if (horizontal) neighbour.right else neighbour.bottom)
}

if(maintainTopContentPosition) {
for (cell in sortedItems) {
val minValue = cell.top
if (cell.stableId == anchorStableId) {
if (minValue != anchorOffset) {
val diff = minValue - anchorOffset
(scrollView as DoubleSidedScrollView).setShiftOffset(diff.toDouble())
break
}
}
}
}

anchorStableId = nextAnchorStableId
anchorOffset = nextAnchorOffset
lastMaxBound = maxBoundNeighbour
lastMinBound = minBound
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.shopify.reactnative.flash_list

import android.content.Context
import android.graphics.Canvas
import android.util.DisplayMetrics
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
Expand All @@ -21,8 +19,19 @@ class AutoLayoutView(context: Context) : ReactViewGroup(context) {
val alShadow = AutoLayoutShadow()
var enableInstrumentation = false
var disableAutoLayout = false
var pixelDensity = 1.0
var experimentalScrollPositionManagement: Boolean = false
set(value: Boolean) {
alShadow.maintainTopContentPosition = value
field = value
}

var pixelDensity = 1.0;
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
if(experimentalScrollPositionManagement) {
fixLayout()
}
super.onLayout(changed, left, top, right, bottom)
}

/** Overriding draw instead of onLayout. RecyclerListView uses absolute positions for each and every item which means that changes in child layouts may not trigger onLayout on this container. The same layout
* can still cause views to overlap. Therefore, it makes sense to override draw to do correction. */
Expand Down Expand Up @@ -66,7 +75,10 @@ class AutoLayoutView(context: Context) : ReactViewGroup(context) {
}
positionSortedViews.sortBy { it.index }
alShadow.offsetFromStart = if (alShadow.horizontal) left else top
alShadow.clearGapsAndOverlaps(positionSortedViews)
alShadow.clearGapsAndOverlaps(
positionSortedViews,
getParentScrollView() as ScrollView,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ class AutoLayoutViewManager: ReactViewManager() {
view.enableInstrumentation = enableInstrumentation
}

@ReactProp(name = "experimentalScrollPositionManagement")
fun setExperimentalMaintainContentPosition(view: AutoLayoutView, isMaintaining: Boolean) {
view.experimentalScrollPositionManagement = isMaintaining
}

private fun convertToPixelLayout(dp: Double, density: Double): Int {
return (dp * density).roundToInt()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
public interface CellContainer {
void setIndex(int value);
int getIndex();
void setStableId(String value);
String getStableId();
void setLeft(int value);
int getLeft();
void setTop(int value);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.shopify.reactnative.flash_list

import android.content.Context
import android.view.View
import com.facebook.react.views.view.ReactViewGroup

class CellContainerImpl(context: Context) : ReactViewGroup(context), CellContainer {
private var index = -1
private var stableId = ""

override fun setIndex(value: Int) {
index = value
}
Expand All @@ -13,4 +16,11 @@ class CellContainerImpl(context: Context) : ReactViewGroup(context), CellContain
return index
}

override fun setStableId(stableId: String) {
this.stableId = stableId
}

override fun getStableId(): String {
return this.stableId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ class CellContainerManager: ReactViewManager() {
fun setIndex(view: CellContainerImpl, index: Int) {
view.index = index
}

@ReactProp(name = "stableId")
fun setStableId(view: CellContainerImpl, stableId: String) {
view.stableId = stableId
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* This file comes courtsey of steuerbot and their work on react-native-bidirectional-flatlist. Huge thanks for helping
* solve this problem with fling!
* */

package com.shopify.reactnative.flash_list;

import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.OverScroller;

import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;

import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.views.scroll.ReactScrollView;

import java.lang.reflect.Field;

public class DoubleSidedScrollView extends ReactScrollView {

private OverScroller mScroller;
private boolean mTriedToGetScroller;
protected double mShiftHeight = 0;
protected double mShiftOffset = 0;

public DoubleSidedScrollView(Context context) {
super(context, null);
}

public void setShiftHeight(double shiftHeight) {
mShiftHeight = shiftHeight;
Log.d("ScrollView", "set shiftHeight " + shiftHeight);
}

public void setShiftOffset(double shiftOffset) {
mShiftOffset = shiftOffset;
adjustOverscroller();
Log.d("ScrollView", "set shiftOffset " + shiftOffset);
}

protected void adjustOverscroller() {
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
if(mShiftOffset != 0) {
// correct
scrollTo(0, getScrollY() + (int) mShiftOffset);
if(getOverScrollerFromParent() != null && !getOverScrollerFromParent().isFinished()) {

// get current directed velocity from scroller
int direction = getOverScrollerFromParent().getFinalY() - getOverScrollerFromParent().getStartY() > 0 ? 1 : -1;
float velocity = getOverScrollerFromParent().getCurrVelocity() * direction;
// stop and restart animation again
getOverScrollerFromParent().abortAnimation();
mScroller.fling(
getScrollX(), // startX
getScrollY(), // startY
0, // velocityX
(int)velocity, // velocityY
0, // minX
0, // maxX
0, // minY
Integer.MAX_VALUE, // maxY
0, // overX
scrollWindowHeight / 2 // overY
);
ViewCompat.postInvalidateOnAnimation(this);
}
}
mShiftHeight = 0;
mShiftOffset = 0;
}

@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom);
adjustOverscroller();
}

@Nullable
private OverScroller getOverScrollerFromParent() {
if(mTriedToGetScroller) {
return mScroller;
}
mTriedToGetScroller = true;
Field field = null;
try {
field = ReactScrollView.class.getDeclaredField("mScroller");
field.setAccessible(true);
} catch (NoSuchFieldException e) {
FLog.w(
"ScrollView",
"Failed to get mScroller field for ScrollView! "
+ "This app will exhibit the bounce-back scrolling bug :(");
}

if(field != null) {
Object scrollerValue = null;
try {
scrollerValue = field.get(this);
if (scrollerValue instanceof OverScroller) {
mScroller = (OverScroller) scrollerValue;
} else {
FLog.w(
ReactConstants.TAG,
"Failed to cast mScroller field in ScrollView (probably due to OEM changes to AOSP)! "
+ "This app will exhibit the bounce-back scrolling bug :(");
mScroller = null;
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to get mScroller from ScrollView!", e);
}
}
return mScroller;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.shopify.reactnative.flash_list

import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
import com.facebook.react.common.MapBuilder
import com.facebook.react.views.scroll.ReactScrollView
import com.facebook.react.views.scroll.ReactScrollViewManager
import kotlin.math.roundToInt

/** ViewManager for AutoLayoutView - Container for all RecyclerListView children. Automatically removes all gaps and overlaps for GridLayouts with flexible spans.
* Note: This cannot work for masonry layouts i.e, pinterest like layout */
@ReactModule(name = AutoLayoutViewManager.REACT_CLASS)
class DoubleSidedScrollViewManager: ReactScrollViewManager() {

companion object {
const val REACT_CLASS = "DoubleSidedScrollView"
}

override fun getName(): String {
return REACT_CLASS
}

override fun createViewInstance(context: ThemedReactContext): ReactScrollView {
return DoubleSidedScrollView(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class ReactNativeFlashListPackage : ReactPackage {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(
AutoLayoutViewManager(),
CellContainerManager()
CellContainerManager(),
DoubleSidedScrollViewManager()
)
}
}
4 changes: 2 additions & 2 deletions fixture/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -597,11 +597,11 @@ SPEC CHECKSUMS:
Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541
FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
glog: 476ee3e89abb49e07f822b48323c51c57124b572
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
RCTRequired: 0f06b6068f530932d10e1a01a5352fad4eaacb74
RCTTypeSafety: b0ee81f10ef1b7d977605a2b266823dabd565e65
React: 3becd12bd51ea8a43bdde7e09d0f40fba7820e03
Expand Down
1 change: 1 addition & 0 deletions fixture/src/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const List = () => {
renderItem={renderItem}
estimatedItemSize={100}
data={data}
experimentalScrollPositionManagement
/>
);
};
Expand Down
Loading
Loading