Files
Lawnchair/src/com/android/launcher3/dragndrop/DragView.java
T
Jon Miranda 0aa263c5a0 Do not theme icons when the original view is not themed.
Fixes bug where animations uses themed icons in All Apps, where the
icons are not themed.

We want the DragView themed since all valid drop locations will have it
appear themed.

Bug: 215650713
Test: open/close an app from All Apps
Change-Id: I9969ce4921831dd12858ed9b0fe64379e9e3b188
2022-02-15 13:16:22 -08:00

584 lines
22 KiB
Java

/*
* Copyright (C) 2008 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.dragndrop;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.Utilities.getBadge;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Path;
import android.graphics.Picture;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
/** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout {
public static final int VIEW_ZOOM_DURATION = 150;
private final View mContent;
// The following are only used for rendering mContent directly during drag-n-drop.
@Nullable private ViewGroup.LayoutParams mContentViewLayoutParams;
@Nullable private ViewGroup mContentViewParent;
private int mContentViewInParentViewIndex = -1;
private final int mWidth;
private final int mHeight;
private final int mBlurSizeOutline;
protected final int mRegistrationX;
protected final int mRegistrationY;
private final float mInitialScale;
protected final float mScaleOnDrop;
protected final int[] mTempLoc = new int[2];
private final RunnableList mOnDragStartCallback = new RunnableList();
private Point mDragVisualizeOffset = null;
private Rect mDragRegion = null;
protected final T mActivity;
private final BaseDragLayer<T> mDragLayer;
private boolean mHasDrawn = false;
final ValueAnimator mAnim;
// Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends.
private boolean mAnimStarted;
private int mLastTouchX;
private int mLastTouchY;
private int mAnimatedShiftX;
private int mAnimatedShiftY;
// Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true}
private Drawable mBgSpringDrawable, mFgSpringDrawable;
private SpringFloatValue mTranslateX, mTranslateY;
private Path mScaledMaskPath;
private Drawable mBadge;
public DragView(T launcher, Drawable drawable, int registrationX,
int registrationY, final float initialScale, final float scaleOnDrop,
final float finalScaleDps) {
this(launcher, getViewFromDrawable(launcher, drawable),
drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps);
}
/**
* Construct the drag view.
* <p>
* The registration point is the point inside our view that the touch events should
* be centered upon.
* @param activity The Launcher instance/ActivityContext this DragView is in.
* @param content the view content that is attached to the drag view.
* @param width the width of the dragView
* @param height the height of the dragView
* @param initialScale The view that we're dragging around. We scale it up when we draw it.
* @param registrationX The x coordinate of the registration point.
* @param registrationY The y coordinate of the registration point.
* @param scaleOnDrop the scale used in the drop animation.
* @param finalScaleDps the scale used in the zoom out animation when the drag view is shown.
*/
public DragView(T activity, View content, int width, int height, int registrationX,
int registrationY, final float initialScale, final float scaleOnDrop,
final float finalScaleDps) {
super(activity);
mActivity = activity;
mDragLayer = activity.getDragLayer();
mContent = content;
mWidth = width;
mHeight = height;
mContentViewLayoutParams = mContent.getLayoutParams();
if (mContent.getParent() instanceof ViewGroup) {
mContentViewParent = (ViewGroup) mContent.getParent();
mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent);
mContentViewParent.removeView(mContent);
}
addView(content, new LayoutParams(width, height));
// If there is already a scale set on the content, we don't want to clip the children.
if (content.getScaleX() != 1 || content.getScaleY() != 1) {
setClipChildren(false);
setClipToPadding(false);
}
final float scale = (width + finalScaleDps) / width;
// Set the initial scale to avoid any jumps
setScaleX(initialScale);
setScaleY(initialScale);
// Animate the view into the correct position
mAnim = ValueAnimator.ofFloat(0f, 1f);
mAnim.setDuration(VIEW_ZOOM_DURATION);
mAnim.addUpdateListener(animation -> {
final float value = (Float) animation.getAnimatedValue();
setScaleX(initialScale + (value * (scale - initialScale)));
setScaleY(initialScale + (value * (scale - initialScale)));
if (!isAttachedToWindow()) {
animation.cancel();
}
});
mAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mAnimStarted = true;
}
});
setDragRegion(new Rect(0, 0, width, height));
// The point in our scaled bitmap that the touch events are located
mRegistrationX = registrationX;
mRegistrationY = registrationY;
mInitialScale = initialScale;
mScaleOnDrop = scaleOnDrop;
// Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
setElevation(getResources().getDimension(R.dimen.drag_elevation));
setWillNotDraw(false);
}
/**
* Initialize {@code #mIconDrawable} if the item can be represented using
* an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
*/
@TargetApi(Build.VERSION_CODES.O)
public void setItemInfo(final ItemInfo info) {
if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
&& info.itemType != LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION
&& info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
&& info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
return;
}
// Load the adaptive icon on a background thread and add the view in ui thread.
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
Object[] outObj = new Object[1];
int w = mWidth;
int h = mHeight;
Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h,
true /* shouldThemeIcon */, outObj);
if (dr instanceof AdaptiveIconDrawable) {
int blurMargin = (int) mActivity.getResources()
.getDimension(R.dimen.blur_size_medium_outline) / 2;
Rect bounds = new Rect(0, 0, w, h);
bounds.inset(blurMargin, blurMargin);
// Badge is applied after icon normalization so the bounds for badge should not
// be scaled down due to icon normalization.
mBadge = getBadge(mActivity, info, outObj[0]);
FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
// Do not draw the background in case of folder as its translucent
final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);
try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
Drawable nDr; // drawable to be normalized
if (shouldDrawBackground) {
nDr = dr;
} else {
// Since we just want the scale, avoid heavy drawing operations
nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
}
Utilities.scaleRectAboutCenter(bounds,
li.getNormalizer().getScale(nDr, null, null, null));
}
AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
// Shrink very tiny bit so that the clip path is smaller than the original bitmap
// that has anti aliased edges and shadows.
Rect shrunkBounds = new Rect(bounds);
Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
adaptiveIcon.setBounds(shrunkBounds);
final Path mask = adaptiveIcon.getIconMask();
mTranslateX = new SpringFloatValue(DragView.this,
w * AdaptiveIconDrawable.getExtraInsetFraction());
mTranslateY = new SpringFloatValue(DragView.this,
h * AdaptiveIconDrawable.getExtraInsetFraction());
bounds.inset(
(int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
(int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
);
mBgSpringDrawable = adaptiveIcon.getBackground();
if (mBgSpringDrawable == null) {
mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
}
mBgSpringDrawable.setBounds(bounds);
mFgSpringDrawable = adaptiveIcon.getForeground();
if (mFgSpringDrawable == null) {
mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
}
mFgSpringDrawable.setBounds(bounds);
new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> {
// TODO: Consider fade-in animation
// Assign the variable on the UI thread to avoid race conditions.
mScaledMaskPath = mask;
// Avoid relayout as we do not care about children affecting layout
removeAllViewsInLayout();
if (info.isDisabled()) {
ColorFilter filter = getDisabledColorFilter();
mBgSpringDrawable.setColorFilter(filter);
mFgSpringDrawable.setColorFilter(filter);
mBadge.setColorFilter(filter);
}
invalidate();
}));
}
});
}
/**
* Called when pre-drag finishes for an icon
*/
public void onDragStart() {
mOnDragStartCallback.executeAllAndDestroy();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
}
public int getDragRegionWidth() {
return mDragRegion.width();
}
public int getDragRegionHeight() {
return mDragRegion.height();
}
public void setDragVisualizeOffset(Point p) {
mDragVisualizeOffset = p;
}
public Point getDragVisualizeOffset() {
return mDragVisualizeOffset;
}
public void setDragRegion(Rect r) {
mDragRegion = r;
}
public Rect getDragRegion() {
return mDragRegion;
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Draw after the content
mHasDrawn = true;
if (mScaledMaskPath != null) {
int cnt = canvas.save();
canvas.clipPath(mScaledMaskPath);
mBgSpringDrawable.draw(canvas);
canvas.translate(mTranslateX.mValue, mTranslateY.mValue);
mFgSpringDrawable.draw(canvas);
canvas.restoreToCount(cnt);
mBadge.draw(canvas);
}
}
public void crossFadeContent(Drawable crossFadeDrawable, int duration) {
if (mContent.getParent() == null) {
// If the content is already removed, ignore
return;
}
View newContent = getViewFromDrawable(getContext(), crossFadeDrawable);
newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
newContent.layout(0, 0, mWidth, mHeight);
addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight));
AnimatorSet anim = new AnimatorSet();
anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1));
anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0));
anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5);
anim.start();
}
public boolean hasDrawn() {
return mHasDrawn;
}
/**
* Create a window containing this view and show it.
*
* @param touchX the x coordinate the user touched in DragLayer coordinates
* @param touchY the y coordinate the user touched in DragLayer coordinates
*/
public void show(int touchX, int touchY) {
mDragLayer.addView(this);
// Start the pick-up animation
BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight);
lp.customPosition = true;
setLayoutParams(lp);
if (mContent != null) {
// At the drag start, the source view visibility is set to invisible.
mContent.setVisibility(VISIBLE);
}
move(touchX, touchY);
// Post the animation to skip other expensive work happening on the first frame
post(mAnim::start);
}
public void cancelAnimation() {
if (mAnim != null && mAnim.isRunning()) {
mAnim.cancel();
}
}
public boolean isAnimationFinished() {
return mAnimStarted && !mAnim.isRunning();
}
/**
* Move the window containing this view.
*
* @param touchX the x coordinate the user touched in DragLayer coordinates
* @param touchY the y coordinate the user touched in DragLayer coordinates
*/
public void move(int touchX, int touchY) {
if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0
&& mScaledMaskPath != null) {
mTranslateX.animateToPos(mLastTouchX - touchX);
mTranslateY.animateToPos(mLastTouchY - touchY);
}
mLastTouchX = touchX;
mLastTouchY = touchY;
applyTranslation();
}
/**
* Animate this DragView to the given DragLayer coordinates and then remove it.
*/
public abstract void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable,
int duration);
public void animateShift(final int shiftX, final int shiftY) {
if (mAnim.isStarted()) {
return;
}
mAnimatedShiftX = shiftX;
mAnimatedShiftY = shiftY;
applyTranslation();
mAnim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = 1 - animation.getAnimatedFraction();
mAnimatedShiftX = (int) (fraction * shiftX);
mAnimatedShiftY = (int) (fraction * shiftY);
applyTranslation();
}
});
}
private void applyTranslation() {
setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX);
setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
}
/**
* Detaches {@link #mContent}, if previously attached, from this view.
*
* <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to
* {@code true} to attach the {@link #mContent} back to its previous parent.
*/
public void detachContentView(boolean reattachToPreviousParent) {
if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) {
Picture picture = new Picture();
mContent.draw(picture.beginRecording(mWidth, mHeight));
picture.endRecording();
View view = new View(mActivity);
view.setBackground(new PictureDrawable(picture));
view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
view.layout(mContent.getLeft(), mContent.getTop(),
mContent.getRight(), mContent.getBottom());
setClipToOutline(mContent.getClipToOutline());
setOutlineProvider(mContent.getOutlineProvider());
addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true);
removeViewInLayout(mContent);
mContent.setVisibility(INVISIBLE);
mContent.setLayoutParams(mContentViewLayoutParams);
if (reattachToPreviousParent) {
mContentViewParent.addView(mContent, mContentViewInParentViewIndex);
}
mContentViewParent = null;
mContentViewInParentViewIndex = -1;
}
}
/**
* Removes this view from the {@link DragLayer}.
*
* <p>If the drag content is a {@link #mContent}, this call doesn't reattach the
* {@link #mContent} back to its previous parent. To reattach to previous parent, the caller
* should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true
* before this call.
*/
public void remove() {
if (getParent() != null) {
mDragLayer.removeView(DragView.this);
}
}
public int getBlurSizeOutline() {
return mBlurSizeOutline;
}
public float getInitialScale() {
return mInitialScale;
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
/** Returns the current content view that is rendered in the drag view. */
public View getContentView() {
return mContent;
}
/**
* Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag
* content is attached to this view.
*/
@Nullable
public ViewGroup getContentViewParent() {
return mContentViewParent;
}
private static class SpringFloatValue {
private static final FloatPropertyCompat<SpringFloatValue> VALUE =
new FloatPropertyCompat<SpringFloatValue>("value") {
@Override
public float getValue(SpringFloatValue object) {
return object.mValue;
}
@Override
public void setValue(SpringFloatValue object, float value) {
object.mValue = value;
object.mView.invalidate();
}
};
// Following three values are fine tuned with motion ux designer
private static final int STIFFNESS = 4000;
private static final float DAMPENING_RATIO = 1f;
private static final int PARALLAX_MAX_IN_DP = 8;
private final View mView;
private final SpringAnimation mSpring;
private final float mDelta;
private float mValue;
public SpringFloatValue(View view, float range) {
mView = view;
mSpring = new SpringAnimation(this, VALUE, 0)
.setMinValue(-range).setMaxValue(range)
.setSpring(new SpringForce(0)
.setDampingRatio(DAMPENING_RATIO)
.setStiffness(STIFFNESS));
mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP;
}
public void animateToPos(float value) {
mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
}
}
private static View getViewFromDrawable(Context context, Drawable drawable) {
ImageView iv = new ImageView(context);
iv.setImageDrawable(drawable);
return iv;
}
/**
* Removes any stray DragView from the DragLayer.
*/
public static void removeAllViews(ActivityContext activity) {
BaseDragLayer dragLayer = activity.getDragLayer();
// Iterate in reverse order. DragView is added later to the dragLayer,
// and will be one of the last views.
for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
View child = dragLayer.getChildAt(i);
if (child instanceof DragView) {
dragLayer.removeView(child);
}
}
}
}