Files
lawnchair/src/com/android/launcher3/dragndrop/DragView.java
Tony Wickham 00f89fbbf6 Fix taskbar drag view scale when returning to original icon
- Use getSourceVisualDragBounds() if the target view is BubbleTextView;
  this accounts for padding as well as extra ring inset for
  PredictedAppIcon
- Also ensure we always use the final drag view scale when switching to
  the system drag and drop, instead of using the current scale which
  might be in the process of animating

Test: drag regular and predicted icons in taskbar, but drop it in a
region that doesn't accept it (e.g. the taskbar itself), check that the
return animation scales and offsets more correctly than before
Bug: 269814838

Change-Id: Ie8398b2617340e1d2568773563aa0263a3366940
2023-02-22 03:41:03 +00:00

599 lines
23 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.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;
private final float mEndScale;
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 Runnable mOnAnimEndCallback = null;
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);
}
mEndScale = (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(Utilities.mapRange(value, initialScale, mEndScale));
setScaleY(Utilities.mapRange(value, initialScale, mEndScale));
if (!isAttachedToWindow()) {
animation.cancel();
}
});
mAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mAnimStarted = true;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mOnAnimEndCallback != null) {
mOnAnimEndCallback.run();
}
}
});
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);
}
public void setOnAnimationEndCallback(Runnable callback) {
mOnAnimEndCallback = callback;
}
/**
* 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) {
// 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;
}
ImageView newContent = getViewFromDrawable(getContext(), crossFadeDrawable);
// We need to fill the ImageView with the content, otherwise the shapes of the final view
// and the drag view might not match exactly
newContent.setScaleType(ImageView.ScaleType.FIT_XY);
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;
}
public float getEndScale() {
return mEndScale;
}
@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 = Math.min(
range, view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP);
}
public void animateToPos(float value) {
mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
}
}
private static ImageView 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);
}
}
}
}