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/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index f3b27fa..a08399a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -29,6 +29,7 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
+import android.content.ComponentName;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -51,6 +52,7 @@
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.views.FloatingIconView;
import com.android.quickstep.util.ClipAnimationHelper;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.views.LauncherLayoutListener;
@@ -108,17 +110,41 @@
@NonNull
@Override
public HomeAnimationFactory prepareHomeUI(Launcher activity) {
- DeviceProfile dp = activity.getDeviceProfile();
+ final DeviceProfile dp = activity.getDeviceProfile();
+ final RecentsView recentsView = activity.getOverviewPanel();
+ final ComponentName component = recentsView.getRunningTaskView().getTask().key
+ .sourceComponent;
+
+ final View workspaceView = activity.getWorkspace().getFirstMatchForAppClose(component);
+ final FloatingIconView floatingView = workspaceView == null ? null
+ : new FloatingIconView(activity);
+ final Rect iconLocation = new Rect();
+ if (floatingView != null) {
+ floatingView.matchPositionOf(activity, workspaceView, true /* hideOriginal */,
+ iconLocation);
+ }
return new HomeAnimationFactory() {
+ @Nullable
+ @Override
+ public View getFloatingView() {
+ return floatingView;
+ }
+
@NonNull
@Override
public RectF getWindowTargetRect() {
- int halfIconSize = dp.iconSizePx / 2;
- float targetCenterX = dp.availableWidthPx / 2;
- float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
- return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
- targetCenterX + halfIconSize, targetCenterY + halfIconSize);
+ final int halfIconSize = dp.iconSizePx / 2;
+ final float targetCenterX = dp.availableWidthPx / 2f;
+ final float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
+
+ if (workspaceView != null) {
+ return new RectF(iconLocation);
+ } else {
+ // Fallback to animate to center of screen.
+ return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
+ targetCenterX + halfIconSize, targetCenterY + halfIconSize);
+ }
}
@NonNull
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index ed83ed6..ab5f479 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -59,10 +59,10 @@
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.DrawableFactory;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.views.FloatingIconView;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.quickstep.util.RemoteAnimationProvider;
@@ -134,7 +134,7 @@
private final float mClosingWindowTransY;
private DeviceProfile mDeviceProfile;
- private View mFloatingView;
+ private FloatingIconView mFloatingView;
private RemoteAnimationProvider mRemoteAnimationProvider;
@@ -404,7 +404,7 @@
boolean toggleVisibility) {
final boolean isBubbleTextView = v instanceof BubbleTextView;
if (mFloatingView == null) {
- mFloatingView = new View(mLauncher);
+ mFloatingView = new FloatingIconView(mLauncher);
} else {
mFloatingView.setTranslationX(0);
mFloatingView.setTranslationY(0);
@@ -413,58 +413,15 @@
mFloatingView.setAlpha(1);
mFloatingView.setBackground(null);
}
- if (isBubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) {
- // Create a copy of the app icon
- mFloatingView.setBackground(DrawableFactory.INSTANCE.get(mLauncher)
- .newIcon(v.getContext(), (ItemInfoWithIcon) v.getTag()));
- }
-
- // Position the floating view exactly on top of the original
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();
- mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect);
- } else {
- mDragLayer.getDescendantRectRelativeToSelf(v, rect);
- }
- int viewLocationLeft = rect.left;
- int viewLocationTop = rect.top;
+ mFloatingView.matchPositionOf(mLauncher, v, toggleVisibility, rect);
- float startScale = 1f;
- if (isBubbleTextView && !fromDeepShortcutView) {
- BubbleTextView btv = (BubbleTextView) v;
- btv.getIconBounds(rect);
- Drawable dr = btv.getIcon();
- if (dr instanceof FastBitmapDrawable) {
- startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
- }
- } else {
- rect.set(0, 0, rect.width(), rect.height());
- }
- viewLocationLeft += rect.left;
- viewLocationTop += rect.top;
- int viewLocationStart = mIsRtl
- ? windowTargetBounds.width() - rect.right
- : viewLocationLeft;
- LayoutParams lp = new LayoutParams(rect.width(), rect.height());
- lp.ignoreInsets = true;
- lp.leftMargin = viewLocationStart;
- lp.topMargin = viewLocationTop;
+ int viewLocationStart = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left;
+ LayoutParams lp = (LayoutParams) mFloatingView.getLayoutParams();
+ // Special RTL logic is needed to handle the window target bounds.
+ lp.leftMargin = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left;
mFloatingView.setLayoutParams(lp);
- // Set the properties here already to make sure they'are available when running the first
- // animation frame.
- mFloatingView.layout(viewLocationLeft, viewLocationTop,
- viewLocationLeft + rect.width(), viewLocationTop + rect.height());
-
- // Swap the two views in place.
- ((ViewGroup) mDragLayer.getParent()).getOverlay().add(mFloatingView);
- if (toggleVisibility) {
- v.setVisibility(View.INVISIBLE);
- }
-
int[] dragLayerBounds = new int[2];
mDragLayer.getLocationOnScreen(dragLayerBounds);
@@ -475,8 +432,8 @@
float xPosition = mIsRtl
? windowTargetBounds.width() - lp.getMarginStart() - rect.width()
: lp.getMarginStart();
- float dX = centerX - xPosition - (lp.width / 2);
- float dY = centerY - lp.topMargin - (lp.height / 2);
+ float dX = centerX - xPosition - (lp.width / 2f);
+ float dY = centerY - lp.topMargin - (lp.height / 2f);
ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX);
ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY);
@@ -502,6 +459,14 @@
float maxScaleX = windowTargetBounds.width() / (float) rect.width();
float maxScaleY = windowTargetBounds.height() / (float) rect.height();
float scale = Math.max(maxScaleX, maxScaleY);
+ float startScale = 1f;
+ if (isBubbleTextView && !(v.getParent() instanceof DeepShortcutView)) {
+ Drawable dr = ((BubbleTextView) v).getIcon();
+ if (dr instanceof FastBitmapDrawable) {
+ startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
+ }
+ }
+
ObjectAnimator scaleAnim = ObjectAnimator
.ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale);
scaleAnim.setDuration(APP_LAUNCH_DURATION)
@@ -521,6 +486,7 @@
alpha.setInterpolator(LINEAR);
appOpenAnimator.play(alpha);
+ appOpenAnimator.addListener(mFloatingView);
appOpenAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 65e71a6..387a09e 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -151,6 +151,11 @@
interface HomeAnimationFactory {
+ /** Return the floating view that will animate in sync with the closing window. */
+ default @Nullable View getFloatingView() {
+ return null;
+ }
+
@NonNull RectF getWindowTargetRect();
@NonNull Animator createActivityAnimationToHome();
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index a3363cd..aeb648d 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -82,9 +82,11 @@
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.launcher3.util.RaceConditionTracker;
import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.views.FloatingIconView;
import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
import com.android.quickstep.ActivityControlHelper.AnimationFactory;
import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState;
+import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory;
import com.android.quickstep.ActivityControlHelper.LayoutListener;
import com.android.quickstep.util.ClipAnimationHelper;
import com.android.quickstep.util.RemoteAnimationTargetSet;
@@ -924,13 +926,13 @@
private void animateToProgressInternal(float start, float end, long duration,
Interpolator interpolator, GestureEndTarget target, float velocityPxPerMs) {
mGestureEndTarget = target;
- ActivityControlHelper.HomeAnimationFactory homeAnimFactory;
+ HomeAnimationFactory homeAnimFactory;
Animator windowAnim;
if (mGestureEndTarget == HOME) {
if (mActivity != null) {
homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity);
} else {
- homeAnimFactory = new ActivityControlHelper.HomeAnimationFactory() {
+ homeAnimFactory = new HomeAnimationFactory() {
@NonNull
@Override
public RectF getWindowTargetRect() {
@@ -948,7 +950,7 @@
mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
isPresent -> mRecentsView.startHome());
}
- windowAnim = createWindowAnimationToHome(start, homeAnimFactory.getWindowTargetRect());
+ windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
mLauncherTransitionController = null;
} else {
windowAnim = mCurrentShift.animateToValue(start, end);
@@ -998,20 +1000,25 @@
/**
* Creates an Animator that transforms the current app window into the home app.
* @param startProgress The progress of {@link #mCurrentShift} to start the window from.
- * @param endTarget Where to animate the window towards.
+ * @param homeAnimationFactory The home animation factory.
*/
- private Animator createWindowAnimationToHome(float startProgress, RectF endTarget) {
+ private Animator createWindowAnimationToHome(float startProgress,
+ HomeAnimationFactory homeAnimationFactory) {
final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
mTransformParams.setProgress(startProgress)));
RectF originalTarget = new RectF(mClipAnimationHelper.getTargetRect());
- final RectF finalTarget = endTarget;
+ final RectF finalTarget = homeAnimationFactory.getWindowTargetRect();
final RectFEvaluator rectFEvaluator = new RectFEvaluator();
final RectF targetRect = new RectF();
final RectF currentRect = new RectF();
+ final View floatingView = homeAnimationFactory.getFloatingView();
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+ if (floatingView instanceof FloatingIconView) {
+ anim.addListener((FloatingIconView) floatingView);
+ }
anim.addUpdateListener(animation -> {
float progress = animation.getAnimatedFraction();
float interpolatedProgress = Interpolators.ACCEL_2.getInterpolation(progress);
@@ -1026,6 +1033,10 @@
mTransformParams.setCurrentRectAndTargetAlpha(currentRect, alpha)
.setSyncTransactionApplier(mSyncTransactionApplier);
mClipAnimationHelper.applyTransform(targetSet, mTransformParams);
+
+ if (floatingView instanceof FloatingIconView) {
+ ((FloatingIconView) floatingView).update(currentRect, 1f - alpha);
+ }
});
anim.addListener(new AnimationSuccessListener() {
@Override
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);
+ }
+ });
+ }
+}