Predictive swipe: show extra app icons at bottom of All Apps's RecyclerViews

The maximum center scale of All Apps to Home is 90%. It means we should add 5% height to All Apps's RecyclerView to render extra app icons.

Test: manual
bug: b/264906511
Change-Id: I2e970580810220e25d7fc3a86c19abaf87ba2c6e
diff --git a/res/values/id.xml b/res/values/id.xml
index 9fc0ff8..52a7e98 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -37,4 +37,6 @@
     <item type="id" name="quick_settings_button" />
     <item type="id" name="notifications_button" />
     <item type="id" name="cache_entry_tag_id" />
+
+    <item type="id" name="saved_clip_children_tag_id" />
 </resources>
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 63e6d13..112d47e 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -21,6 +21,7 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.annotation.Px;
 import androidx.core.view.accessibility.AccessibilityEventCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.core.view.accessibility.AccessibilityRecordCompat;
@@ -144,6 +145,19 @@
         }
 
         /**
+         * We need to extend all apps' RecyclerView's bottom by 5% of view height to ensure extra
+         * roll(s) of app icons is rendered at the bottom, so that they can fill the bottom gap
+         * created during predictive back's scale animation from all apps to home.
+         */
+        @Override
+        protected void calculateExtraLayoutSpace(RecyclerView.State state, int[] extraLayoutSpace) {
+            super.calculateExtraLayoutSpace(state, extraLayoutSpace);
+            @Px int extraSpacePx = (int) (getHeight()
+                    * (1 - AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE) / 2);
+            extraLayoutSpace[1] = Math.max(extraLayoutSpace[1], extraSpacePx);
+        }
+
+        /**
          * Returns the number of rows before {@param adapterPosition}, including this position
          * which should not be counted towards the collection info.
          */
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 24bfedb..6f6f86b 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -32,14 +32,18 @@
 import android.util.FloatProperty;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.FloatRange;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.Interpolators;
@@ -66,7 +70,7 @@
         implements StateHandler<LauncherState>, OnDeviceProfileChangeListener {
     // This constant should match the second derivative of the animator interpolator.
     public static final float INTERP_COEFF = 1.7f;
-    private static final float SWIPE_ALL_APPS_TO_HOME_MIN_SCALE = 0.9f;
+    public static final float SWIPE_ALL_APPS_TO_HOME_MIN_SCALE = 0.9f;
     private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200;
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
@@ -168,6 +172,8 @@
 
     private boolean mIsTablet;
 
+    private boolean mHasScaleEffect;
+
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
         DeviceProfile dp = mLauncher.getDeviceProfile();
@@ -276,8 +282,18 @@
             rv.getScrollbar().setVisibility(scaleProgress < 1f ? View.INVISIBLE : View.VISIBLE);
         }
 
-        // TODO(b/264906511): We need to disable view clipping on all apps' parent views so
-        //  that the extra roll of app icons are displayed.
+        // Disable view clipping from all apps' RecyclerView up to all apps view during scale
+        // animation, and vice versa. The goal is to display extra roll(s) app icons (rendered in
+        // {@link AppsGridLayoutManager#calculateExtraLayoutSpace}) during scale animation.
+        boolean hasScaleEffect = scaleProgress < 1f;
+        if (hasScaleEffect != mHasScaleEffect) {
+            mHasScaleEffect = hasScaleEffect;
+            if (mHasScaleEffect) {
+                setClipChildrenOnViewTree(rv, mLauncher.getAppsView(), false);
+            } else {
+                restoreClipChildrenOnViewTree(rv, mLauncher.getAppsView());
+            }
+        }
     }
 
     private void animateAllAppsToNoScale() {
@@ -381,6 +397,79 @@
     }
 
     /**
+     * Recursively call {@link ViewGroup#setClipChildren(boolean)} from {@link View} to ts parent
+     * (direct or indirect) inclusive. This method will also save the old clipChildren value on each
+     * view with {@link View#setTag(int, Object)}, which can be restored in
+     * {@link #restoreClipChildrenOnViewTree(View, ViewParent)}.
+     *
+     * Note that if parent is null or not a parent of the view, this method will be applied all the
+     * way to root view.
+     *
+     * @param v child view
+     * @param parent direct or indirect parent of child view
+     * @param clipChildren whether we should clip children
+     */
+    private static void setClipChildrenOnViewTree(
+            @Nullable View v,
+            @Nullable ViewParent parent,
+            boolean clipChildren) {
+        if (v == null) {
+            return;
+        }
+
+        if (v instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) v;
+            boolean oldClipChildren = viewGroup.getClipChildren();
+            if (oldClipChildren != clipChildren) {
+                v.setTag(R.id.saved_clip_children_tag_id, oldClipChildren);
+                viewGroup.setClipChildren(clipChildren);
+            }
+        }
+
+        if (v == parent) {
+            return;
+        }
+
+        if (v.getParent() instanceof View) {
+            setClipChildrenOnViewTree((View) v.getParent(), parent, clipChildren);
+        }
+    }
+
+    /**
+     * Recursively call {@link ViewGroup#setClipChildren(boolean)} to restore clip children value
+     * set in {@link #setClipChildrenOnViewTree(View, ViewParent, boolean)} on view to its parent
+     * (direct or indirect) inclusive.
+     *
+     * Note that if parent is null or not a parent of the view, this method will be applied all the
+     * way to root view.
+     *
+     * @param v child view
+     * @param parent direct or indirect parent of child view
+     */
+    private static void restoreClipChildrenOnViewTree(
+            @Nullable View v, @Nullable ViewParent parent) {
+        if (v == null) {
+            return;
+        }
+        if (v instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) v;
+            Object viewTag = viewGroup.getTag(R.id.saved_clip_children_tag_id);
+            if (viewTag instanceof Boolean) {
+                viewGroup.setClipChildren((boolean) viewTag);
+                viewGroup.setTag(R.id.saved_clip_children_tag_id, null);
+            }
+        }
+
+        if (v == parent) {
+            return;
+        }
+
+        if (v.getParent() instanceof View) {
+            restoreClipChildrenOnViewTree((View) v.getParent(), parent);
+        }
+    }
+
+    /**
      * Updates the total scroll range but does not update the UI.
      */
     public void setShiftRange(float shiftRange) {