mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-28 15:56:49 +00:00
Flag: com.android.launcher3.enable_additional_home_animations Fixes: 343051344 Test: temp logs, launched apps from home, quickly swiped home while quick switching (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:9fb1f8043573d5bc3376f0786354fc8f7a9cf2cd) Merged-In: I91071d53f753eb7bc99305c46241de203bcfdc98 Change-Id: I91071d53f753eb7bc99305c46241de203bcfdc98
386 lines
14 KiB
Java
386 lines
14 KiB
Java
/*
|
|
* Copyright (C) 2020 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 com.android.app.animation.Interpolators.LINEAR;
|
|
import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
|
|
import static com.android.launcher3.Utilities.boundToRange;
|
|
import static com.android.launcher3.Utilities.mapToRange;
|
|
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
|
|
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
|
|
|
|
import static java.lang.Math.max;
|
|
|
|
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.util.AttributeSet;
|
|
import android.view.View;
|
|
import android.view.ViewGroup.MarginLayoutParams;
|
|
import android.view.ViewOutlineProvider;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.core.util.Consumer;
|
|
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
|
|
import com.android.launcher3.graphics.IconShape;
|
|
|
|
/**
|
|
* A view used to draw both layers of an {@link AdaptiveIconDrawable}.
|
|
* Supports springing just the foreground layer.
|
|
* Supports clipping the icon to/from its icon shape.
|
|
*/
|
|
public class ClipIconView extends View implements ClipPathView {
|
|
|
|
private static final Rect sTmpRect = new Rect();
|
|
|
|
private final int mBlurSizeOutline;
|
|
private final boolean mIsRtl;
|
|
|
|
private @Nullable Drawable mForeground;
|
|
private @Nullable Drawable mBackground;
|
|
|
|
private boolean mIsAdaptiveIcon = false;
|
|
|
|
private ValueAnimator mRevealAnimator;
|
|
|
|
private final Rect mStartRevealRect = new Rect();
|
|
private final Rect mEndRevealRect = new Rect();
|
|
private Path mClipPath;
|
|
private float mTaskCornerRadius;
|
|
|
|
private final Rect mOutline = new Rect();
|
|
private final Rect mFinalDrawableBounds = new Rect();
|
|
|
|
@Nullable private TaskViewArtist mTaskViewArtist;
|
|
|
|
public ClipIconView(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public ClipIconView(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
super(context, attrs, defStyleAttr);
|
|
mBlurSizeOutline = getResources().getDimensionPixelSize(
|
|
R.dimen.blur_size_medium_outline);
|
|
mIsRtl = Utilities.isRtl(getResources());
|
|
}
|
|
|
|
/**
|
|
* Sets a {@link TaskViewArtist} that will draw a {@link com.android.quickstep.views.TaskView}
|
|
* within the clip bounds of this view.
|
|
*/
|
|
public void setTaskViewArtist(TaskViewArtist taskViewArtist) {
|
|
if (!enableAdditionalHomeAnimations()) {
|
|
return;
|
|
}
|
|
mTaskViewArtist = taskViewArtist;
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* Update the icon UI to match the provided parameters during an animation frame
|
|
*/
|
|
public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
|
|
boolean isOpening, View container, DeviceProfile dp) {
|
|
update(rect, progress, shapeProgressStart, cornerRadius, isOpening, container, dp, 255);
|
|
}
|
|
|
|
/**
|
|
* Update the icon UI to match the provided parameters during an animation frame, optionally
|
|
* varying the alpha of the {@link TaskViewArtist}
|
|
*/
|
|
public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
|
|
boolean isOpening, View container, DeviceProfile dp, int taskViewDrawAlpha) {
|
|
MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams();
|
|
|
|
float dX = mIsRtl
|
|
? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
|
|
: rect.left - lp.getMarginStart();
|
|
float dY = rect.top - lp.topMargin;
|
|
container.setTranslationX(dX);
|
|
container.setTranslationY(dY);
|
|
|
|
float minSize = Math.min(lp.width, lp.height);
|
|
float scaleX = rect.width() / minSize;
|
|
float scaleY = rect.height() / minSize;
|
|
float scale = Math.max(1f, Math.min(scaleX, scaleY));
|
|
if (mTaskViewArtist != null) {
|
|
mTaskViewArtist.taskViewDrawWidth = lp.width;
|
|
mTaskViewArtist.taskViewDrawHeight = lp.height;
|
|
mTaskViewArtist.taskViewDrawAlpha = taskViewDrawAlpha;
|
|
mTaskViewArtist.taskViewDrawScale = (mTaskViewArtist.drawForPortraitLayout
|
|
? Math.min(lp.height, lp.width) : Math.max(lp.height, lp.width))
|
|
/ mTaskViewArtist.taskViewMinSize;
|
|
}
|
|
|
|
if (Float.isNaN(scale) || Float.isInfinite(scale)) {
|
|
// Views are no longer laid out, do not update.
|
|
return;
|
|
}
|
|
|
|
update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale, minSize, dp);
|
|
|
|
container.setPivotX(0);
|
|
container.setPivotY(0);
|
|
container.setScaleX(scale);
|
|
container.setScaleY(scale);
|
|
|
|
container.invalidate();
|
|
}
|
|
|
|
private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
|
|
boolean isOpening, float scale, float minSize, DeviceProfile dp) {
|
|
// shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
|
|
float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
|
|
|
|
float shapeRevealProgress = boundToRange(mapToRange(max(shapeProgressStart, progress),
|
|
shapeProgressStart, 1f, 0, toMax, LINEAR), 0, 1);
|
|
|
|
if (dp.isLandscape) {
|
|
mOutline.right = (int) (rect.width() / scale);
|
|
} else {
|
|
mOutline.bottom = (int) (rect.height() / scale);
|
|
}
|
|
|
|
mTaskCornerRadius = cornerRadius / scale;
|
|
if (mIsAdaptiveIcon) {
|
|
if (!isOpening && progress >= shapeProgressStart) {
|
|
if (mRevealAnimator == null) {
|
|
mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
|
|
.createRevealAnimator(this, mStartRevealRect,
|
|
mOutline, mTaskCornerRadius, !isOpening);
|
|
mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
|
|
mRevealAnimator.start();
|
|
// We pause here so we can set the current fraction ourselves.
|
|
mRevealAnimator.pause();
|
|
}
|
|
mRevealAnimator.setCurrentFraction(shapeRevealProgress);
|
|
}
|
|
|
|
float drawableScale = (dp.isLandscape ? mOutline.width() : mOutline.height())
|
|
/ minSize;
|
|
setBackgroundDrawableBounds(drawableScale, dp.isLandscape);
|
|
|
|
// Center align foreground
|
|
int height = mFinalDrawableBounds.height();
|
|
int width = mFinalDrawableBounds.width();
|
|
int diffY = dp.isLandscape ? 0
|
|
: (int) (((height * drawableScale) - height) / 2);
|
|
int diffX = dp.isLandscape ? (int) (((width * drawableScale) - width) / 2)
|
|
: 0;
|
|
sTmpRect.set(mFinalDrawableBounds);
|
|
sTmpRect.offset(diffX, diffY);
|
|
mForeground.setBounds(sTmpRect);
|
|
}
|
|
invalidate();
|
|
invalidateOutline();
|
|
}
|
|
|
|
private void setBackgroundDrawableBounds(float scale, boolean isLandscape) {
|
|
sTmpRect.set(mFinalDrawableBounds);
|
|
Utilities.scaleRectAboutCenter(sTmpRect, scale);
|
|
// Since the drawable is at the top of the view, we need to offset to keep it centered.
|
|
if (isLandscape) {
|
|
sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top);
|
|
} else {
|
|
sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale));
|
|
}
|
|
mBackground.setBounds(sTmpRect);
|
|
}
|
|
|
|
protected void endReveal() {
|
|
if (mRevealAnimator != null) {
|
|
mRevealAnimator.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the icon for this view as part of initial setup
|
|
*/
|
|
public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp,
|
|
boolean isOpening, DeviceProfile dp) {
|
|
mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
|
|
if (mIsAdaptiveIcon) {
|
|
boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
|
|
|
|
AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
|
|
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;
|
|
|
|
final int originalHeight = lp.height;
|
|
final int originalWidth = lp.width;
|
|
|
|
int blurMargin = mBlurSizeOutline / 2;
|
|
mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
|
|
|
|
if (!isFolderIcon) {
|
|
mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
|
|
}
|
|
mForeground.setBounds(mFinalDrawableBounds);
|
|
mBackground.setBounds(mFinalDrawableBounds);
|
|
|
|
mStartRevealRect.set(0, 0, originalWidth, originalHeight);
|
|
|
|
if (!isFolderIcon) {
|
|
Utilities.scaleRectAboutCenter(mStartRevealRect,
|
|
IconShape.INSTANCE.get(getContext()).getNormalizationScale());
|
|
}
|
|
|
|
if (dp.isLandscape) {
|
|
lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio);
|
|
} else {
|
|
lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio);
|
|
}
|
|
|
|
int left = mIsRtl
|
|
? dp.widthPx - lp.getMarginStart() - lp.width
|
|
: lp.leftMargin;
|
|
layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
|
|
|
|
float scale = Math.max((float) lp.height / originalHeight,
|
|
(float) lp.width / originalWidth);
|
|
float bgDrawableStartScale;
|
|
if (isOpening) {
|
|
bgDrawableStartScale = 1f;
|
|
mOutline.set(0, 0, originalWidth, originalHeight);
|
|
} else {
|
|
bgDrawableStartScale = scale;
|
|
mOutline.set(0, 0, lp.width, lp.height);
|
|
}
|
|
setBackgroundDrawableBounds(bgDrawableStartScale, dp.isLandscape);
|
|
mEndRevealRect.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(drawable);
|
|
setClipToOutline(false);
|
|
}
|
|
|
|
invalidate();
|
|
invalidateOutline();
|
|
}
|
|
|
|
@Override
|
|
public void setClipPath(Path clipPath) {
|
|
mClipPath = clipPath;
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
public void draw(Canvas canvas) {
|
|
int count = canvas.save();
|
|
if (mClipPath != null) {
|
|
canvas.clipPath(mClipPath);
|
|
}
|
|
super.draw(canvas);
|
|
if (mBackground != null) {
|
|
mBackground.draw(canvas);
|
|
}
|
|
if (mForeground != null) {
|
|
mForeground.draw(canvas);
|
|
}
|
|
if (mTaskViewArtist != null) {
|
|
canvas.saveLayerAlpha(
|
|
0,
|
|
0,
|
|
mTaskViewArtist.taskViewDrawWidth,
|
|
mTaskViewArtist.taskViewDrawHeight,
|
|
mTaskViewArtist.taskViewDrawAlpha);
|
|
float drawScale = mTaskViewArtist.taskViewDrawScale;
|
|
canvas.translate(drawScale * mTaskViewArtist.taskViewTranslationX,
|
|
drawScale * mTaskViewArtist.taskViewTranslationY);
|
|
canvas.scale(drawScale, drawScale);
|
|
mTaskViewArtist.taskViewDrawCallback.accept(canvas);
|
|
}
|
|
canvas.restoreToCount(count);
|
|
}
|
|
|
|
void recycle() {
|
|
setBackground(null);
|
|
mIsAdaptiveIcon = false;
|
|
mForeground = null;
|
|
mBackground = null;
|
|
mClipPath = null;
|
|
mFinalDrawableBounds.setEmpty();
|
|
if (mRevealAnimator != null) {
|
|
mRevealAnimator.cancel();
|
|
}
|
|
mRevealAnimator = null;
|
|
mTaskCornerRadius = 0;
|
|
mOutline.setEmpty();
|
|
mTaskViewArtist = null;
|
|
}
|
|
|
|
/**
|
|
* Utility class to help draw a {@link com.android.quickstep.views.TaskView} within
|
|
* a {@link ClipIconView} bounds.
|
|
*/
|
|
public static class TaskViewArtist {
|
|
|
|
public final Consumer<Canvas> taskViewDrawCallback;
|
|
public final float taskViewTranslationX;
|
|
public final float taskViewTranslationY;
|
|
public final float taskViewMinSize;
|
|
public final boolean drawForPortraitLayout;
|
|
|
|
public int taskViewDrawAlpha;
|
|
public float taskViewDrawScale;
|
|
public int taskViewDrawWidth;
|
|
public int taskViewDrawHeight;
|
|
|
|
public TaskViewArtist(
|
|
Consumer<Canvas> taskViewDrawCallback,
|
|
float taskViewTranslationX,
|
|
float taskViewTranslationY,
|
|
float taskViewMinSize,
|
|
boolean drawForPortraitLayout) {
|
|
this.taskViewDrawCallback = taskViewDrawCallback;
|
|
this.taskViewTranslationX = taskViewTranslationX;
|
|
this.taskViewTranslationY = taskViewTranslationY;
|
|
this.taskViewMinSize = taskViewMinSize;
|
|
this.drawForPortraitLayout = drawForPortraitLayout;
|
|
}
|
|
}
|
|
}
|