Skip to content

Commit

Permalink
[AppBarLayout] Save and restore scroll state during scroll range reca…
Browse files Browse the repository at this point in the history
…lculation

When scroll range changes, the current scroll position may not make sense anymore. Therefore we need to save the scroll state and restore it after the scroll range is invalidated. This change reuses and refactors the existing saving instance state logic to support this need. Also adds a flag to denote "fully expanded" state to avoid improper scroll position calculation when views are still being initialized.

PiperOrigin-RevId: 421348135
  • Loading branch information
drchen authored and dsn5ft committed Jan 13, 2022
1 parent ad97f01 commit 698cf9b
Showing 1 changed file with 48 additions and 16 deletions.
64 changes: 48 additions & 16 deletions lib/java/com/google/android/material/appbar/AppBarLayout.java
Expand Up @@ -68,6 +68,7 @@
import androidx.core.view.accessibility.AccessibilityViewCommand;
import androidx.customview.view.AbsSavedState;
import com.google.android.material.animation.AnimationUtils;
import com.google.android.material.appbar.AppBarLayout.BaseBehavior.SavedState;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.MaterialShapeUtils;
Expand Down Expand Up @@ -203,6 +204,8 @@ public interface LiftOnScrollListener {

@Nullable private Drawable statusBarForeground;

private Behavior behavior;

public AppBarLayout(@NonNull Context context) {
this(context, null);
}
Expand Down Expand Up @@ -533,10 +536,20 @@ private boolean hasCollapsibleChild() {
}

private void invalidateScrollRanges() {
// Saves the current scrolling state when we need to recalculate scroll ranges
SavedState savedState = behavior == null || totalScrollRange == INVALID_SCROLL_RANGE
? null : behavior.saveScrollState(AbsSavedState.EMPTY_STATE, this);
// Invalidate the scroll ranges
totalScrollRange = INVALID_SCROLL_RANGE;
downPreScrollRange = INVALID_SCROLL_RANGE;
downScrollRange = INVALID_SCROLL_RANGE;
// Restores the previous scrolling state. Don't override if there's a previously saved state
// which has not be restored yet. Multiple re-measuring can happen before the scroll state
// is actually restored. We don't want to restore the state in-between those re-measuring,
// since they can be incorrect.
if (savedState != null) {
behavior.restoreScrollState(savedState, false);
}
}

@Override
Expand All @@ -558,7 +571,8 @@ protected void onAttachedToWindow() {
@Override
@NonNull
public CoordinatorLayout.Behavior<AppBarLayout> getBehavior() {
return new AppBarLayout.Behavior();
behavior = new AppBarLayout.Behavior();
return behavior;
}

@RequiresApi(VERSION_CODES.LOLLIPOP)
Expand Down Expand Up @@ -1651,6 +1665,9 @@ public boolean onLayoutChild(
if (savedState.fullyScrolled) {
// Keep fully scrolled.
setHeaderTopBottomOffset(parent, abl, -abl.getTotalScrollRange());
} else if (savedState.fullyExpanded) {
// Keep fully expanded.
setHeaderTopBottomOffset(parent, abl, 0);
} else {
// Not fully scrolled, restore the visible percetage of child layout.
View child = abl.getChildAt(savedState.firstVisibleChildIndex);
Expand Down Expand Up @@ -2042,7 +2059,25 @@ int getTopBottomOffsetForScrollingSibling() {

@Override
public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull T abl) {
final Parcelable superState = super.onSaveInstanceState(parent, abl);
Parcelable superState = super.onSaveInstanceState(parent, abl);
SavedState scrollState = saveScrollState(superState, abl);
return scrollState == null ? superState : scrollState;
}

@Override
public void onRestoreInstanceState(
@NonNull CoordinatorLayout parent, @NonNull T appBarLayout, Parcelable state) {
if (state instanceof SavedState) {
restoreScrollState((SavedState) state, true);
super.onRestoreInstanceState(parent, appBarLayout, savedState.getSuperState());
} else {
super.onRestoreInstanceState(parent, appBarLayout, state);
savedState = null;
}
}

@Nullable
SavedState saveScrollState(@Nullable Parcelable superState, @NonNull T abl) {
final int offset = getTopAndBottomOffset();

// Try and find the first visible child...
Expand All @@ -2051,42 +2086,38 @@ public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNul
final int visBottom = child.getBottom() + offset;

if (child.getTop() + offset <= 0 && visBottom >= 0) {
final SavedState ss = new SavedState(superState);
ss.fullyScrolled = -getTopAndBottomOffset() >= abl.getTotalScrollRange();
final SavedState ss =
new SavedState(superState == null ? AbsSavedState.EMPTY_STATE : superState);
ss.fullyExpanded = offset == 0;
ss.fullyScrolled = !ss.fullyExpanded && -offset >= abl.getTotalScrollRange();
ss.firstVisibleChildIndex = i;
ss.firstVisibleChildAtMinimumHeight =
visBottom == (ViewCompat.getMinimumHeight(child) + abl.getTopInset());
ss.firstVisibleChildPercentageShown = visBottom / (float) child.getHeight();
return ss;
}
}

// Else we'll just return the super state
return superState;
return null;
}

@Override
public void onRestoreInstanceState(
@NonNull CoordinatorLayout parent, @NonNull T appBarLayout, Parcelable state) {
if (state instanceof SavedState) {
savedState = (SavedState) state;
super.onRestoreInstanceState(parent, appBarLayout, savedState.getSuperState());
} else {
super.onRestoreInstanceState(parent, appBarLayout, state);
savedState = null;
void restoreScrollState(@Nullable SavedState state, boolean force) {
if (savedState == null || force) {
savedState = state;
}
}

/** A {@link Parcelable} implementation for {@link AppBarLayout}. */
protected static class SavedState extends AbsSavedState {
boolean fullyScrolled;
boolean fullyExpanded;
int firstVisibleChildIndex;
float firstVisibleChildPercentageShown;
boolean firstVisibleChildAtMinimumHeight;

public SavedState(@NonNull Parcel source, ClassLoader loader) {
super(source, loader);
fullyScrolled = source.readByte() != 0;
fullyExpanded = source.readByte() != 0;
firstVisibleChildIndex = source.readInt();
firstVisibleChildPercentageShown = source.readFloat();
firstVisibleChildAtMinimumHeight = source.readByte() != 0;
Expand All @@ -2100,6 +2131,7 @@ public SavedState(Parcelable superState) {
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeByte((byte) (fullyScrolled ? 1 : 0));
dest.writeByte((byte) (fullyExpanded ? 1 : 0));
dest.writeInt(firstVisibleChildIndex);
dest.writeFloat(firstVisibleChildPercentageShown);
dest.writeByte((byte) (firstVisibleChildAtMinimumHeight ? 1 : 0));
Expand Down

0 comments on commit 698cf9b

Please sign in to comment.