Suppress layout handling during animateOut()

When playing video, data change of rows will automatically
pulls row in. This CL suppresses layout when animateOut()
until a specific animateIn() is called.

Bug: 35399351
Test: testAnimateOutBlockLayout testAnimateOutBlockSmoothScroll
      testAnimateOutBlockScrollTo
Change-Id: Ie91137687e96f0d48a674c410041b9412c8945d6
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index a2480d4..9438ce5 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -704,6 +704,7 @@
             public void onRequestChildFocus(View child, View focused) {
                 if (child != mRootView.getFocusedChild()) {
                     if (child.getId() == R.id.details_fragment_root) {
+                        slideInGridView();
                         showTitle(true);
                     } else if (child.getId() == R.id.video_surface_container) {
                         slideOutGridView();
@@ -774,4 +775,9 @@
         }
     }
 
+    void slideInGridView() {
+        if (getVerticalGridView() != null) {
+            getVerticalGridView().animateIn();
+        }
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index d2e0ef2..79ab727 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -707,6 +707,7 @@
             public void onRequestChildFocus(View child, View focused) {
                 if (child != mRootView.getFocusedChild()) {
                     if (child.getId() == R.id.details_fragment_root) {
+                        slideInGridView();
                         showTitle(true);
                     } else if (child.getId() == R.id.video_surface_container) {
                         slideOutGridView();
@@ -777,4 +778,9 @@
         }
     }
 
+    void slideInGridView() {
+        if (getVerticalGridView() != null) {
+            getVerticalGridView().animateIn();
+        }
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
index 005a441..b19458b 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -1004,21 +1004,39 @@
 
     /**
      * Temporarily slide out child views to bottom (for VerticalGridView) or end
-     * (for HorizontalGridView). The views will be automatically slide-in in next
-     * {@link #smoothScrollToPosition(int)} or {@link #scrollToPosition(int)}.
+     * (for HorizontalGridView). Layout and scrolling will be suppressed until
+     * {@link #animateIn()} is called.
      */
     public void animateOut() {
         mLayoutManager.slideOut();
     }
 
     /**
-     * @deprecated No longer needed. Children being slide out by {@link #animateOut()} will be
-     * slide in next focus or (smooth)scrollToPosition action.
+     * Undo animateOut() and slide in child views.
      */
-    @Deprecated
     public void animateIn() {
+        mLayoutManager.slideIn();
     }
 
+    @Override
+    public void scrollToPosition(int position) {
+        // dont abort the animateOut() animation, just record the position
+        if (mLayoutManager.mIsSlidingChildViews) {
+            mLayoutManager.setSelectionWithSub(position, 0, 0);
+            return;
+        }
+        super.scrollToPosition(position);
+    }
+
+    @Override
+    public void smoothScrollToPosition(int position) {
+        // dont abort the animateOut() animation, just record the position
+        if (mLayoutManager.mIsSlidingChildViews) {
+            mLayoutManager.setSelectionWithSub(position, 0, 0);
+            return;
+        }
+        super.smoothScrollToPosition(position);
+    }
 
     /**
      * Sets the number of items to prefetch in
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index 5569836..fc7c890 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -16,6 +16,7 @@
 import static android.support.v7.widget.RecyclerView.HORIZONTAL;
 import static android.support.v7.widget.RecyclerView.NO_ID;
 import static android.support.v7.widget.RecyclerView.NO_POSITION;
+import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
 import static android.support.v7.widget.RecyclerView.VERTICAL;
 
 import android.content.Context;
@@ -379,6 +380,7 @@
 
     // Represents whether child views are temporarily sliding out
     boolean mIsSlidingChildViews;
+    boolean mLayoutEatenInSliding;
 
     String getTag() {
         return TAG + ":" + mBaseGridView.getId();
@@ -1738,8 +1740,31 @@
         return mGrid.appendOneColumnVisibleItems();
     }
 
+    void slideIn() {
+        if (mIsSlidingChildViews) {
+            mIsSlidingChildViews = false;
+            scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
+            if (mLayoutEatenInSliding) {
+                mLayoutEatenInSliding = false;
+                if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
+                    mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                        @Override
+                        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                            if (newState == SCROLL_STATE_IDLE) {
+                                mBaseGridView.removeOnScrollListener(this);
+                                requestLayout();
+                            }
+                        }
+                    });
+                } else {
+                    requestLayout();
+                }
+            }
+        }
+    }
+
     /**
-     * Temporarily slide out child and will be auto slide-in in next scrollToView().
+     * Temporarily slide out child and block layout and scroll requests.
      */
     void slideOut() {
         if (mIsSlidingChildViews) {
@@ -1936,6 +1961,10 @@
             return;
         }
 
+        if (mIsSlidingChildViews) {
+            mLayoutEatenInSliding = true;
+            return;
+        }
         if (!mLayoutEnabled) {
             discardLayoutInfo();
             removeAndRecycleAllViews(recycler);
@@ -2385,7 +2414,7 @@
 
     public void setSelection(int position, int subposition, boolean smooth,
             int primaryScrollExtra) {
-        if (mIsSlidingChildViews || mFocusPosition != position && position != NO_POSITION
+        if (mFocusPosition != position && position != NO_POSITION
                 || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
             scrollToSelection(position, subposition, smooth, primaryScrollExtra);
         }
@@ -2404,7 +2433,7 @@
             mFocusPosition = position;
             mSubFocusPosition = subposition;
             mFocusPositionOffset = Integer.MIN_VALUE;
-            if (!mLayoutEnabled) {
+            if (!mLayoutEnabled || mIsSlidingChildViews) {
                 return;
             }
             if (smooth) {
@@ -2651,17 +2680,19 @@
     }
 
     /**
-     * Scroll to a given child view and change mFocusPosition.
+     * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
      */
     void scrollToView(View view, boolean smooth) {
         scrollToView(view, view == null ? null : view.findFocus(), smooth);
     }
 
     /**
-     * Scroll to a given child view and change mFocusPosition.
+     * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
      */
     private void scrollToView(View view, View childView, boolean smooth) {
-        mIsSlidingChildViews = false;
+        if (mIsSlidingChildViews) {
+            return;
+        }
         int newFocusPosition = getPositionByView(view);
         int newSubFocusPosition = getSubPositionByView(view, childView);
         if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
index 6678101..2536f0a 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
@@ -278,6 +278,13 @@
     }
 
 
+    void changeItem(int position, int itemValue) {
+        mItemLengths[position] = itemValue;
+        if (mGridView.getAdapter() != null) {
+            mGridView.getAdapter().notifyItemChanged(position);
+        }
+    }
+
     void addItems(int index, int[] items) {
         int length = items.length;
         if (mItemLengths.length < mNumItems + length) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 7dab382..8bb53a4 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -2988,8 +2988,28 @@
         assertTrue(selectedPosition2 < selectedPosition1);
     }
 
+    void slideInAndWaitIdle() throws Throwable {
+        slideInAndWaitIdle(5000);
+    }
+
+    void slideInAndWaitIdle(long timeout) throws Throwable {
+        // animateIn() would reset position
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateIn();
+            }
+        });
+        PollingCheck.waitFor(timeout, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return !mGridView.getLayoutManager().isSmoothScrolling()
+                        && mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+    }
+
     @Test
-    public void testAnimateOutResetByScrollTo() throws Throwable {
+    public void testAnimateOutBlockScrollTo() throws Throwable {
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
                 R.layout.vertical_linear_with_button_onleft);
@@ -3012,12 +3032,14 @@
                 mGridView.animateOut();
             }
         });
+        // wait until sliding out.
         PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
                 return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
             }
         });
+        // scrollToPosition() should not affect slideOut status
         mActivityTestRule.runOnUiThread(new Runnable() {
             public void run() {
                 mGridView.scrollToPosition(0);
@@ -3029,13 +3051,180 @@
                 return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
             }
         });
+        assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+                >= mGridView.getHeight());
 
+        slideInAndWaitIdle();
         assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
                 mGridView.getChildAt(0).getTop());
     }
 
     @Test
-    public void testAnimateOutResetByFocusChange() throws Throwable {
+    public void testAnimateOutBlockSmoothScrolling() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[30];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        // wait until sliding out.
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        // smoothScrollToPosition() should not affect slideOut status
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.smoothScrollToPosition(29);
+            }
+        });
+        PollingCheck.waitFor(10000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+        assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+                >= mGridView.getHeight());
+
+        slideInAndWaitIdle();
+        View lastChild = mGridView.getChildAt(mGridView.getChildCount() - 1);
+        assertSame("Scrolled to last child",
+                mGridView.findViewHolderForAdapterPosition(29).itemView, lastChild);
+    }
+
+    @Test
+    public void testAnimateOutBlockLongScrollTo() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[30];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        // wait until sliding out.
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        // smoothScrollToPosition() should not affect slideOut status
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.scrollToPosition(29);
+            }
+        });
+        PollingCheck.waitFor(10000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+        assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+                >= mGridView.getHeight());
+
+        slideInAndWaitIdle();
+        View lastChild = mGridView.getChildAt(mGridView.getChildCount() - 1);
+        assertSame("Scrolled to last child",
+                mGridView.findViewHolderForAdapterPosition(29).itemView, lastChild);
+    }
+
+    @Test
+    public void testAnimateOutBlockLayout() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        // wait until sliding out.
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        // change adapter should not affect slideOut status
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mActivity.changeItem(0, 200);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+        assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+                >= mGridView.getHeight());
+        assertEquals("onLayout suppressed during slide out", 300,
+                mGridView.getChildAt(0).getHeight());
+
+        slideInAndWaitIdle();
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+        // size of item should be updated immediately after slide in animation finishes:
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return 200 == mGridView.getChildAt(0).getHeight();
+            }
+        });
+    }
+
+    @Test
+    public void testAnimateOutBlockFocusChange() throws Throwable {
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
                 R.layout.vertical_linear_with_button_onleft);
@@ -3077,13 +3266,16 @@
                 return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
             }
         });
+        assertTrue("First view slided Out", mGridView.getChildAt(0).getTop()
+                >= mGridView.getHeight());
 
+        slideInAndWaitIdle();
         assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
                 mGridView.getChildAt(0).getTop());
     }
 
     @Test
-    public void testHorizontalAnimateOutResetByScrollTo() throws Throwable {
+    public void testHorizontalAnimateOutBlockScrollTo() throws Throwable {
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
                 R.layout.horizontal_linear);
@@ -3124,8 +3316,13 @@
             }
         });
 
+        assertTrue("First view is slided out", mGridView.getChildAt(0).getLeft()
+                > mGridView.getWidth());
+
+        slideInAndWaitIdle();
         assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
                 mGridView.getChildAt(0).getLeft());
+
     }
 
     @Test
@@ -3172,6 +3369,9 @@
             }
         });
 
+        assertTrue("First view is slided out", mGridView.getChildAt(0).getRight() < 0);
+
+        slideInAndWaitIdle();
         assertEquals("First view is aligned with padding right",
                 mGridView.getWidth() - mGridView.getPaddingRight(),
                 mGridView.getChildAt(0).getRight());