Merge "overview: rebalance hidden tasks in grid" into sc-v2-dev
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 3aed7cc..538e61e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -125,6 +125,7 @@
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
@@ -2087,23 +2088,39 @@
         }
     }
 
-    /** Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
+    /**
+     * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
      * layout.
      * This method is used when no task dismissal has occurred.
      */
     private void updateGridProperties() {
-        updateGridProperties(false);
+        updateGridProperties(false, Integer.MAX_VALUE);
+    }
+
+    /**
+     * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
+     * layout.
+     *
+     * This method is used when task dismissal has occurred, but rebalance is not needed.
+     *
+     * @param isTaskDismissal indicates if update was called due to task dismissal
+     */
+    private void updateGridProperties(boolean isTaskDismissal) {
+        updateGridProperties(isTaskDismissal, Integer.MAX_VALUE);
     }
 
     /**
      * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
      * layout.
+     *
      * This method only calculates the potential position and depends on {@link #setGridProgress} to
      * apply the actual scaling and translation.
      *
-     * @param isTaskDismissal indicates if update was called due to task dismissal
+     * @param isTaskDismissal    indicates if update was called due to task dismissal
+     * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE
+     *                           to skip rebalance
      */
-    private void updateGridProperties(boolean isTaskDismissal) {
+    private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter) {
         int taskCount = getTaskViewCount();
         if (taskCount == 0) {
             return;
@@ -2170,8 +2187,20 @@
                     focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
                 }
                 int taskViewId = taskView.getTaskViewId();
-                boolean isTopRow = isTaskDismissal ? mTopRowIdSet.contains(taskViewId)
-                        : topRowWidth <= bottomRowWidth;
+
+                // Rebalance the grid starting after a certain index
+                boolean isTopRow;
+                if (isTaskDismissal) {
+                    if (i > startRebalanceAfter) {
+                        mTopRowIdSet.remove(taskViewId);
+                        isTopRow = topRowWidth <= bottomRowWidth;
+                    } else {
+                        isTopRow = mTopRowIdSet.contains(taskViewId);
+                    }
+                } else {
+                    isTopRow = topRowWidth <= bottomRowWidth;
+                }
+
                 if (isTopRow) {
                     if (homeTaskView != null && nextFocusedTaskView == null) {
                         // TaskView will be focused when swipe up, don't count towards row width.
@@ -2677,9 +2706,44 @@
                             mTopRowIdSet.remove(mFocusedTaskViewId);
                             finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
                         }
-                        updateTaskSize(true);
+                        updateTaskSize(/*isTaskDismissal=*/ true);
                         // Update scroll and snap to page.
                         updateScrollSynchronously();
+
+                        int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
+                        if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
+                            TaskView taskView = getTaskViewAt(highestVisibleTaskIndex);
+
+                            boolean shouldRebalance = false;
+                            int screenStart = mOrientationHandler.getPrimaryScroll(
+                                    RecentsView.this);
+                            int taskStart = mOrientationHandler.getChildStart(taskView)
+                                    + (int) taskView.getOffsetAdjustment(
+                                    /*fullscreenEnabled=*/ false,
+                                    /*gridEnabled=*/ true);
+
+                            // Rebalance only if there is a maximum gap between the task and the
+                            // screen's edge; this ensures that rebalanced tasks are outside the
+                            // visible screen.
+                            if (mIsRtl) {
+                                shouldRebalance = taskStart <= screenStart + mPageSpacing;
+                            } else {
+                                int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(
+                                        RecentsView.this);
+                                int taskSize = (int) (mOrientationHandler.getMeasuredSize(taskView)
+                                        * taskView.getSizeAdjustment(/*fullscreenEnabled=*/ false));
+                                int taskEnd = taskStart + taskSize;
+
+                                shouldRebalance = taskEnd >= screenEnd - mPageSpacing;
+                            }
+
+                            if (shouldRebalance) {
+                                updateGridProperties(/*isTaskDismissal=*/ true,
+                                        highestVisibleTaskIndex);
+                                updateScrollSynchronously();
+                            }
+                        }
+
                         setCurrentPage(pageToSnapTo);
                         dispatchScrollChanged();
                     }
@@ -2691,6 +2755,52 @@
         return anim;
     }
 
+    /**
+     * Returns all the tasks in the bottom row, without the focused task
+     */
+    private IntArray getBottomRowIdArray() {
+        IntArray bottomArray = new IntArray();
+        int taskViewCount = getTaskViewCount();
+        for (int i = 0; i < taskViewCount; i++) {
+            int taskViewId = getTaskViewAt(i).getTaskViewId();
+            if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) {
+                bottomArray.add(taskViewId);
+            }
+        }
+        return bottomArray;
+    }
+
+    /**
+     * Iterate the grid by columns instead of by TaskView index, starting after the focused task and
+     * up to the last balanced column.
+     *
+     * @return the highest visible TaskView index between both rows
+     */
+    private int getHighestVisibleTaskIndex() {
+        if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier
+
+        int lastVisibleIndex = Integer.MAX_VALUE;
+        IntArray topRowIdArray = mTopRowIdSet.getArray();
+        IntArray bottomRowIdArray = getBottomRowIdArray();
+        int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size());
+
+        for (int i = 0; i < balancedColumns; i++) {
+            TaskView topTask = getTaskViewFromTaskViewId(topRowIdArray.get(i));
+
+            if (isTaskViewVisible(topTask)) {
+                TaskView bottomTask = getTaskViewFromTaskViewId(bottomRowIdArray.get(i));
+                lastVisibleIndex = Math.max(
+                        indexOfChild(topTask) - mTaskViewStartIndex,
+                        indexOfChild(bottomTask) - mTaskViewStartIndex
+                );
+            } else if (lastVisibleIndex < Integer.MAX_VALUE) {
+                break;
+            }
+        }
+
+        return lastVisibleIndex;
+    }
+
     private void removeTaskInternal(int dismissedTaskId) {
         UI_HELPER_EXECUTOR.getHandler().postDelayed(
                 () -> ActivityManagerWrapper.getInstance().removeTask(dismissedTaskId),