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;
     }