blob: 3fdb256d4cd4b3fa2a784cfcc04081b4671569de [file] [log] [blame]
/*
* 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.util.Executors.MODEL_EXECUTOR;
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.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
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.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherState;
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.statemanager.StateManager.StateListener;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
/** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
public class DragView extends FrameLayout implements StateListener<LauncherState> {
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;
private final int mRegistrationX;
private final int mRegistrationY;
private final float mInitialScale;
private final float mScaleOnDrop;
private final int[] mTempLoc = new int[2];
private final RunnableList mOnDragStartCallback = new RunnableList();
private Point mDragVisualizeOffset = null;
private Rect mDragRegion = null;
private final Launcher mLauncher;
private final DragLayer mDragLayer;
@Thunk final DragController mDragController;
private boolean mHasDrawn = false;
final ValueAnimator mAnim;
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(Launcher 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 launcher The Launcher instance
* @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(Launcher launcher, View content, int width, int height, int registrationX,
int registrationY, final float initialScale, final float scaleOnDrop,
final float finalScaleDps) {
super(launcher);
mLauncher = launcher;
mDragLayer = launcher.getDragLayer();
mDragController = launcher.getDragController();
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));
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();
}
});
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);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mLauncher.getStateManager().addStateListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mLauncher.getStateManager().removeStateListener(this);
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
setVisibility((finalState == LauncherState.NORMAL
|| finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE);
}
/**
* 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_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(mLauncher, info, w, h, outObj);
if (dr instanceof AdaptiveIconDrawable) {
int blurMargin = (int) mLauncher.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.
Rect badgeBounds = new Rect(bounds);
mBadge = getBadge(mLauncher, info, outObj[0]);
mBadge.setBounds(badgeBounds);
// Do not draw the background in case of folder as its translucent
final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);
try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) {
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()) {
FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null);
d.setIsDisabled(true);
mBgSpringDrawable.setColorFilter(d.getColorFilter());
mFgSpringDrawable.setColorFilter(d.getColorFilter());
mBadge.setColorFilter(d.getColorFilter());
}
invalidate();
}));
}
});
}
/**
* Called when pre-drag finishes for an icon
*/
public void onDragStart() {
mOnDragStartCallback.executeAllAndDestroy();
}
// TODO(b/183609936): This is only for LauncherAppWidgetHostView that is rendered in a drawable.
// Once LauncherAppWidgetHostView is directly rendered in this view, removes this method.
@Override
public void invalidate() {
super.invalidate();
if (mContent instanceof ImageView) {
mContent.invalidate();
}
}
@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();
}
}
/**
* 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();
}
public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) {
mTempLoc[0] = toTouchX - mRegistrationX;
mTempLoc[1] = toTouchY - mRegistrationY;
mDragLayer.animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop, mScaleOnDrop,
DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, 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(mLauncher);
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;
}
}
/**
* If the drag view uses color extraction, block it.
*/
public void disableColorExtraction() {
if (mContent instanceof LauncherAppWidgetHostView) {
((LauncherAppWidgetHostView) mContent).disableColorExtraction();
}
}
/**
* If the drag view uses color extraction, restores it.
*/
public void resumeColorExtraction() {
if (mContent instanceof LauncherAppWidgetHostView) {
((LauncherAppWidgetHostView) mContent).enableColorExtraction(/* updateColors= */ false);
}
}
/**
* 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;
}
}