/* * 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 static android.view.Gravity.LEFT; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; import static com.android.launcher3.Utilities.getFullDrawable; import static com.android.launcher3.Utilities.mapToRange; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; import android.animation.Animator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.os.CancellationSignal; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.FrameLayout; import android.widget.ImageView; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.annotation.WorkerThread; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.shortcuts.DeepShortcutView; import java.util.function.Supplier; /** * A view that is created to look like another view with the purpose of creating fluid animations. */ public class FloatingIconView extends FrameLayout implements Animator.AnimatorListener, OnGlobalLayoutListener, FloatingView { private static final String TAG = "FloatingIconView"; // Manages loading the icon on a worker thread private static @Nullable IconLoadResult sIconLoadResult; private static long sFetchIconId = 0; private static long sRecycledFetchIconId = sFetchIconId; public static final float SHAPE_PROGRESS_DURATION = 0.10f; private static final RectF sTmpRectF = new RectF(); private Runnable mEndRunnable; private CancellationSignal mLoadIconSignal; private final Launcher mLauncher; private final boolean mIsRtl; private boolean mIsOpening; private IconLoadResult mIconLoadResult; private View mBtvDrawable; private ClipIconView mClipIconView; private @Nullable Drawable mBadge; // A view whose visibility should update in sync with mOriginalIcon. private @Nullable View mMatchVisibilityView; // A view that will fade out as the animation progresses. private @Nullable View mFadeOutView; private View mOriginalIcon; private RectF mPositionOut; private Runnable mOnTargetChangeRunnable; private final Rect mFinalDrawableBounds = new Rect(); private ListenerView mListenerView; private Runnable mFastFinishRunnable; private float mIconOffsetY; public FloatingIconView(Context context) { this(context, null); } public FloatingIconView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mLauncher = Launcher.getLauncher(context); mIsRtl = Utilities.isRtl(getResources()); mListenerView = new ListenerView(context, attrs); mClipIconView = new ClipIconView(context, attrs); mBtvDrawable = new ImageView(context, attrs); addView(mBtvDrawable); addView(mClipIconView); setWillNotDraw(false); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!mIsOpening) { getViewTreeObserver().addOnGlobalLayoutListener(this); } } @Override protected void onDetachedFromWindow() { getViewTreeObserver().removeOnGlobalLayoutListener(this); super.onDetachedFromWindow(); } /** * Positions this view to match the size and location of {@code rect}. */ public void update(float alpha, RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening) { update(alpha, rect, progress, shapeProgressStart, cornerRadius, isOpening, 0); } /** * Positions this view to match the size and location of {@code rect}. *
* @param alpha The alpha[0, 1] of the entire floating view. * @param progress A value from [0, 1] that represents the animation progress. * @param shapeProgressStart The progress value at which to start the shape reveal. * @param cornerRadius The corner radius of {@code rect}. * @param isOpening True if view is used for app open animation, false for app close animation. * @param taskViewDrawAlpha the drawn {@link com.android.quickstep.views.TaskView} alpha */ public void update(float alpha, RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, int taskViewDrawAlpha) { // The non-running task home animation has some very funky first few frames because this // FIV hasn't fully laid out. During those frames, hide this FIV and continue drawing the // TaskView directly while transforming it in the place of this FIV. However, if we fade // the TaskView at all, we need to display this FIV regardless. setAlpha(!enableAdditionalHomeAnimations() || isLaidOut() || taskViewDrawAlpha < 255 ? alpha : 0f); mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, this, mLauncher.getDeviceProfile(), taskViewDrawAlpha); if (mFadeOutView != null) { // The alpha goes from 1 to 0 when progress is 0 and 0.33 respectively. mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.33f, 0, 1, LINEAR))); } } /** * Sets a {@link com.android.quickstep.views.TaskView} that will draw a * {@link com.android.quickstep.views.TaskView} within the {@code mClipIconView} clip bounds */ public void setOverlayArtist(ClipIconView.TaskViewArtist taskViewArtist) { mClipIconView.setTaskViewArtist(taskViewArtist); } @Override public void onAnimationEnd(Animator animator) { if (mLoadIconSignal != null) { mLoadIconSignal.cancel(); } if (mEndRunnable != null) { mEndRunnable.run(); } else { // End runnable also ends the reveal animator, so we manually handle it here. mClipIconView.endReveal(); } } /** * Sets the size and position of this view to match {@code v}. *
* @param v The view to copy * @param positionOut Rect that will hold the size and position of v. */ private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) { getLocationBoundsForView(launcher, v, isOpening, positionOut); final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams( Math.round(positionOut.width()), Math.round(positionOut.height())); updatePosition(positionOut, lp); setLayoutParams(lp); // For code simplicity, we always layout the child views using Gravity.LEFT // and manually handle RTL for FloatingIconView when positioning it on the screen. mClipIconView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT)); mBtvDrawable.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT)); } private void updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp) { mPositionOut.set(pos); lp.ignoreInsets = true; // Position the floating view exactly on top of the original lp.topMargin = Math.round(pos.top); if (mIsRtl) { lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right)); } else { lp.setMarginStart(Math.round(pos.left)); } // Set the properties here already to make sure they are available when running the first // animation frame. int left = mIsRtl ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width : lp.leftMargin; layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); } private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect) { getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect()); } /** * Gets the location bounds of a view and returns the overall rotation. * - 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, boolean isOpening, RectF outRect, Rect outViewBounds) { boolean ignoreTransform = !isOpening; if (v instanceof BubbleTextHolder) { v = ((BubbleTextHolder) v).getBubbleText(); ignoreTransform = false; } else if (v.getParent() instanceof DeepShortcutView) { v = ((DeepShortcutView) v.getParent()).getIconView(); ignoreTransform = false; } if (v == null) { return; } if (v instanceof BubbleTextView) { ((BubbleTextView) v).getIconBounds(outViewBounds); } else if (v instanceof FolderIcon) { ((FolderIcon) v).getPreviewBounds(outViewBounds); } else { outViewBounds.set(0, 0, v.getWidth(), v.getHeight()); } Utilities.getBoundsForViewInDragLayer(launcher.getDragLayer(), v, outViewBounds, ignoreTransform, null /** recycle */, outRect); } /** * Loads the icon and saves the results to {@link #sIconLoadResult}. *
* Runs onIconLoaded callback (if any), which signifies that the FloatingIconView is * ready to display the icon. Otherwise, the FloatingIconView will grab the results when its * initialized. *
* @param originalView The View that the FloatingIconView will replace.
* @param info ItemInfo of the originalView
* @param pos The position of the view.
* @param btvIcon The drawable of the BubbleTextView. May be null if original view is not a BTV
* @param outIconLoadResult We store the icon results into this object.
*/
@WorkerThread
@SuppressWarnings("WrongThread")
private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos,
@Nullable Drawable btvIcon, IconLoadResult outIconLoadResult) {
Drawable drawable;
boolean supportsAdaptiveIcons = !info.isDisabled(); // Use original icon for disabled icons.
Drawable badge = null;
if (info instanceof SystemShortcut) {
if (originalView instanceof ImageView) {
drawable = ((ImageView) originalView).getDrawable();
} else if (originalView instanceof DeepShortcutView) {
drawable = ((DeepShortcutView) originalView).getIconView().getBackground();
} else {
drawable = originalView.getBackground();
}
} else if (btvIcon instanceof PreloadIconDrawable) {
// Force the progress bar to display.
drawable = btvIcon;
} else if (originalView instanceof ImageView) {
drawable = ((ImageView) originalView).getDrawable();
} else {
int width = (int) pos.width();
int height = (int) pos.height();
Pair
* @param drawable The drawable of the original view.
* @param badge The badge of the original view.
* @param iconOffset The amount of offset needed to match this view with the original view.
*/
@UiThread
private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge,
@Nullable Supplier
* This is used to:
* - Have icon displayed while Adaptive Icon is loading
* - Displays the built in shadow to ensure a clean handoff
*
* Allows nullable as this may be cleared when drawing is deferred to ClipIconView.
*/
private void setOriginalDrawableBackground(@Nullable Supplier
* @param originalView The view to copy
* @param visibilitySyncView A view whose visibility should update in sync with originalView.
* @param fadeOutView A view that will fade out as the animation progresses.
* @param hideOriginal If true, it will hide {@code originalView} while this view is visible.
* Else, we will not draw anything in this view.
* @param positionOut Rect that will hold the size and position of v.
* @param isOpening True if this view replaces the icon for app open animation.
*/
public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView,
@Nullable View visibilitySyncView, @Nullable View fadeOutView, boolean hideOriginal,
RectF positionOut, boolean isOpening) {
final DragLayer dragLayer = launcher.getDragLayer();
ViewGroup parent = (ViewGroup) dragLayer.getParent();
FloatingIconView view = launcher.getViewCache().getView(R.layout.floating_icon_view,
launcher, parent);
view.recycle();
// Init properties before getting the drawable.
view.mIsOpening = isOpening;
view.mOriginalIcon = originalView;
view.mMatchVisibilityView = visibilitySyncView;
view.mFadeOutView = fadeOutView;
view.mPositionOut = positionOut;
// Get the drawable on the background thread
boolean shouldLoadIcon = originalView.getTag() instanceof ItemInfo && hideOriginal;
if (shouldLoadIcon) {
if (sIconLoadResult != null && sIconLoadResult.itemInfo == originalView.getTag()) {
view.mIconLoadResult = sIconLoadResult;
} else {
view.mIconLoadResult = fetchIcon(launcher, originalView,
(ItemInfo) originalView.getTag(), isOpening);
}
view.setOriginalDrawableBackground(view.mIconLoadResult.btvDrawable);
}
resetIconLoadResult();
// Match the position of the original view.
view.matchPositionOf(launcher, originalView, isOpening, positionOut);
// We need to add it to the overlay, but keep it invisible until animation starts..
view.setVisibility(View.INVISIBLE);
parent.addView(view);
dragLayer.addView(view.mListenerView);
view.mListenerView.setListener(view::fastFinish);
view.mEndRunnable = () -> {
view.mEndRunnable = null;
if (view.mFadeOutView != null) {
view.mFadeOutView.setAlpha(1f);
}
if (hideOriginal) {
view.updateViewsVisibility(true /* isVisible */);
view.finish(dragLayer);
} else {
view.finish(dragLayer);
}
};
// Must be called after matchPositionOf so that we know what size to load.
// Must be called after the fastFinish listener and end runnable is created so that
// the icon is not left in a hidden state.
if (shouldLoadIcon) {
view.checkIconResult();
}
return view;
}
private void updateViewsVisibility(boolean isVisible) {
if (mOriginalIcon != null) {
setIconAndDotVisible(mOriginalIcon, isVisible);
}
if (mMatchVisibilityView != null) {
setIconAndDotVisible(mMatchVisibilityView, isVisible);
}
}
private void finish(DragLayer dragLayer) {
((ViewGroup) dragLayer.getParent()).removeView(this);
dragLayer.removeView(mListenerView);
recycle();
mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
}
private void recycle() {
setTranslationX(0);
setTranslationY(0);
setScaleX(1);
setScaleY(1);
setAlpha(1);
if (mLoadIconSignal != null) {
mLoadIconSignal.cancel();
}
mLoadIconSignal = null;
mEndRunnable = null;
mFinalDrawableBounds.setEmpty();
mIsOpening = false;
mPositionOut = null;
mListenerView.setListener(null);
mOriginalIcon = null;
mOnTargetChangeRunnable = null;
mBadge = null;
sRecycledFetchIconId = sFetchIconId;
mIconLoadResult = null;
mClipIconView.recycle();
mBtvDrawable.setBackground(null);
mFastFinishRunnable = null;
mIconOffsetY = 0;
mMatchVisibilityView = null;
mFadeOutView = null;
}
private static class IconLoadResult {
final ItemInfo itemInfo;
final boolean isThemed;
Supplier