| /* |
| * 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.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Outline; |
| import android.graphics.Path; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.drawable.AdaptiveIconDrawable; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewOutlineProvider; |
| |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.InsettableFrameLayout.LayoutParams; |
| import com.android.launcher3.ItemInfo; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherModel; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.Interpolators; |
| import com.android.launcher3.dragndrop.DragLayer; |
| import com.android.launcher3.folder.FolderIcon; |
| import com.android.launcher3.folder.FolderShape; |
| import com.android.launcher3.graphics.ShiftedBitmapDrawable; |
| import com.android.launcher3.icons.LauncherIcons; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.WorkerThread; |
| |
| import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; |
| |
| /** |
| * 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, ClipPathView { |
| |
| private static final Rect sTmpRect = new Rect(); |
| |
| private Runnable mStartRunnable; |
| private Runnable mEndRunnable; |
| |
| private Drawable mDrawable; |
| private int mOriginalHeight; |
| private final int mBlurSizeOutline; |
| |
| private boolean mIsAdaptiveIcon = false; |
| |
| private @Nullable Drawable mForeground; |
| private @Nullable Drawable mBackground; |
| private ValueAnimator mRevealAnimator; |
| private final Rect mStartRevealRect = new Rect(); |
| private final Rect mEndRevealRect = new Rect(); |
| private Path mClipPath; |
| protected final Rect mOutline = new Rect(); |
| private final float mTaskCornerRadius; |
| |
| private final Rect mFinalDrawableBounds = new Rect(); |
| private final Rect mBgDrawableBounds = new Rect(); |
| private float mBgDrawableStartScale = 1f; |
| |
| private FloatingIconView(Context context) { |
| super(context); |
| |
| mBlurSizeOutline = context.getResources().getDimensionPixelSize( |
| R.dimen.blur_size_medium_outline); |
| |
| mTaskCornerRadius = 0; // TODO |
| } |
| |
| /** |
| * Positions this view to match the size and location of {@param rect}. |
| * |
| * @param alpha The alpha to set this view. |
| * @param progress A value from [0, 1] that represents the animation progress. |
| * @param windowAlphaThreshold The value at which the window alpha is 0. |
| */ |
| public void update(RectF rect, float alpha, float progress, float windowAlphaThreshold) { |
| 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) lp.width; |
| float scaleY = rect.height() / (float) lp.height; |
| float scale = mIsAdaptiveIcon ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY); |
| setPivotX(0); |
| setPivotY(0); |
| setScaleX(scale); |
| setScaleY(scale); |
| |
| // Wait until the window is no longer visible before morphing the icon into its final shape. |
| float shapeRevealProgress = Utilities.mapToRange(Math.max(windowAlphaThreshold, progress), |
| windowAlphaThreshold, 1f, 0f, 1, Interpolators.LINEAR); |
| if (mIsAdaptiveIcon && shapeRevealProgress > 0) { |
| if (mRevealAnimator == null) { |
| mEndRevealRect.set(mOutline); |
| // We play the reveal animation in reverse so that we end with the icon shape. |
| mRevealAnimator = (ValueAnimator) FolderShape.getShape().createRevealAnimator(this, |
| mStartRevealRect, mEndRevealRect, mTaskCornerRadius / scale, true); |
| mRevealAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mRevealAnimator = null; |
| } |
| }); |
| mRevealAnimator.start(); |
| // We pause here so we can set the current fraction ourselves. |
| mRevealAnimator.pause(); |
| } |
| |
| float bgScale = shapeRevealProgress + mBgDrawableStartScale * (1 - shapeRevealProgress); |
| setBackgroundDrawableBounds(bgScale); |
| |
| mRevealAnimator.setCurrentFraction(shapeRevealProgress); |
| if (Float.compare(shapeRevealProgress, 1f) >= 0f) { |
| mRevealAnimator.end(); |
| } |
| } |
| invalidate(); |
| invalidateOutline(); |
| } |
| |
| @Override |
| public void onAnimationStart(Animator animator) { |
| if (mStartRunnable != null) { |
| mStartRunnable.run(); |
| } |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| if (mEndRunnable != null) { |
| mEndRunnable.run(); |
| } |
| } |
| |
| /** |
| * Sets the size and position of this view to match {@param 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, Rect positionOut) { |
| Utilities.getLocationBoundsForView(launcher, v, positionOut); |
| final LayoutParams lp = new LayoutParams(positionOut.width(), positionOut.height()); |
| lp.ignoreInsets = true; |
| mOriginalHeight = lp.height; |
| |
| // 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); |
| } |
| |
| @WorkerThread |
| private void getIcon(Launcher launcher, View v, ItemInfo info, boolean useDrawableAsIs, |
| float aspectRatio) { |
| final LayoutParams lp = (LayoutParams) getLayoutParams(); |
| |
| boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get() && !useDrawableAsIs |
| && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; |
| if (!supportsAdaptiveIcons && v instanceof BubbleTextView) { |
| // Similar to DragView, we simply use the BubbleTextView icon here. |
| mDrawable = ((BubbleTextView) v).getIcon(); |
| } |
| if (mDrawable == null) { |
| mDrawable = Utilities.getFullDrawable(launcher, info, lp.width, lp.height, |
| useDrawableAsIs, new Object[1]); |
| } |
| |
| if (supportsAdaptiveIcons && mDrawable instanceof AdaptiveIconDrawable) { |
| mIsAdaptiveIcon = true; |
| |
| AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) mDrawable; |
| Drawable background = adaptiveIcon.getBackground(); |
| if (background == null) { |
| background = new ColorDrawable(Color.TRANSPARENT); |
| } |
| mBackground = background; |
| Drawable foreground = adaptiveIcon.getForeground(); |
| if (foreground == null) { |
| foreground = new ColorDrawable(Color.TRANSPARENT); |
| } |
| mForeground = foreground; |
| |
| int offset = getOffsetForAdaptiveIconBounds(); |
| mFinalDrawableBounds.set(offset, offset, lp.width - offset, mOriginalHeight - offset); |
| if (mForeground instanceof ShiftedBitmapDrawable && v instanceof FolderIcon) { |
| ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mForeground; |
| ((FolderIcon) v).getPreviewBounds(sTmpRect); |
| sbd.setShiftX(sbd.getShiftX() - sTmpRect.left); |
| sbd.setShiftY(sbd.getShiftY() - sTmpRect.top); |
| } |
| mForeground.setBounds(mFinalDrawableBounds); |
| mBackground.setBounds(mFinalDrawableBounds); |
| |
| int blurMargin = mBlurSizeOutline / 2; |
| mStartRevealRect.set(blurMargin, blurMargin , lp.width - blurMargin, |
| mOriginalHeight - blurMargin); |
| |
| if (aspectRatio > 0) { |
| lp.height = (int) Math.max(lp.height, lp.width * aspectRatio); |
| layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin |
| + lp.height); |
| } |
| mBgDrawableStartScale = (float) lp.height / mOriginalHeight; |
| setBackgroundDrawableBounds(mBgDrawableStartScale); |
| |
| // Set up outline |
| mOutline.set(0, 0, lp.width, lp.height); |
| setOutlineProvider(new ViewOutlineProvider() { |
| @Override |
| public void getOutline(View view, Outline outline) { |
| outline.setRoundRect(mOutline, mTaskCornerRadius); |
| } |
| }); |
| setClipToOutline(true); |
| } else { |
| setBackground(mDrawable); |
| } |
| |
| new Handler(Looper.getMainLooper()).post(() -> { |
| invalidate(); |
| invalidateOutline(); |
| }); |
| } |
| |
| private void setBackgroundDrawableBounds(float scale) { |
| mBgDrawableBounds.set(mFinalDrawableBounds); |
| Utilities.scaleRectAboutCenter(mBgDrawableBounds, scale); |
| // Since the drawable is at the top of the view, we need to offset to keep it centered. |
| mBgDrawableBounds.offsetTo(mBgDrawableBounds.left, |
| (int) (mFinalDrawableBounds.top * scale)); |
| mBackground.setBounds(mBgDrawableBounds); |
| } |
| |
| private int getOffsetForAdaptiveIconBounds() { |
| if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O || |
| !(mDrawable instanceof AdaptiveIconDrawable)) { |
| return 0; |
| } |
| |
| final LayoutParams lp = (LayoutParams) getLayoutParams(); |
| Rect bounds = new Rect(0, 0, lp.width + mBlurSizeOutline, lp.height + mBlurSizeOutline); |
| bounds.inset(mBlurSizeOutline / 2, mBlurSizeOutline / 2); |
| |
| try (LauncherIcons li = LauncherIcons.obtain(Launcher.fromContext(getContext()))) { |
| Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(mDrawable, null)); |
| } |
| |
| bounds.inset( |
| (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), |
| (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) |
| ); |
| |
| return bounds.left; |
| } |
| |
| @Override |
| public void setClipPath(Path clipPath) { |
| mClipPath = clipPath; |
| invalidate(); |
| } |
| |
| private void drawAdaptiveIconIfExists(Canvas canvas) { |
| if (mBackground != null) { |
| mBackground.draw(canvas); |
| } |
| if (mForeground != null) { |
| mForeground.draw(canvas); |
| } |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (mClipPath == null) { |
| super.draw(canvas); |
| drawAdaptiveIconIfExists(canvas); |
| } else { |
| int count = canvas.save(); |
| canvas.clipPath(mClipPath); |
| super.draw(canvas); |
| drawAdaptiveIconIfExists(canvas); |
| canvas.restoreToCount(count); |
| } |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animator) {} |
| |
| @Override |
| public void onAnimationRepeat(Animator animator) {} |
| |
| /** |
| * Creates a floating icon view for {@param originalView}. |
| * |
| * @param originalView The view to copy |
| * @param hideOriginal If true, it will hide {@param originalView} while this view is visible. |
| * @param useDrawableAsIs If true, we do not separate the foreground/background of adaptive |
| * icons. TODO(b/122843905): We can remove this once app opening uses new animation. |
| * @param aspectRatio If >= 0, we will use this aspect ratio for the initial adaptive icon size. |
| * @param positionOut Rect that will hold the size and position of v. |
| */ |
| public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView, |
| boolean hideOriginal, boolean useDrawableAsIs, float aspectRatio, Rect positionOut, |
| FloatingIconView recycle) { |
| FloatingIconView view = recycle != null ? recycle : new FloatingIconView(launcher); |
| |
| // Match the position of the original view. |
| view.matchPositionOf(launcher, originalView, positionOut); |
| |
| // Get the drawable on the background thread |
| // Must be called after matchPositionOf so that we know what size to load. |
| if (originalView.getTag() instanceof ItemInfo) { |
| new Handler(LauncherModel.getWorkerLooper()).postAtFrontOfQueue(() -> { |
| view.getIcon(launcher, originalView, (ItemInfo) originalView.getTag(), |
| useDrawableAsIs, aspectRatio); |
| }); |
| } |
| |
| // We need to add it to the overlay, but keep it invisible until animation starts.. |
| final DragLayer dragLayer = launcher.getDragLayer(); |
| view.setVisibility(INVISIBLE); |
| ((ViewGroup) dragLayer.getParent()).getOverlay().add(view); |
| |
| view.mStartRunnable = () -> { |
| view.setVisibility(VISIBLE); |
| if (hideOriginal) { |
| originalView.setVisibility(INVISIBLE); |
| } |
| }; |
| view.mEndRunnable = () -> { |
| ((ViewGroup) dragLayer.getParent()).getOverlay().remove(view); |
| if (hideOriginal) { |
| originalView.setVisibility(VISIBLE); |
| } |
| }; |
| return view; |
| } |
| } |