1 package com.android.quickstep.views; 2 3 import static com.android.launcher3.anim.Interpolators.ACCEL; 4 import static com.android.launcher3.anim.Interpolators.DEACCEL_3; 5 import static com.android.launcher3.anim.Interpolators.LINEAR; 6 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows; 7 8 import android.animation.ValueAnimator; 9 import android.content.Context; 10 import android.graphics.Rect; 11 import android.graphics.RectF; 12 import android.util.AttributeSet; 13 import android.view.View; 14 import android.view.ViewGroup; 15 import android.widget.FrameLayout; 16 import android.widget.ImageView; 17 18 import androidx.annotation.Nullable; 19 20 import com.android.launcher3.BaseActivity; 21 import com.android.launcher3.BaseDraggingActivity; 22 import com.android.launcher3.InsettableFrameLayout; 23 import com.android.launcher3.LauncherAnimUtils; 24 import com.android.launcher3.R; 25 import com.android.launcher3.Utilities; 26 import com.android.launcher3.anim.PendingAnimation; 27 import com.android.launcher3.statemanager.StatefulActivity; 28 import com.android.launcher3.touch.PagedOrientationHandler; 29 import com.android.launcher3.views.BaseDragLayer; 30 import com.android.quickstep.util.MultiValueUpdateListener; 31 32 /** 33 * Create an instance via {@link #getFloatingTaskView(StatefulActivity, TaskView, RectF)} to 34 * which will have the thumbnail from the provided existing TaskView overlaying the taskview itself. 35 * 36 * Can then animate the taskview using 37 * {@link #addAnimation(PendingAnimation, RectF, Rect, View, boolean)} 38 * giving a starting and ending bounds. Currently this is set to use the split placeholder view, 39 * but it could be generified. 40 * 41 * TODO: Figure out how to copy thumbnail data from existing TaskView to this view. 42 */ 43 public class FloatingTaskView extends FrameLayout { 44 45 private SplitPlaceholderView mSplitPlaceholderView; 46 private RectF mStartingPosition; 47 private final BaseDraggingActivity mActivity; 48 private final boolean mIsRtl; 49 private final Rect mOutline = new Rect(); 50 private PagedOrientationHandler mOrientationHandler; 51 private ImageView mImageView; 52 FloatingTaskView(Context context)53 public FloatingTaskView(Context context) { 54 this(context, null); 55 } 56 FloatingTaskView(Context context, @Nullable AttributeSet attrs)57 public FloatingTaskView(Context context, @Nullable AttributeSet attrs) { 58 this(context, attrs, 0); 59 } 60 FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)61 public FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 62 super(context, attrs, defStyleAttr); 63 mActivity = BaseActivity.fromContext(context); 64 mIsRtl = Utilities.isRtl(getResources()); 65 } 66 67 @Override onFinishInflate()68 protected void onFinishInflate() { 69 super.onFinishInflate(); 70 mImageView = findViewById(R.id.thumbnail); 71 mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); 72 mImageView.setLayerType(LAYER_TYPE_HARDWARE, null); 73 mSplitPlaceholderView = findViewById(R.id.split_placeholder); 74 mSplitPlaceholderView.setAlpha(0); 75 } 76 init(StatefulActivity launcher, TaskView originalView, RectF positionOut)77 private void init(StatefulActivity launcher, TaskView originalView, RectF positionOut) { 78 mStartingPosition = positionOut; 79 updateInitialPositionForView(originalView); 80 final InsettableFrameLayout.LayoutParams lp = 81 (InsettableFrameLayout.LayoutParams) getLayoutParams(); 82 83 mSplitPlaceholderView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height)); 84 positionOut.round(mOutline); 85 setPivotX(0); 86 setPivotY(0); 87 88 // Copy bounds of exiting thumbnail into ImageView 89 TaskThumbnailView thumbnail = originalView.getThumbnail(); 90 mImageView.setImageBitmap(thumbnail.getThumbnail()); 91 mImageView.setVisibility(VISIBLE); 92 93 mOrientationHandler = originalView.getRecentsView().getPagedOrientationHandler(); 94 mSplitPlaceholderView.setIconView(originalView.getIconView(), 95 launcher.getDeviceProfile().overviewTaskIconDrawableSizePx); 96 mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated()); 97 } 98 99 /** 100 * Configures and returns a an instance of {@link FloatingTaskView} initially matching the 101 * appearance of {@code originalView}. 102 */ getFloatingTaskView(StatefulActivity launcher, TaskView originalView, RectF positionOut)103 public static FloatingTaskView getFloatingTaskView(StatefulActivity launcher, 104 TaskView originalView, RectF positionOut) { 105 final BaseDragLayer dragLayer = launcher.getDragLayer(); 106 ViewGroup parent = (ViewGroup) dragLayer.getParent(); 107 final FloatingTaskView floatingView = (FloatingTaskView) launcher.getLayoutInflater() 108 .inflate(R.layout.floating_split_select_view, parent, false); 109 110 floatingView.init(launcher, originalView, positionOut); 111 parent.addView(floatingView); 112 return floatingView; 113 } 114 updateInitialPositionForView(TaskView originalView)115 public void updateInitialPositionForView(TaskView originalView) { 116 View thumbnail = originalView.getThumbnail(); 117 Rect viewBounds = new Rect(0, 0, thumbnail.getWidth(), thumbnail.getHeight()); 118 Utilities.getBoundsForViewInDragLayer(mActivity.getDragLayer(), thumbnail, viewBounds, 119 true /* ignoreTransform */, null /* recycle */, 120 mStartingPosition); 121 mStartingPosition.offset(originalView.getTranslationX(), originalView.getTranslationY()); 122 final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams( 123 Math.round(mStartingPosition.width()), 124 Math.round(mStartingPosition.height())); 125 initPosition(mStartingPosition, lp); 126 setLayoutParams(lp); 127 } 128 129 // TODO(194414938) set correct corner radii update(RectF position, float progress, float windowRadius)130 public void update(RectF position, float progress, float windowRadius) { 131 MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); 132 133 float dX = position.left - mStartingPosition.left; 134 float dY = position.top - lp.topMargin; 135 136 setTranslationX(dX); 137 setTranslationY(dY); 138 139 float scaleX = position.width() / lp.width; 140 float scaleY = position.height() / lp.height; 141 setScaleX(scaleX); 142 setScaleY(scaleY); 143 float childScaleX = 1f / scaleX; 144 float childScaleY = 1f / scaleY; 145 146 invalidate(); 147 // TODO(194414938) seems like this scale value could be fine tuned, some stretchiness 148 mImageView.setScaleX(1f / scaleX + scaleX * progress); 149 mImageView.setScaleY(1f / scaleY + scaleY * progress); 150 mOrientationHandler.setPrimaryScale(mSplitPlaceholderView.getIconView(), childScaleX); 151 mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY); 152 } 153 updateOrientationHandler(PagedOrientationHandler orientationHandler)154 public void updateOrientationHandler(PagedOrientationHandler orientationHandler) { 155 mOrientationHandler = orientationHandler; 156 mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated()); 157 } 158 initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp)159 protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) { 160 mStartingPosition.set(pos); 161 lp.ignoreInsets = true; 162 // Position the floating view exactly on top of the original 163 lp.topMargin = Math.round(pos.top); 164 if (mIsRtl) { 165 lp.setMarginStart(mActivity.getDeviceProfile().widthPx - Math.round(pos.right)); 166 } else { 167 lp.setMarginStart(Math.round(pos.left)); 168 } 169 170 // Set the properties here already to make sure they are available when running the first 171 // animation frame. 172 int left = (int) pos.left; 173 layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); 174 } 175 addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, View viewToCover, boolean fadeWithThumbnail)176 public void addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, 177 View viewToCover, boolean fadeWithThumbnail) { 178 final BaseDragLayer dragLayer = mActivity.getDragLayer(); 179 int[] dragLayerBounds = new int[2]; 180 dragLayer.getLocationOnScreen(dragLayerBounds); 181 SplitOverlayProperties prop = new SplitOverlayProperties(endBounds, 182 startingBounds, viewToCover, dragLayerBounds[0], 183 dragLayerBounds[1]); 184 185 ValueAnimator transitionAnimator = ValueAnimator.ofFloat(0, 1); 186 animation.add(transitionAnimator); 187 long animDuration = animation.getDuration(); 188 Rect crop = new Rect(); 189 RectF floatingTaskViewBounds = new RectF(); 190 final float initialWindowRadius = supportsRoundedCornersOnWindows(getResources()) 191 ? Math.max(crop.width(), crop.height()) / 2f 192 : 0f; 193 194 if (fadeWithThumbnail) { 195 animation.addFloat(mSplitPlaceholderView, SplitPlaceholderView.ALPHA_FLOAT, 196 0, 1, ACCEL); 197 animation.addFloat(mImageView, LauncherAnimUtils.VIEW_ALPHA, 198 1, 0, DEACCEL_3); 199 } 200 201 MultiValueUpdateListener listener = new MultiValueUpdateListener() { 202 final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, 203 initialWindowRadius, 0, animDuration, LINEAR); 204 final FloatProp mDx = new FloatProp(0, prop.dX, 0, animDuration, LINEAR); 205 final FloatProp mDy = new FloatProp(0, prop.dY, 0, animDuration, LINEAR); 206 final FloatProp mTaskViewScaleX = new FloatProp(prop.initialTaskViewScaleX, 207 prop.finalTaskViewScaleX, 0, animDuration, LINEAR); 208 final FloatProp mTaskViewScaleY = new FloatProp(prop.initialTaskViewScaleY, 209 prop.finalTaskViewScaleY, 0, animDuration, LINEAR); 210 @Override 211 public void onUpdate(float percent, boolean initOnly) { 212 // Calculate the icon position. 213 floatingTaskViewBounds.set(startingBounds); 214 floatingTaskViewBounds.offset(mDx.value, mDy.value); 215 Utilities.scaleRectFAboutCenter(floatingTaskViewBounds, mTaskViewScaleX.value, 216 mTaskViewScaleY.value); 217 218 update(floatingTaskViewBounds, percent, mWindowRadius.value * 1); 219 } 220 }; 221 transitionAnimator.addUpdateListener(listener); 222 } 223 224 private static class SplitOverlayProperties { 225 226 private final float initialTaskViewScaleX; 227 private final float initialTaskViewScaleY; 228 private final float finalTaskViewScaleX; 229 private final float finalTaskViewScaleY; 230 private final float dX; 231 private final float dY; 232 SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, View view, int dragLayerLeft, int dragLayerTop)233 SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, View view, 234 int dragLayerLeft, int dragLayerTop) { 235 float maxScaleX = endBounds.width() / startTaskViewBounds.width(); 236 float maxScaleY = endBounds.height() / startTaskViewBounds.height(); 237 238 initialTaskViewScaleX = view.getScaleX(); 239 initialTaskViewScaleY = view.getScaleY(); 240 finalTaskViewScaleX = maxScaleX; 241 finalTaskViewScaleY = maxScaleY; 242 243 // Animate the app icon to the center of the window bounds in screen coordinates. 244 float centerX = endBounds.centerX() - dragLayerLeft; 245 float centerY = endBounds.centerY() - dragLayerTop; 246 247 dX = centerX - startTaskViewBounds.centerX(); 248 dY = centerY - startTaskViewBounds.centerY(); 249 } 250 } 251 } 252