fix(workspace): rework infinite scrolling without wrap state (#7413)

Infinite scrolling kept a wrap state (target page, saved scroll bounds,
a cached feed flag) that every path out of a swipe had to tear down.
Paths that missed it caused the bugs: a short or backward swipe on the
last page wrapped anyway, the first page flashed when a layout pass
reset the bounds mid-wrap, touching during a wrap jumped to the target,
two-panel layouts got stuck between pages, and the page indicator lost
its active dot.

The wrap now follows from the scroll position alone: on an end that
wraps, the scroll may run one screen further, and the screen from the
other end is translated into that gap. Workspace decides which ends
wrap; other paged views behave as in AOSP.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
x-JOCandClaude Opus 5.5 authored and GitHub committed 2026-10-21 19:41:57 +08:00
1 parent d9afe890a3
commit 6c2c2c22ef
2 files changed
+165 -290

No files matched your search

+155 -290
View File
@@ -52,7 +52,6 @@ import android.widget.ScrollView;
import androidx.annotation.Nullable;
import app.lawnchair.preferences2.PreferenceManager2;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.pageindicators.PageIndicator;
@@ -63,11 +62,9 @@ import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ActivityContext;
import app.lawnchair.preferences2.PreferenceCacheExtensionsKt;
import java.util.ArrayList;
import java.util.function.Consumer;
import app.lawnchair.preferences.PreferenceManager;
import app.lawnchair.ui.StretchEdgeEffect;
/**
@@ -88,9 +85,6 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f;
private static final float MAX_SCROLL_PROGRESS = 1.0f;
private final PreferenceManager prefs = PreferenceManager.getInstance(getContext());
private final PreferenceManager2 prefs2 = PreferenceManager2.getInstance(getContext());
private boolean mFreeScroll = false;
@@ -135,15 +129,8 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
@Nullable protected int[] mPageScrolls = null;
private boolean mIsBeingDragged;
// LC-Note: Track temporary page translation and bounds for animated infinite scrolling.
private int mWrapToPage = INVALID_PAGE;
private int mSavedMinScroll;
private int mSavedMaxScroll;
private boolean mCachedEnableFeed;
private boolean isWrapScrolling() {
return mWrapToPage != INVALID_PAGE;
}
// LC-Note: Whether the end pages still carry a wrap translation (see updateWrapTranslation).
private boolean mWrapTranslated;
// The amount of movement to begin scrolling
protected int mTouchSlop;
@@ -289,10 +276,6 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
private void abortScrollerAnimation(boolean resetNextPage) {
mScroller.abortAnimation();
// LC-Note: Restore temporary infinite-scroll state when AOSP aborts an animation.
if (isWrapScrolling()) {
finalizeWrapScroll();
}
onScrollerAnimationAborted();
// We need to clean up the next page here to avoid computeScrollHelper from
// updating current page on the pass.
@@ -308,10 +291,6 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
*/
public void forceFinishScroller() {
mScroller.forceFinished(true);
// LC-Note: Restore temporary infinite-scroll state when AOSP finishes an animation.
if (isWrapScrolling()) {
finalizeWrapScroll();
}
// We need to clean up the next page here to avoid computeScrollHelper from
// updating current page on the pass.
mNextPage = INVALID_PAGE;
@@ -555,11 +534,15 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
@Override
public void scrollTo(int x, int y) {
// LC-Note: Infinite scrolling lets the scroll run one screen past the ends that wrap.
int minScroll = getMinScrollWithWrap();
int maxScroll = getMaxScrollWithWrap();
x = Utilities.boundToRange(x,
mOrientationHandler.getPrimaryValue(mMinScroll, 0), mMaxScroll);
mOrientationHandler.getPrimaryValue(minScroll, 0), maxScroll);
y = Utilities.boundToRange(y,
mOrientationHandler.getPrimaryValue(0, mMinScroll), mMaxScroll);
mOrientationHandler.getPrimaryValue(0, minScroll), maxScroll);
super.scrollTo(x, y);
updateWrapTranslation();
}
private void sendScrollAccessibilityEvent() {
@@ -592,13 +575,15 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO, mScroller.getCurrX());
}
// LC-Note: Wrap animations extend the scroll bounds and must not trigger edge effects.
if (mAllowOverScroll && !isWrapScrolling()) {
if (newPos < mMinScroll && oldPos >= mMinScroll) {
if (mAllowOverScroll) {
// LC-Note: Wrapping past an end is not an overscroll.
int minScroll = getMinScrollWithWrap();
int maxScroll = getMaxScrollWithWrap();
if (newPos < minScroll && oldPos >= minScroll) {
mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
abortScrollerAnimation(false);
onEdgeAbsorbingScroll();
} else if (newPos > mMaxScroll && oldPos <= mMaxScroll) {
} else if (newPos > maxScroll && oldPos <= maxScroll) {
mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
abortScrollerAnimation(false);
onEdgeAbsorbingScroll();
@@ -621,6 +606,12 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
mCurrentPage = validateNewPage(mNextPage);
mCurrentScrollOverPage = mCurrentPage;
mNextPage = INVALID_PAGE;
// LC-Note: A finished wrap rests on the shifted copy of the page; move to the real
// page, which looks the same.
if (getWrapOverscroll() != 0) {
mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO,
getScrollForPage(mCurrentPage));
}
notifyPageSwitchListener(prevPage);
// We don't want to trigger a page end moving unless the page has settled
@@ -1213,7 +1204,7 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
protected float getScrollProgress(int screenCenter, View v, int page) {
final int halfScreenSize = getMeasuredWidth() / 2;
// LC-Note: Account for wrap translation in page effects and layout offsets.
// LC-Note: Use where the page is drawn, including its wrap shift.
int delta = screenCenter - (getVisualScrollForPage(page) + halfScreenSize);
int panelCount = getPanelCount();
int pageCount = getChildCount();
@@ -1240,27 +1231,14 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
public int getScrollForPage(int index) {
if (!isPageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
return 0;
} else {
return mPageScrolls[index];
}
return mPageScrolls[index];
}
/**
* LC-Note: Returns the visual scroll position for a page, accounting for wrap-scroll
* translation.
* Only used by getScrollProgress so that alpha/scroll-progress calculations see the
* correct on-screen location of the wrap target.
*/
// LC-Note: Scroll of the page where it is drawn, including its wrap shift.
private int getVisualScrollForPage(int index) {
int scroll = getScrollForPage(index);
if (isWrapScrolling() && index == mWrapToPage) {
int totalWidth = mSavedMaxScroll - mSavedMinScroll + getOnePageDistance();
if ((mWrapToPage == 0) != mIsRtl) {
scroll += totalWidth;
} else {
scroll -= totalWidth;
}
}
return scroll;
return getScrollForPage(index) + getWrapOffset(index);
}
// While layout transitions are occurring, a child's position may stray from its baseline
@@ -1273,12 +1251,8 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
int scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft();
int baselineX = mPageScrolls[index] + scrollOffset;
float childX = child.getX();
// LC-Note: Wrap translation isn't a layout transition; exclude it from the offset.
if (isWrapScrolling() && index == mWrapToPage) {
childX -= child.getTranslationX();
}
return (int) (childX - baselineX);
// LC-Note: A wrap shift isn't a layout transition; leave it out.
return (int) (child.getX() - getWrapOffset(index) - baselineX);
}
}
@@ -1309,7 +1283,6 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
@Override
public boolean onTouchEvent(MotionEvent ev) {
// LC-Note: Animate infinite-scroll wrapping across drag, release, and cancellation.
// Skip touch handling if there are no pages to swipe
if (getChildCount() <= 0) return false;
@@ -1326,7 +1299,9 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
abortScrollerAnimation(false);
// LC-Note: A caught wrap keeps its starting page, so the drag goes on from
// where the pages are instead of jumping to the target.
abortScrollerAnimation(getWrapOverscroll() != 0);
}
// Remember where the motion event started
@@ -1337,7 +1312,6 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
mTotalMotion = 0;
mAllowEasyFling = false;
mActivePointerId = ev.getPointerId(0);
mCachedEnableFeed = PreferenceCacheExtensionsKt.firstCached(prefs2.getEnableFeed());
if (mIsBeingDragged) {
pageBeginTransition();
}
@@ -1389,33 +1363,18 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
mLastMotion = direction;
if (delta != 0) {
if (prefs.getInfiniteScrolling().get() && !mFreeScroll && getChildCount() > 1) {
boolean enableFeed = mCachedEnableFeed;
float pulledTo = oldScroll + delta;
if (!isWrapScrolling() && mCurrentPage == getChildCount() - 1) {
boolean pastEnd = mIsRtl ? pulledTo < mMinScroll : pulledTo > mMaxScroll;
if (pastEnd) {
startWrapDrag(0);
}
} else if (!isWrapScrolling() && mCurrentPage == 0 && !enableFeed) {
boolean pastStart = mIsRtl ? pulledTo > mMaxScroll : pulledTo < mMinScroll;
if (pastStart) {
startWrapDrag(getChildCount() - 1);
}
}
}
mOrientationHandler.setPrimary(this, VIEW_SCROLL_BY, delta);
if (mAllowOverScroll && !isWrapScrolling()) {
if (mAllowOverScroll) {
final float pulledToX = oldScroll + delta;
if (pulledToX < mMinScroll) {
// LC-Note: Wrapping past an end is not an overscroll.
if (pulledToX < getMinScrollWithWrap()) {
mEdgeGlowLeft.onPullDistance(-delta / size, 1.f - displacement, ev);
if (!mEdgeGlowRight.isFinished()) {
mEdgeGlowRight.onRelease(ev);
}
} else if (pulledToX > mMaxScroll) {
} else if (pulledToX > getMaxScrollWithWrap()) {
mEdgeGlowRight.onPullDistance(delta / size, displacement, ev);
if (!mEdgeGlowLeft.isFinished()) {
mEdgeGlowLeft.onRelease(ev);
@@ -1484,52 +1443,30 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
// test for a large move if a fling has been registered. That is, a large
// move to the left and fling to the right will register as a fling to the right.
boolean infiniteScroll = prefs.getInfiniteScrolling().get();
boolean enableFeed = mCachedEnableFeed;
// LC-Note: Past the first or last page, a fling onward or a significant drag
// wraps around; anything else falls through and settles back. A finger
// moving left raises the scroll.
int wrapOverscroll = getWrapOverscroll();
boolean wrap = wrapOverscroll != 0 && !returnToOriginalPage && (isFling
? (velocity < 0) == (wrapOverscroll > 0)
: isSignificantMove(Math.abs(wrapOverscroll), pageOrientedSize));
if (((isSignificantMove && !isDeltaLeft && !isFling) ||
if (wrap) {
snapToWrappedPage(wrapOverscroll, velocity);
} else if (((isSignificantMove && !isDeltaLeft && !isFling) ||
(isFling && !isVelocityLeft)) && mCurrentPage > 0) {
if (isWrapScrolling()) {
cancelWrapScroll();
}
finalPage = returnToOriginalPage
? mCurrentPage : mCurrentPage - getPanelCount();
runOnPageScrollsInitialized(
() -> snapToPageWithVelocity(finalPage, velocity));
} else if (((isSignificantMove && isDeltaLeft && !isFling) || (isFling && isVelocityLeft)) && mCurrentPage < getChildCount() - 1) {
if (isWrapScrolling()) {
cancelWrapScroll();
}
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + getPanelCount();
runOnPageScrollsInitialized(() -> snapToPageWithVelocity(finalPage, velocity));
} else if (mCurrentPage == getChildCount() - 1 && infiniteScroll) {
finalPage = returnToOriginalPage ? mCurrentPage : 0;
if (!returnToOriginalPage) {
runOnPageScrollsInitialized(
() -> snapToPageWrapped(finalPage, velocity));
} else {
if (isWrapScrolling()) {
cancelWrapScroll();
}
runOnPageScrollsInitialized(
() -> snapToPageWithVelocity(finalPage, velocity));
}
} else if (mCurrentPage == 0 && infiniteScroll && !enableFeed) {
finalPage = returnToOriginalPage ? mCurrentPage : getChildCount() - 1;
if (!returnToOriginalPage) {
runOnPageScrollsInitialized(
() -> snapToPageWrapped(finalPage, velocity));
} else {
if (isWrapScrolling()) {
cancelWrapScroll();
}
runOnPageScrollsInitialized(
() -> snapToPageWithVelocity(finalPage, velocity));
}
} else if (((isSignificantMove && isDeltaLeft && !isFling) ||
(isFling && isVelocityLeft)) &&
mCurrentPage < getChildCount() - 1) {
finalPage = returnToOriginalPage
? mCurrentPage : mCurrentPage + getPanelCount();
runOnPageScrollsInitialized(
() -> snapToPageWithVelocity(finalPage, velocity));
} else {
if (isWrapScrolling()) {
cancelWrapScroll();
}
runOnPageScrollsInitialized(this::snapToDestination);
}
} else {
@@ -1579,9 +1516,6 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
if (isWrapScrolling()) {
cancelWrapScroll();
}
runOnPageScrollsInitialized(this::snapToDestination);
}
mEdgeGlowLeft.onRelease(ev);
@@ -1789,199 +1723,128 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
}
/**
* LC-Note: Initiates a wrap-scroll animation that moves one page distance in the correct
* swipe direction when wrapping between first↔last page.
* LC-Note: Infinite scrolling. On an end that wraps, the scroll may run one screen further;
* while it is out there, the screen from the other end is translated into that gap so the
* pages read as a loop. All of it follows from the scroll position, so there is no wrap state
* to set up or tear down. Off by default; Workspace turns it on.
*/
private void snapToPageWrapped(int targetPage, int velocity) {
if (!mScroller.isFinished()) {
abortScrollerAnimation(false);
}
if (!isWrapScrolling()) {
startWrapDrag(targetPage);
}
if (!isWrapScrolling()) {
return;
}
int onePageDistance = getOnePageDistance();
if (onePageDistance == 0) {
finalizeWrapScroll();
return;
}
int currentScroll = mOrientationHandler.getPrimaryScroll(this);
// LC-Note: In LTR, page 0 wraps to the right beyond maxScroll.
// In RTL: directions are reversed.
int virtualTarget = ((targetPage == 0) != mIsRtl)
? mSavedMaxScroll + onePageDistance
: mSavedMinScroll - onePageDistance;
int delta = virtualTarget - currentScroll;
int duration = computeSnapDuration(delta, velocity);
mNextPage = validateNewPage(targetPage);
pageBeginTransition();
mScroller.startScroll(currentScroll, 0, delta, 0, duration);
updatePageIndicator();
invalidate();
protected boolean canWrapPastEnd(boolean maxEnd) {
return false;
}
/**
* LC-Note: Resets wrap-scroll state: clears translation, restores scroll bounds, and jumps
* scroll position to the target page's real location.
*/
private void finalizeWrapScroll() {
if (!isWrapScrolling()) {
return;
}
int targetPage = mWrapToPage;
mWrapToPage = INVALID_PAGE;
View targetView = getPageAt(targetPage);
if (targetView != null) {
targetView.setTranslationX(0);
}
mMinScroll = mSavedMinScroll;
mMaxScroll = mSavedMaxScroll;
int targetScroll = getScrollForPage(targetPage);
mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO, targetScroll);
// LC-Note: The page at the max or the min scroll end; in RTL page 0 sits at the max end.
protected int getPageAtEnd(boolean maxEnd) {
return maxEnd != mIsRtl ? getChildCount() - 1 : 0;
}
/**
* LC-Note: Cancels wrap-scroll state without jumping scroll position.
* Used when the user drags back or the gesture is cancelled, so the subsequent
* snapToPage animates from the current drag position instead of jumping first.
*/
private void cancelWrapScroll() {
if (!isWrapScrolling()) {
return;
}
View targetView = getPageAt(mWrapToPage);
if (targetView != null) {
targetView.setTranslationX(0);
}
mWrapToPage = INVALID_PAGE;
int clamped = Utilities.boundToRange(
mOrientationHandler.getPrimaryScroll(this), mSavedMinScroll, mSavedMaxScroll);
mOrientationHandler.setPrimary(this, VIEW_SCROLL_TO, clamped);
mMinScroll = mSavedMinScroll;
mMaxScroll = mSavedMaxScroll;
}
/**
* LC-Note: Returns the scroll value to use for the page indicator during wrap scrolling.
* Maps out-of-bounds scroll values back into the normal range so dots animate correctly.
*/
protected int getScrollForPageIndicator() {
int scroll = getScrollX();
if (!isWrapScrolling() || !isPageScrollsInitialized() || getChildCount() < 2) {
return scroll;
}
int onePageDistance = getOnePageDistance();
if (onePageDistance == 0) {
return scroll;
}
int totalRange = computeMaxScroll() + onePageDistance;
if (scroll > mSavedMaxScroll) {
return scroll - totalRange;
}
if (scroll < mSavedMinScroll) {
return scroll + totalRange;
}
return scroll;
}
/**
* LC-Note: Returns the scroll value to use for wallpaper parallax during wrap scrolling.
* While the page wraps one page distance via translationX, the wallpaper sweeps the full
* scroll range in the opposite direction so it slides continuously from one edge to the
* other (matching the pre-wrap-animation behavior and other launchers like Nova).
*/
public int getScrollForWallpaper() {
int scroll = getScrollX();
if (!isWrapScrolling() || !isPageScrollsInitialized() || getChildCount() < 2) {
return scroll;
}
int onePageDistance = getOnePageDistance();
if (onePageDistance == 0) {
return scroll;
}
int scrollRange = mSavedMaxScroll - mSavedMinScroll;
if (scroll > mSavedMaxScroll) {
float progress = (float) (scroll - mSavedMaxScroll) / onePageDistance;
return Math.round(mSavedMaxScroll - progress * scrollRange);
}
if (scroll < mSavedMinScroll) {
float progress = (float) (mSavedMinScroll - scroll) / onePageDistance;
return Math.round(mSavedMinScroll + progress * scrollRange);
}
return scroll;
}
// LC-Note: Returns 0 on two-panel (foldable) workspaces, disabling wrap scroll.
private int getOnePageDistance() {
if (!isPageScrollsInitialized() || mPageScrolls.length < 2) {
// LC-Note: Distance between neighbouring screens, or 0 when there is nothing to wrap to.
private int getWrapScreenDistance() {
if (mFreeScroll || !isPageScrollsInitialized()) {
return 0;
}
return Math.abs(mPageScrolls[1] - mPageScrolls[0]);
int panelCount = getPanelCount();
if (getChildCount() <= panelCount) {
return 0;
}
return Math.abs(getScrollForPage(panelCount) - getScrollForPage(0));
}
private int getMinScrollWithWrap() {
return canWrapPastEnd(false) ? mMinScroll - getWrapScreenDistance() : mMinScroll;
}
private int getMaxScrollWithWrap() {
return canWrapPastEnd(true) ? mMaxScroll + getWrapScreenDistance() : mMaxScroll;
}
// LC-Note: How far the scroll is past the max end (positive) or the min end (negative).
private int getWrapOverscroll() {
int scroll = mOrientationHandler.getPrimaryScroll(this);
if (scroll > mMaxScroll && getMaxScrollWithWrap() > mMaxScroll) {
return scroll - mMaxScroll;
}
if (scroll < mMinScroll && getMinScrollWithWrap() < mMinScroll) {
return scroll - mMinScroll;
}
return 0;
}
// LC-Note: How far the page is translated into the gap past the other end.
private int getWrapOffset(int page) {
int wrapOverscroll = getWrapOverscroll();
if (wrapOverscroll == 0 || getLeftmostVisiblePageForIndex(page)
!= getLeftmostVisiblePageForIndex(getPageAtEnd(wrapOverscroll < 0))) {
return 0;
}
int period = mMaxScroll - mMinScroll + getWrapScreenDistance();
return wrapOverscroll > 0 ? period : -period;
}
private void updateWrapTranslation() {
boolean wrapping = getWrapOverscroll() != 0;
if (wrapping || mWrapTranslated) {
mWrapTranslated = wrapping;
for (int i = 0; i < getChildCount(); i++) {
getPageAt(i).setTranslationX(getWrapOffset(i));
}
}
}
// LC-Note: Ends a wrap on the translated copy of the page at the other end; once the scroll
// settles, computeScrollHelper moves to the real page.
private void snapToWrappedPage(int wrapOverscroll, int velocity) {
int targetPage = getPageAtEnd(wrapOverscroll < 0);
int delta = getVisualScrollForPage(targetPage)
- mOrientationHandler.getPrimaryScroll(this);
snapToPageWithVelocity(targetPage, delta, velocity);
}
/**
* LC-Note: Scroll for the page indicator. Past an end it shows whichever of the two pages
* there is more of, so the active dot never leaves the row.
*/
protected int getScrollForPageIndicator() {
int wrapOverscroll = getWrapOverscroll();
if (wrapOverscroll == 0) {
return getScrollX();
}
boolean pastHalf = Math.abs(wrapOverscroll) > getWrapScreenDistance() / 2;
return (wrapOverscroll > 0) == pastHalf ? mMinScroll : mMaxScroll;
}
/**
* LC-Note: Scroll for the wallpaper. While the pages wrap by one screen, the wallpaper sweeps
* the whole range back to the other end, as it did before the wrap animation (and as in Nova).
*/
public int getScrollForWallpaper() {
int wrapOverscroll = getWrapOverscroll();
if (wrapOverscroll == 0) {
return getScrollX();
}
float progress = (float) wrapOverscroll / getWrapScreenDistance();
int end = wrapOverscroll > 0 ? mMaxScroll : mMinScroll;
return Math.round(end - progress * (mMaxScroll - mMinScroll));
}
// LC-Note: Share AOSP snap timing with the animated infinite-scroll helpers below.
protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
whichPage = validateNewPage(whichPage);
final int newLoc = getScrollForPage(whichPage);
int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);
// LC-Note: The rest takes the delta so a wrap can snap onto the translated copy of a page.
return snapToPageWithVelocity(whichPage, delta, velocity);
}
private boolean snapToPageWithVelocity(int whichPage, int delta, int velocity) {
int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;
int duration = 0;
if (Math.abs(velocity) < mMinFlingVelocity) {
// If the velocity is low enough, then treat this more as an automatic page advance
// as opposed to an apparent physical response to flinging
return snapToPage(whichPage, getSnapAnimationDuration());
return snapToPage(whichPage, delta, getSnapAnimationDuration());
}
duration = computeSnapDuration(delta, velocity);
return snapToPage(whichPage, delta, duration);
}
/**
* LC-Note: Begins a wrap-drag by translating the target page's View next to the current page
* and extending scroll bounds so scrollTo doesn't clamp.
*/
private void startWrapDrag(int targetPage) {
if (!isPageScrollsInitialized() || getChildCount() < 2 || isWrapScrolling()) {
return;
}
int onePageDistance = getOnePageDistance();
if (onePageDistance == 0) {
return;
}
View targetView = getPageAt(targetPage);
if (targetView == null) {
return;
}
int totalWidth = mMaxScroll - mMinScroll + onePageDistance;
mSavedMinScroll = mMinScroll;
mSavedMaxScroll = mMaxScroll;
mWrapToPage = targetPage;
// LC-Note: In LTR, page 0 is leftmost. Wrapping from the last page places it
// to the right of the last page (positive translation).
// In RTL: page 0 is rightmost, so wrapping to page 0 means placing it to the left.
if ((targetPage == 0) != mIsRtl) {
targetView.setTranslationX(totalWidth);
mMaxScroll = mSavedMaxScroll + onePageDistance;
} else {
targetView.setTranslationX(-totalWidth);
mMinScroll = mSavedMinScroll - onePageDistance;
}
}
private int computeSnapDuration(int delta, int velocity) {
int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;
// Here we compute a "distance" that will be used in the computation of the overall
// snap duration. This is a function of the actual distance that needs to be traveled;
// we keep this value close to half screen size in order to reduce the variance in snap
@@ -1996,7 +1859,9 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
// we want the page's snap velocity to approximately match the velocity at which the
// user flings, so we scale the duration by a value near to the derivative of the scroll
// interpolator at zero, ie. 5. We use 4 to make it a little slower.
return 4 * Math.round(1000 * Math.abs(distance / velocity));
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
return snapToPage(whichPage, delta, duration);
}
protected int getSnapAnimationDuration() {
+10
View File
@@ -1514,6 +1514,16 @@ public class Workspace<T extends View & PageIndicator> extends PagedView<T>
}
}
@Override
protected boolean canWrapPastEnd(boolean maxEnd) {
if (!mPreferenceManger.getInfiniteScrolling().get()) {
return false;
}
// LC-Note: The side before the first page belongs to the feed while it is on.
return getPageAtEnd(maxEnd) != 0
|| !PreferenceCacheExtensionsKt.firstCached(mPreferenceManager2.getEnableFeed());
}
@Override
protected boolean shouldFlingForVelocity(int velocityX) {
// When the overlay is moving, the fling or settle transition is controlled by the overlay.