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