39334f4013
Previously we only let the icons take up the max width if the device was in vertical bar layout. For tablets this meant that the icons would be smaller than the actual window crop. We want the full width in any cases where the profile width is greater than the height, so created a new method to check for that. Bug: 203157974 Test: phone/tablet in portrait/landscape Change-Id: I467f142bac87ec7c3b369c01f8d9c96ddf74fc76
389 lines
14 KiB
Java
389 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.launcher3.Utilities.boundToRange;
|
|
import static com.android.launcher3.Utilities.mapToRange;
|
|
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
|
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
|
|
|
|
import static java.lang.Math.max;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.TargetApi;
|
|
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.util.AttributeSet;
|
|
import android.view.View;
|
|
import android.view.ViewGroup.MarginLayoutParams;
|
|
import android.view.ViewOutlineProvider;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.dynamicanimation.animation.FloatPropertyCompat;
|
|
import androidx.dynamicanimation.animation.SpringAnimation;
|
|
import androidx.dynamicanimation.animation.SpringForce;
|
|
|
|
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.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.Q)
|
|
public class ClipIconView extends View implements ClipPathView {
|
|
|
|
private static final Rect sTmpRect = new Rect();
|
|
|
|
// We spring the foreground drawable relative to the icon's movement in the DragLayer.
|
|
// We then use these two factor values to scale the movement of the fg within this view.
|
|
private static final int FG_TRANS_X_FACTOR = 60;
|
|
private static final int FG_TRANS_Y_FACTOR = 75;
|
|
|
|
private static final FloatPropertyCompat<ClipIconView> mFgTransYProperty =
|
|
new FloatPropertyCompat<ClipIconView>("ClipIconViewFgTransY") {
|
|
@Override
|
|
public float getValue(ClipIconView view) {
|
|
return view.mFgTransY;
|
|
}
|
|
|
|
@Override
|
|
public void setValue(ClipIconView view, float transY) {
|
|
view.mFgTransY = transY;
|
|
view.invalidate();
|
|
}
|
|
};
|
|
|
|
private static final FloatPropertyCompat<ClipIconView> mFgTransXProperty =
|
|
new FloatPropertyCompat<ClipIconView>("ClipIconViewFgTransX") {
|
|
@Override
|
|
public float getValue(ClipIconView view) {
|
|
return view.mFgTransX;
|
|
}
|
|
|
|
@Override
|
|
public void setValue(ClipIconView view, float transX) {
|
|
view.mFgTransX = transX;
|
|
view.invalidate();
|
|
}
|
|
};
|
|
|
|
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();
|
|
|
|
private final SpringAnimation mFgSpringY;
|
|
private float mFgTransY;
|
|
private final SpringAnimation mFgSpringX;
|
|
private float mFgTransX;
|
|
|
|
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());
|
|
|
|
mFgSpringX = new SpringAnimation(this, mFgTransXProperty)
|
|
.setSpring(new SpringForce()
|
|
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
|
|
.setStiffness(SpringForce.STIFFNESS_LOW));
|
|
mFgSpringY = new SpringAnimation(this, mFgTransYProperty)
|
|
.setSpring(new SpringForce()
|
|
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
|
|
.setStiffness(SpringForce.STIFFNESS_LOW));
|
|
}
|
|
|
|
/**
|
|
* Update the icon UI to match the provided parameters during an animation frame
|
|
*/
|
|
public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
|
|
int fgIconAlpha, boolean isOpening, View container, DeviceProfile dp) {
|
|
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 (Float.isNaN(scale)) {
|
|
// Views are no longer laid out, do not update.
|
|
return;
|
|
}
|
|
|
|
update(rect, progress, shapeProgressStart, cornerRadius, fgIconAlpha, isOpening, scale,
|
|
minSize, lp, 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,
|
|
int fgIconAlpha, boolean isOpening, float scale, float minSize,
|
|
MarginLayoutParams parentLp, DeviceProfile dp) {
|
|
float dX = mIsRtl
|
|
? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width)
|
|
: rect.left - parentLp.getMarginStart();
|
|
float dY = rect.top - parentLp.topMargin;
|
|
|
|
// 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 = (ValueAnimator) IconShape.getShape().createRevealAnimator(
|
|
this, mStartRevealRect, mOutline, mTaskCornerRadius, !isOpening);
|
|
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();
|
|
}
|
|
mRevealAnimator.setCurrentFraction(shapeRevealProgress);
|
|
}
|
|
|
|
float drawableScale = (dp.isLandscape ? mOutline.width() : mOutline.height())
|
|
/ minSize;
|
|
setBackgroundDrawableBounds(drawableScale, dp.isLandscape);
|
|
if (isOpening) {
|
|
// 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);
|
|
} else {
|
|
mForeground.setAlpha(fgIconAlpha);
|
|
|
|
// Spring the foreground relative to the icon's movement within the DragLayer.
|
|
int diffX = (int) (dX / dp.availableWidthPx * FG_TRANS_X_FACTOR);
|
|
int diffY = (int) (dY / dp.availableHeightPx * FG_TRANS_Y_FACTOR);
|
|
|
|
mFgSpringX.animateToFinalPosition(diffX);
|
|
mFgSpringY.animateToFinalPosition(diffY);
|
|
}
|
|
}
|
|
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.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) {
|
|
int count2 = canvas.save();
|
|
canvas.translate(mFgTransX, mFgTransY);
|
|
mForeground.draw(canvas);
|
|
canvas.restoreToCount(count2);
|
|
}
|
|
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();
|
|
mFgTransY = 0;
|
|
mFgSpringX.cancel();
|
|
mFgTransX = 0;
|
|
mFgSpringY.cancel();
|
|
}
|
|
}
|