-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add maintainVisibleContentPosition prop for android scroll view
- Loading branch information
1 parent
950ea91
commit c069ae3
Showing
8 changed files
with
322 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
...id/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
/* | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
package com.facebook.react.views.scroll; | ||
|
||
import android.graphics.Rect; | ||
import android.view.View; | ||
import android.view.ViewGroup; | ||
|
||
import androidx.annotation.Nullable; | ||
|
||
import com.facebook.infer.annotation.Assertions; | ||
import com.facebook.react.bridge.ReactContext; | ||
import com.facebook.react.bridge.ReadableMap; | ||
import com.facebook.react.bridge.UIManager; | ||
import com.facebook.react.bridge.UIManagerListener; | ||
import com.facebook.react.bridge.UiThreadUtil; | ||
import com.facebook.react.uimanager.UIManagerHelper; | ||
import com.facebook.react.uimanager.common.ViewUtil; | ||
import com.facebook.react.views.view.ReactViewGroup; | ||
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasSmoothScroll; | ||
|
||
import java.lang.ref.WeakReference; | ||
|
||
/** | ||
* Manage state for the maintainVisibleContentPosition prop. | ||
* | ||
* This uses UIManager to listen to updates and capture position of items before and after layout. | ||
*/ | ||
public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & HasSmoothScroll> implements UIManagerListener { | ||
private final ScrollViewT mScrollView; | ||
private final boolean mHorizontal; | ||
private @Nullable Config mConfig; | ||
private @Nullable WeakReference<View> mFirstVisibleView = null; | ||
private @Nullable Rect mPrevFirstVisibleFrame = null; | ||
private boolean mListening = false; | ||
|
||
public static class Config { | ||
public final int minIndexForVisible; | ||
public final @Nullable Integer autoScrollToTopThreshold; | ||
|
||
Config(int minIndexForVisible, @Nullable Integer autoScrollToTopThreshold) { | ||
this.minIndexForVisible = minIndexForVisible; | ||
this.autoScrollToTopThreshold = autoScrollToTopThreshold; | ||
} | ||
|
||
static Config fromReadableMap(ReadableMap value) { | ||
int minIndexForVisible = value.getInt("minIndexForVisible"); | ||
Integer autoScrollToTopThreshold = | ||
value.hasKey("autoscrollToTopThreshold") | ||
? value.getInt("autoscrollToTopThreshold") | ||
: null; | ||
return new Config(minIndexForVisible, autoScrollToTopThreshold); | ||
} | ||
} | ||
|
||
public MaintainVisibleScrollPositionHelper(ScrollViewT scrollView, boolean horizontal) { | ||
mScrollView = scrollView; | ||
mHorizontal = horizontal; | ||
} | ||
|
||
public void setConfig(@Nullable Config config) { | ||
mConfig = config; | ||
} | ||
|
||
/** | ||
* Start listening to view hierarchy updates. Should be called when this is created. | ||
*/ | ||
public void start() { | ||
if (mListening) { | ||
return; | ||
} | ||
mListening = true; | ||
getUIManagerModule().addUIManagerEventListener(this); | ||
} | ||
|
||
/** | ||
* Stop listening to view hierarchy updates. Should be called before this is destroyed. | ||
*/ | ||
public void stop() { | ||
if (!mListening) { | ||
return; | ||
} | ||
mListening = false; | ||
getUIManagerModule().removeUIManagerEventListener(this); | ||
} | ||
|
||
/** | ||
* Update the scroll position of the managed ScrollView. This should be called after layout | ||
* has been updated. | ||
*/ | ||
public void updateScrollPosition() { | ||
if (mConfig == null | ||
|| mFirstVisibleView == null | ||
|| mPrevFirstVisibleFrame == null) { | ||
return; | ||
} | ||
|
||
View firstVisibleView = mFirstVisibleView.get(); | ||
Rect newFrame = new Rect(); | ||
firstVisibleView.getHitRect(newFrame); | ||
|
||
if (mHorizontal) { | ||
int deltaX = newFrame.left - mPrevFirstVisibleFrame.left; | ||
if (deltaX != 0) { | ||
int scrollX = mScrollView.getScrollX(); | ||
mScrollView.scrollTo(scrollX + deltaX, mScrollView.getScrollY()); | ||
mPrevFirstVisibleFrame = newFrame; | ||
if (mConfig.autoScrollToTopThreshold != null && scrollX <= mConfig.autoScrollToTopThreshold) { | ||
mScrollView.reactSmoothScrollTo(0, mScrollView.getScrollY()); | ||
} | ||
} | ||
} else { | ||
int deltaY = newFrame.top - mPrevFirstVisibleFrame.top; | ||
if (deltaY != 0) { | ||
int scrollY = mScrollView.getScrollY(); | ||
mScrollView.scrollTo(mScrollView.getScrollX(), scrollY + deltaY); | ||
mPrevFirstVisibleFrame = newFrame; | ||
if (mConfig.autoScrollToTopThreshold != null && scrollY <= mConfig.autoScrollToTopThreshold) { | ||
mScrollView.reactSmoothScrollTo(mScrollView.getScrollX(), 0); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private @Nullable ReactViewGroup getContentView() { | ||
return (ReactViewGroup) mScrollView.getChildAt(0); | ||
} | ||
|
||
private UIManager getUIManagerModule() { | ||
return Assertions.assertNotNull( | ||
UIManagerHelper.getUIManager( | ||
(ReactContext) mScrollView.getContext(), | ||
ViewUtil.getUIManagerType(mScrollView.getId()))); | ||
} | ||
|
||
private void computeTargetView() { | ||
if (mConfig == null) { | ||
return; | ||
} | ||
ReactViewGroup contentView = getContentView(); | ||
if (contentView == null) { | ||
return; | ||
} | ||
|
||
int currentScroll = mHorizontal ? mScrollView.getScrollX() : mScrollView.getScrollY(); | ||
for (int i = mConfig.minIndexForVisible; i < contentView.getChildCount(); i++) { | ||
View child = contentView.getChildAt(i); | ||
float position = mHorizontal ? child.getX() : child.getY(); | ||
if (position > currentScroll || i == contentView.getChildCount() - 1) { | ||
mFirstVisibleView = new WeakReference<>(child); | ||
Rect frame = new Rect(); | ||
child.getHitRect(frame); | ||
mPrevFirstVisibleFrame = frame; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
// UIManagerListener | ||
|
||
@Override | ||
public void willDispatchViewUpdates(final UIManager uiManager) { | ||
UiThreadUtil.runOnUiThread( | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
computeTargetView(); | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public void didDispatchMountItems(UIManager uiManager) { | ||
// noop | ||
} | ||
|
||
@Override | ||
public void didScheduleMountItems(UIManager uiManager) { | ||
// noop | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.