App to home animation zooms into the app icon.

- Created FloatingIconView which is now used by both the app close and
  app open animation.
- getItemLocation in DeviceProfile is used to get an item's final location
  since getLocationOnScreen may return a View's location mid-animation.
- Added getFirstMatchForAppClose which is optimized to return for best
  visual animation.
- Also fixes app open RTL bug.
- Next CL will use AdaptiveIcons and FolderShape reveal animator to match
  the app icon to the app window.

Bug: 123900446
Bug: 123541334
Change-Id: Ief75f63fc5141c1ee59d4773946d08794846cb31
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 812cf9f..296c951 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -26,12 +26,15 @@
 import android.graphics.Rect;
 import android.util.DisplayMetrics;
 import android.view.Surface;
+import android.view.View;
 import android.view.WindowManager;
 
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
@@ -574,6 +577,33 @@
         }
     }
 
+    /**
+     * Gets an item's location on the home screen. This is useful if the home screen
+     * is animating, otherwise use {@link View#getLocationOnScreen(int[])}.
+     *
+     * TODO(b/123900446): Handle landscape mode
+     * @param pageDiff The page difference relative to the current page.
+     */
+    public void getItemLocation(int cellX, int cellY, int spanX, int spanY, int container,
+            int pageDiff, Rect outBounds) {
+        outBounds.setEmpty();
+        outBounds.left = mInsets.left
+                + workspacePadding.left + cellLayoutPaddingLeftRightPx + (cellX * getCellSize().x);
+        outBounds.top = mInsets.top;
+        if (container == CONTAINER_HOTSEAT) {
+            outBounds.top += workspacePadding.top
+                    + (inv.numRows * getCellSize().y)
+                    + verticalDragHandleSizePx
+                    - verticalDragHandleOverlapWorkspace;
+            outBounds.bottom = outBounds.top + hotseatBarSizePx - hotseatBarBottomPaddingPx;
+        } else {
+            outBounds.top += workspacePadding.top + (cellY * getCellSize().y);
+            outBounds.bottom = outBounds.top + (getCellSize().y * spanY);
+            outBounds.left += (pageDiff) * availableWidthPx;
+        }
+        outBounds.right = outBounds.left + (getCellSize().x * spanX);
+    }
+
     private static Context getContext(Context c, int orientation) {
         Configuration context = new Configuration(c.getResources().getConfiguration());
         context.orientation = orientation;
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 1dec173..e5ab2d1 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -52,6 +52,8 @@
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.util.IntArray;
 
 import java.io.Closeable;
@@ -65,6 +67,9 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+
 /**
  * Various utilities shared amongst the Launcher's classes.
  */
@@ -541,4 +546,48 @@
     public static String getPointString(int x, int y) {
         return String.format(Locale.ENGLISH, "%d,%d", x, y);
     }
+
+    /**
+     * Returns the location bounds of a view.
+     * - For DeepShortcutView, we return the bounds of the icon view.
+     * - For BubbleTextView, we return the icon bounds.
+     */
+    public static void getLocationBoundsForView(Launcher launcher, View v, Rect outRect) {
+        final DragLayer dragLayer = launcher.getDragLayer();
+        final boolean isBubbleTextView = v instanceof BubbleTextView;
+        final Rect rect = new Rect();
+
+        final boolean fromDeepShortcutView = v.getParent() instanceof DeepShortcutView;
+        if (fromDeepShortcutView) {
+            // Deep shortcut views have their icon drawn in a separate view.
+            DeepShortcutView view = (DeepShortcutView) v.getParent();
+            dragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect);
+        } else if (isBubbleTextView && v.getTag() instanceof ItemInfo
+                && (((ItemInfo) v.getTag()).container == CONTAINER_DESKTOP
+                || ((ItemInfo) v.getTag()).container == CONTAINER_HOTSEAT)) {
+            BubbleTextView btv = (BubbleTextView) v;
+            CellLayout pageViewIsOn = ((CellLayout) btv.getParent().getParent());
+            int pageNum = launcher.getWorkspace().indexOfChild(pageViewIsOn);
+
+            DeviceProfile dp = launcher.getDeviceProfile();
+            ItemInfo info = ((ItemInfo) btv.getTag());
+            dp.getItemLocation(info.cellX, info.cellY, info.spanX, info.spanY,
+                    info.container, pageNum - launcher.getCurrentWorkspaceScreen(), rect);
+        } else {
+            dragLayer.getDescendantRectRelativeToSelf(v, rect);
+        }
+        int viewLocationLeft = rect.left;
+        int viewLocationTop = rect.top;
+
+        if (isBubbleTextView && !fromDeepShortcutView) {
+            BubbleTextView btv = (BubbleTextView) v;
+            btv.getIconBounds(rect);
+        } else {
+            rect.set(0, 0, rect.width(), rect.height());
+        }
+        viewLocationLeft += rect.left;
+        viewLocationTop += rect.top;
+        outRect.set(viewLocationLeft, viewLocationTop, viewLocationLeft + rect.width(),
+                viewLocationTop + rect.height());
+    }
 }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 2db6cd9..7f5ca42 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -34,6 +34,7 @@
 import android.app.WallpaperManager;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -97,6 +98,7 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -2889,6 +2891,88 @@
         return layouts;
     }
 
+    /**
+     * Returns a list of all the CellLayouts on the Homescreen, starting with
+     * {@param startPage}, then going outward alternating between pages prior to the startPage,
+     * and then the pages after the startPage.
+     * ie. if there are 5 pages [0, 1, 2, 3, 4] and startPage is 1, we return [1, 0, 2, 3, 4].
+     */
+    private CellLayout[] getWorkspaceCellLayouts(int startPage) {
+        int screenCount = getChildCount();
+        final CellLayout[] layouts = new CellLayout[screenCount];
+        int screen = 0;
+
+        layouts[screen] = (CellLayout) getChildAt(startPage);
+        screen++;
+
+        for (int i = 1; screen < screenCount; ++i) {
+            CellLayout prevPage = (CellLayout) getChildAt(startPage - i);
+            CellLayout nextPage = (CellLayout) getChildAt(startPage + i);
+
+            if (prevPage != null) {
+                layouts[screen] = prevPage;
+                screen++;
+            }
+            if (nextPage != null) {
+                layouts[screen] = nextPage;
+                screen++;
+            }
+        }
+        return layouts;
+    }
+
+    /**
+     * Similar to {@link #getFirstMatch} but optimized to finding a suitable view for the app close
+     * animation.
+     *
+     * @param component The component of the task being dismissed.
+     */
+    public View getFirstMatchForAppClose(ComponentName component) {
+        final int curPage = getCurrentPage();
+        final CellLayout currentPage = (CellLayout) getPageAt(curPage);
+        final Workspace.ItemOperator isItemComponent = (info, view) ->
+                info != null && Objects.equals(info.getTargetComponent(), component);
+        final Workspace.ItemOperator isItemInFolder = (info, view) -> {
+            if (info instanceof FolderInfo) {
+                FolderInfo folderInfo = (FolderInfo) info;
+                for (ShortcutInfo shortcutInfo : folderInfo.contents) {
+                    if (Objects.equals(shortcutInfo.getTargetComponent(), component)) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        };
+
+        CellLayout[] hotseatAndCurrentPage = new CellLayout[] { getHotseat(), currentPage };
+        // First we look if the app itself is in the hotseat or on the current workspace page.
+        View icon = getFirstMatch(hotseatAndCurrentPage, isItemComponent);
+        if (icon != null) {
+            return icon;
+        }
+        // Then we look if the app is in a folder on the hotseat or current workspace page.
+        icon = getFirstMatch(hotseatAndCurrentPage, isItemInFolder);
+        if (icon != null) {
+            return icon;
+        }
+        // Continue searching for the app or for a folder with the app on other pages of the
+        // workspace. We skip the current page, since we already searched above.
+        CellLayout[] allPages = getWorkspaceCellLayouts(curPage);
+        CellLayout[] page = new CellLayout[1];
+        for (int i = 1; i < allPages.length; ++i) {
+            page[0] = allPages[i];
+            icon = getFirstMatch(page, isItemComponent);
+            if (icon != null) {
+                return icon;
+            }
+            icon = getFirstMatch(page, isItemInFolder);
+            if (icon != null) {
+                return icon;
+            }
+        }
+        return null;
+    }
+
     public View getHomescreenIconByItemId(final int id) {
         return getFirstMatch((info, v) -> info != null && info.id == id);
     }
@@ -2914,6 +2998,23 @@
         return value[0];
     }
 
+    private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator operator) {
+        final View[] value = new View[1];
+        for (CellLayout cellLayout : cellLayouts) {
+            mapOverCellLayout(MAP_NO_RECURSE, cellLayout, (info, v) -> {
+                if (operator.evaluate(info, v)) {
+                    value[0] = v;
+                    return true;
+                }
+                return false;
+            });
+            if (value[0] != null) {
+                break;
+            }
+        }
+        return value[0];
+    }
+
     void clearDropTargets() {
         mapOverItems(MAP_NO_RECURSE, new ItemOperator() {
             @Override
@@ -2992,31 +3093,38 @@
      */
     public void mapOverItems(boolean recurse, ItemOperator op) {
         for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
-            ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
-            // map over all the shortcuts on the workspace
-            final int itemCount = container.getChildCount();
-            for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
-                View item = container.getChildAt(itemIdx);
-                ItemInfo info = (ItemInfo) item.getTag();
-                if (recurse && info instanceof FolderInfo && item instanceof FolderIcon) {
-                    FolderIcon folder = (FolderIcon) item;
-                    ArrayList<View> folderChildren = folder.getFolder().getItemsInReadingOrder();
-                    // map over all the children in the folder
-                    final int childCount = folderChildren.size();
-                    for (int childIdx = 0; childIdx < childCount; childIdx++) {
-                        View child = folderChildren.get(childIdx);
-                        info = (ItemInfo) child.getTag();
-                        if (op.evaluate(info, child)) {
-                            return;
-                        }
+            if (mapOverCellLayout(recurse, layout, op)) {
+                return;
+            }
+        }
+    }
+
+    private boolean mapOverCellLayout(boolean recurse, CellLayout layout, ItemOperator op) {
+        ShortcutAndWidgetContainer container = layout.getShortcutsAndWidgets();
+        // map over all the shortcuts on the workspace
+        final int itemCount = container.getChildCount();
+        for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
+            View item = container.getChildAt(itemIdx);
+            ItemInfo info = (ItemInfo) item.getTag();
+            if (recurse && info instanceof FolderInfo && item instanceof FolderIcon) {
+                FolderIcon folder = (FolderIcon) item;
+                ArrayList<View> folderChildren = folder.getFolder().getItemsInReadingOrder();
+                // map over all the children in the folder
+                final int childCount = folderChildren.size();
+                for (int childIdx = 0; childIdx < childCount; childIdx++) {
+                    View child = folderChildren.get(childIdx);
+                    info = (ItemInfo) child.getTag();
+                    if (op.evaluate(info, child)) {
+                        return true;
                     }
-                } else {
-                    if (op.evaluate(info, item)) {
-                        return;
-                    }
+                }
+            } else {
+                if (op.evaluate(info, item)) {
+                    return true;
                 }
             }
         }
+        return false;
     }
 
     void updateShortcuts(ArrayList<ShortcutInfo> shortcuts) {
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
new file mode 100644
index 0000000..07318c9
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.InsettableFrameLayout.LayoutParams;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.DrawableFactory;
+
+/**
+ * A view that is created to look like another view with the purpose of creating fluid animations.
+ */
+public class FloatingIconView extends View implements Animator.AnimatorListener {
+
+    private Runnable mStartRunnable;
+    private Runnable mEndRunnable;
+
+    public FloatingIconView(Context context) {
+        super(context);
+    }
+
+    public void setRunnables(Runnable startRunnable, Runnable endRunnable) {
+        mStartRunnable = startRunnable;
+        mEndRunnable = endRunnable;
+    }
+
+    /**
+     * Positions this view to match the size and location of {@param rect}.
+     */
+    public void update(RectF rect, float alpha) {
+        setAlpha(alpha);
+
+        LayoutParams lp = (LayoutParams) getLayoutParams();
+        float dX = rect.left - lp.leftMargin;
+        float dY = rect.top - lp.topMargin;
+        setTranslationX(dX);
+        setTranslationY(dY);
+
+        float scaleX = rect.width() / (float) getWidth();
+        float scaleY = rect.height() / (float) getHeight();
+        float scale = Math.min(scaleX, scaleY);
+        setPivotX(0);
+        setPivotY(0);
+        setScaleX(scale);
+        setScaleY(scale);
+    }
+
+    @Override
+    public void onAnimationStart(Animator animator) {
+        if (mStartRunnable != null) {
+            mStartRunnable.run();
+        }
+    }
+
+    @Override
+    public void onAnimationEnd(Animator animator) {
+        if (mEndRunnable != null) {
+            mEndRunnable.run();
+        }
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animator) {
+    }
+
+    @Override
+    public void onAnimationRepeat(Animator animator) {
+    }
+
+    /**
+     * Sets the size and position of this view to match {@param v}.
+     *
+     * @param v The view to copy
+     * @param hideOriginal If true, it will hide {@param v} while this view is visible.
+     * @param positionOut Rect that will hold the size and position of v.
+     */
+    public void matchPositionOf(Launcher launcher, View v, boolean hideOriginal, Rect positionOut) {
+        Utilities.getLocationBoundsForView(launcher, v, positionOut);
+        final LayoutParams lp = new LayoutParams(positionOut.width(), positionOut.height());
+        lp.ignoreInsets = true;
+
+        // Position the floating view exactly on top of the original
+        lp.leftMargin = positionOut.left;
+        lp.topMargin = positionOut.top;
+        setLayoutParams(lp);
+        // Set the properties here already to make sure they are available when running the first
+        // animation frame.
+        layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
+                + lp.height);
+
+        if (v instanceof BubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) {
+            // Create a copy of the app icon
+            setBackground(DrawableFactory.INSTANCE.get(launcher)
+                    .newIcon(v.getContext(), (ItemInfoWithIcon) v.getTag()));
+        }
+
+        // We need to add it to the overlay, but keep it invisible until animation starts..
+        final DragLayer dragLayer = launcher.getDragLayer();
+        setVisibility(INVISIBLE);
+        ((ViewGroup) dragLayer.getParent()).getOverlay().add(this);
+
+        setRunnables(() -> {
+                    setVisibility(VISIBLE);
+                    if (hideOriginal) {
+                        v.setVisibility(INVISIBLE);
+                    }
+                },
+                () -> {
+                    ((ViewGroup) dragLayer.getParent()).getOverlay().remove(this);
+                    if (hideOriginal) {
+                        v.setVisibility(VISIBLE);
+                    }
+                });
+    }
+}