Allow CoordinatorLayout Behaviors to save state
AppBarLayout is the first customer to make sure that
they restore correctly.
BUG: 20993010
Change-Id: Iad740edbba80abd18b44a0fa69d22fe7f1983677
diff --git a/design/api/current.txt b/design/api/current.txt
index dee9596..6053b92 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -17,10 +17,18 @@
method public boolean onNestedFling(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, float, float, boolean);
method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, int, int, int[]);
method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, int, int, int, int);
+ method public void onRestoreInstanceState(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.os.Parcelable);
+ method public android.os.Parcelable onSaveInstanceState(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout);
method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View, android.view.View, int);
method public void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, android.support.design.widget.AppBarLayout, android.view.View);
}
+ protected static class AppBarLayout.Behavior.SavedState extends android.view.View.BaseSavedState {
+ ctor public AppBarLayout.Behavior.SavedState(android.os.Parcel);
+ ctor public AppBarLayout.Behavior.SavedState(android.os.Parcelable);
+ field public static final android.os.Parcelable.Creator<android.support.design.widget.AppBarLayout.Behavior.SavedState> CREATOR;
+ }
+
public static class AppBarLayout.LayoutParams extends android.widget.LinearLayout.LayoutParams {
ctor public AppBarLayout.LayoutParams(android.content.Context, android.util.AttributeSet);
ctor public AppBarLayout.LayoutParams(int, int);
@@ -127,6 +135,8 @@
method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
method public void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+ method public void onRestoreInstanceState(android.support.design.widget.CoordinatorLayout, V, android.os.Parcelable);
+ method public android.os.Parcelable onSaveInstanceState(android.support.design.widget.CoordinatorLayout, V);
method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
method public void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View);
method public boolean onTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
@@ -150,6 +160,12 @@
field public int keyline;
}
+ protected static class CoordinatorLayout.SavedState extends android.view.View.BaseSavedState {
+ ctor public CoordinatorLayout.SavedState(android.os.Parcel);
+ ctor public CoordinatorLayout.SavedState(android.os.Parcelable);
+ field public static final android.os.Parcelable.Creator<android.support.design.widget.CoordinatorLayout.SavedState> CREATOR;
+ }
+
public class FloatingActionButton extends android.widget.ImageView {
ctor public FloatingActionButton(android.content.Context);
ctor public FloatingActionButton(android.content.Context, android.util.AttributeSet);
diff --git a/design/src/android/support/design/widget/AppBarLayout.java b/design/src/android/support/design/widget/AppBarLayout.java
index bb77821..b15c03a 100644
--- a/design/src/android/support/design/widget/AppBarLayout.java
+++ b/design/src/android/support/design/widget/AppBarLayout.java
@@ -18,6 +18,8 @@
import android.content.Context;
import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.design.R;
import android.support.v4.view.ViewCompat;
@@ -579,7 +581,9 @@
* scroll handling with offsetting.
*/
public static class Behavior extends ViewOffsetBehavior<AppBarLayout> {
- private int mLogicalOffsetTop;
+ private static final int INVALID_POSITION = -1;
+
+ private int mOffsetDelta;
private boolean mSkipNestedPreScroll;
private Runnable mFlingRunnable;
@@ -587,6 +591,10 @@
private ValueAnimatorCompat mAnimator;
+ private int mOffsetToChildIndexOnLayout = INVALID_POSITION;
+ private boolean mOffsetToChildIndexOnLayoutIsMinHeight;
+ private float mOffsetToChildIndexOnLayoutPerc;
+
public Behavior() {}
public Behavior(Context context, AttributeSet attrs) {
@@ -682,7 +690,7 @@
}
}
- if (mLogicalOffsetTop != targetScroll) {
+ if (getTopBottomOffsetForScrollingSibling() != targetScroll) {
animateOffsetTo(coordinatorLayout, child, targetScroll);
return true;
}
@@ -722,7 +730,7 @@
}
mScroller.fling(
- 0, mLogicalOffsetTop, // curr
+ 0, getTopBottomOffsetForScrollingSibling(), // curr
0, Math.round(velocityY), // velocity.
0, 0, // x
minOffset, maxOffset); // y
@@ -758,12 +766,24 @@
}
@Override
- public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout child,
+ public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout appBarLayout,
int layoutDirection) {
- boolean handled = super.onLayoutChild(parent, child, layoutDirection);
+ boolean handled = super.onLayoutChild(parent, appBarLayout, layoutDirection);
+
+ if (mOffsetToChildIndexOnLayout >= 0) {
+ View child = appBarLayout.getChildAt(mOffsetToChildIndexOnLayout);
+ int offset = -child.getBottom();
+ if (mOffsetToChildIndexOnLayoutIsMinHeight) {
+ offset += ViewCompat.getMinimumHeight(child);
+ } else {
+ offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
+ }
+ setTopAndBottomOffset(offset);
+ mOffsetToChildIndexOnLayout = INVALID_POSITION;
+ }
// Make sure we update the elevation
- dispatchOffsetUpdates(child);
+ dispatchOffsetUpdates(appBarLayout);
return handled;
}
@@ -771,7 +791,7 @@
private int scroll(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout,
int dy, int minOffset, int maxOffset) {
return setAppBarTopBottomOffset(coordinatorLayout, appBarLayout,
- mLogicalOffsetTop - dy, minOffset, maxOffset);
+ getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
}
final int setAppBarTopBottomOffset(CoordinatorLayout coordinatorLayout,
@@ -782,7 +802,7 @@
final int setAppBarTopBottomOffset(CoordinatorLayout coordinatorLayout,
AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
- final int curOffset = mLogicalOffsetTop;
+ final int curOffset = getTopBottomOffsetForScrollingSibling();
int consumed = 0;
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
@@ -791,15 +811,16 @@
newOffset = MathUtils.constrain(newOffset, minOffset, maxOffset);
if (curOffset != newOffset) {
- boolean offsetChanged = setTopAndBottomOffset(
- appBarLayout.hasChildWithInterpolator()
- ? interpolateOffset(appBarLayout, newOffset)
- : newOffset);
+ final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
+ ? interpolateOffset(appBarLayout, newOffset)
+ : newOffset;
+
+ boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
// Update how much dy we have consumed
consumed = curOffset - newOffset;
// Update the stored sibling offset
- mLogicalOffsetTop = newOffset;
+ mOffsetDelta = newOffset - interpolatedOffset;
if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
// If the offset hasn't changed and we're using an interpolated scroll
@@ -873,7 +894,84 @@
}
final int getTopBottomOffsetForScrollingSibling() {
- return mLogicalOffsetTop;
+ return getTopAndBottomOffset() + mOffsetDelta;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout) {
+ final Parcelable superState = super.onSaveInstanceState(parent, appBarLayout);
+ final int offset = getTopAndBottomOffset();
+
+ // Try and find the first visible child...
+ for (int i = 0, count = appBarLayout.getChildCount(); i < count; i++) {
+ View child = appBarLayout.getChildAt(i);
+ final int visBottom = child.getBottom() + offset;
+
+ if (child.getTop() + offset <= 0 && visBottom >= 0) {
+ final SavedState ss = new SavedState(superState);
+ ss.firstVisibleChildIndex = i;
+ ss.firstVisibileChildAtMinimumHeight =
+ visBottom == ViewCompat.getMinimumHeight(child);
+ ss.firstVisibileChildPercentageShown = visBottom / (float) child.getHeight();
+ return ss;
+ }
+ }
+
+ // Else we'll just return the super state
+ return superState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout,
+ Parcelable state) {
+ if (state instanceof SavedState) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(parent, appBarLayout, ss.getSuperState());
+ mOffsetToChildIndexOnLayout = ss.firstVisibleChildIndex;
+ mOffsetToChildIndexOnLayoutPerc = ss.firstVisibileChildPercentageShown;
+ mOffsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibileChildAtMinimumHeight;
+ } else {
+ super.onRestoreInstanceState(parent, appBarLayout, state);
+ mOffsetToChildIndexOnLayout = INVALID_POSITION;
+ }
+ }
+
+ protected static class SavedState extends View.BaseSavedState {
+ int firstVisibleChildIndex;
+ float firstVisibileChildPercentageShown;
+ boolean firstVisibileChildAtMinimumHeight;
+
+ public SavedState(Parcel source) {
+ super(source);
+ firstVisibleChildIndex = source.readInt();
+ firstVisibileChildPercentageShown = source.readFloat();
+ firstVisibileChildAtMinimumHeight = source.readByte() != 0;
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(firstVisibleChildIndex);
+ dest.writeFloat(firstVisibileChildPercentageShown);
+ dest.writeByte((byte) (firstVisibileChildAtMinimumHeight ? 1 : 0));
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel source) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
}
}
diff --git a/design/src/android/support/design/widget/CoordinatorLayout.java b/design/src/android/support/design/widget/CoordinatorLayout.java
index 100d346..1641d1b 100644
--- a/design/src/android/support/design/widget/CoordinatorLayout.java
+++ b/design/src/android/support/design/widget/CoordinatorLayout.java
@@ -26,6 +26,8 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.os.SystemClock;
import android.support.design.R;
import android.support.v4.content.ContextCompat;
@@ -37,6 +39,7 @@
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.SparseArray;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
@@ -2042,6 +2045,44 @@
V child, WindowInsetsCompat insets) {
return insets;
}
+
+ /**
+ * Hook allowing a behavior to re-apply a representation of its internal state that had
+ * previously been generated by {@link #onSaveInstanceState}. This function will never
+ * be called with a null state.
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child child view to restore from
+ * @param state The frozen state that had previously been returned by
+ * {@link #onSaveInstanceState}.
+ *
+ * @see #onSaveInstanceState()
+ */
+ public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
+ // no-op
+ }
+
+ /**
+ * Hook allowing a behavior to generate a representation of its internal state
+ * that can later be used to create a new instance with that same state.
+ * This state should only contain information that is not persistent or can
+ * not be reconstructed later.
+ *
+ * <p>Behavior state is only saved when both the parent {@link CoordinatorLayout} and
+ * a view using this behavior have valid IDs set.</p>
+ *
+ * @param parent the parent CoordinatorLayout
+ * @param child child view to restore from
+ *
+ * @return Returns a Parcelable object containing the behavior's current dynamic
+ * state.
+ *
+ * @see #onRestoreInstanceState(android.os.Parcelable)
+ * @see View#onSaveInstanceState()
+ */
+ public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
+ return BaseSavedState.EMPTY_STATE;
+ }
}
/**
@@ -2412,4 +2453,106 @@
return insets.consumeSystemWindowInsets();
}
}
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ final SparseArray<Parcelable> behaviorStates = ss.behaviorStates;
+
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ final View child = getChildAt(i);
+ final int childId = child.getId();
+ final LayoutParams lp = getResolvedLayoutParams(child);
+ final Behavior b = lp.getBehavior();
+
+ if (childId != NO_ID && b != null) {
+ Parcelable savedState = behaviorStates.get(childId);
+ if (savedState != null) {
+ b.onRestoreInstanceState(this, child, savedState);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final SavedState ss = new SavedState(super.onSaveInstanceState());
+
+ final SparseArray<Parcelable> behaviorStates = new SparseArray<>();
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ final View child = getChildAt(i);
+ final int childId = child.getId();
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final Behavior b = lp.getBehavior();
+
+ if (childId != NO_ID && b != null) {
+ // If the child has an ID and a Behavior, let it save some state...
+ Parcelable state = b.onSaveInstanceState(this, child);
+ if (state != null) {
+ behaviorStates.append(childId, state);
+ }
+ }
+ }
+ ss.behaviorStates = behaviorStates;
+ return ss;
+ }
+
+ protected static class SavedState extends BaseSavedState {
+ SparseArray<Parcelable> behaviorStates;
+
+ public SavedState(Parcel source) {
+ super(source);
+
+ final int size = source.readInt();
+
+ final int[] ids = new int[size];
+ source.readIntArray(ids);
+
+ final Parcelable[] states = source.readParcelableArray(
+ CoordinatorLayout.class.getClassLoader());
+
+ behaviorStates = new SparseArray<>(size);
+ for (int i = 0; i < size; i++) {
+ behaviorStates.append(ids[i], states[i]);
+ }
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ final int size = behaviorStates != null ? behaviorStates.size() : 0;
+ dest.writeInt(size);
+
+ final int[] ids = new int[size];
+ final Parcelable[] states = new Parcelable[size];
+
+ for (int i = 0; i < size; i++) {
+ ids[i] = behaviorStates.keyAt(i);
+ states[i] = behaviorStates.valueAt(i);
+ }
+ dest.writeIntArray(ids);
+ dest.writeParcelableArray(states, flags);
+
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel source) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
}
diff --git a/design/src/android/support/design/widget/ViewOffsetBehavior.java b/design/src/android/support/design/widget/ViewOffsetBehavior.java
index a89db5c..3ffc744 100644
--- a/design/src/android/support/design/widget/ViewOffsetBehavior.java
+++ b/design/src/android/support/design/widget/ViewOffsetBehavior.java
@@ -17,7 +17,6 @@
package android.support.design.widget;
import android.content.Context;
-import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
@@ -28,6 +27,9 @@
private ViewOffsetHelper mViewOffsetHelper;
+ private int mTempTopBottomOffset = 0;
+ private int mTempLeftRightOffset = 0;
+
public ViewOffsetBehavior() {}
public ViewOffsetBehavior(Context context, AttributeSet attrs) {
@@ -44,12 +46,23 @@
}
mViewOffsetHelper.onViewLayout();
+ if (mTempTopBottomOffset != 0) {
+ mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
+ mTempTopBottomOffset = 0;
+ }
+ if (mTempLeftRightOffset != 0) {
+ mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
+ mTempLeftRightOffset = 0;
+ }
+
return true;
}
public boolean setTopAndBottomOffset(int offset) {
if (mViewOffsetHelper != null) {
return mViewOffsetHelper.setTopAndBottomOffset(offset);
+ } else {
+ mTempTopBottomOffset = offset;
}
return false;
}
@@ -57,6 +70,8 @@
public boolean setLeftAndRightOffset(int offset) {
if (mViewOffsetHelper != null) {
return mViewOffsetHelper.setLeftAndRightOffset(offset);
+ } else {
+ mTempLeftRightOffset = offset;
}
return false;
}