1 package com.android.quickstep.views; 2 3 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; 4 import static com.android.launcher3.anim.Interpolators.LINEAR; 5 import static com.android.launcher3.anim.Interpolators.clampToProgress; 6 7 import android.animation.ValueAnimator; 8 import android.content.Context; 9 import android.graphics.Bitmap; 10 import android.graphics.Canvas; 11 import android.graphics.Paint; 12 import android.graphics.Rect; 13 import android.graphics.RectF; 14 import android.graphics.drawable.BitmapDrawable; 15 import android.graphics.drawable.Drawable; 16 import android.util.AttributeSet; 17 import android.util.FloatProperty; 18 import android.view.View; 19 import android.view.ViewGroup; 20 import android.widget.FrameLayout; 21 22 import androidx.annotation.Nullable; 23 24 import com.android.launcher3.AbstractFloatingView; 25 import com.android.launcher3.BaseActivity; 26 import com.android.launcher3.InsettableFrameLayout; 27 import com.android.launcher3.R; 28 import com.android.launcher3.Utilities; 29 import com.android.launcher3.anim.PendingAnimation; 30 import com.android.launcher3.statemanager.StatefulActivity; 31 import com.android.launcher3.taskbar.TaskbarActivityContext; 32 import com.android.launcher3.touch.PagedOrientationHandler; 33 import com.android.launcher3.util.SplitConfigurationOptions; 34 import com.android.launcher3.views.BaseDragLayer; 35 import com.android.quickstep.util.AnimUtils; 36 import com.android.quickstep.util.MultiValueUpdateListener; 37 import com.android.quickstep.util.SplitAnimationTimings; 38 import com.android.quickstep.util.TaskCornerRadius; 39 import com.android.systemui.shared.system.QuickStepContract; 40 41 /** 42 * Create an instance via 43 * {@link #getFloatingTaskView(StatefulActivity, View, Bitmap, Drawable, RectF)} to 44 * which will have the thumbnail from the provided existing TaskView overlaying the taskview itself. 45 * 46 * Can then animate the taskview using 47 * {@link #addStagingAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} or 48 * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} 49 * giving a starting and ending bounds. Currently this is set to use the split placeholder view, 50 * but it could be generified. 51 * 52 * TODO: Figure out how to copy thumbnail data from existing TaskView to this view. 53 */ 54 public class FloatingTaskView extends FrameLayout { 55 56 public static final FloatProperty<FloatingTaskView> PRIMARY_TRANSLATE_OFFSCREEN = 57 new FloatProperty<FloatingTaskView>("floatingTaskPrimaryTranslateOffscreen") { 58 @Override 59 public void setValue(FloatingTaskView view, float translation) { 60 ((RecentsView) view.mActivity.getOverviewPanel()).getPagedOrientationHandler() 61 .setFloatingTaskPrimaryTranslation( 62 view, 63 translation, 64 view.mActivity.getDeviceProfile() 65 ); 66 } 67 68 @Override 69 public Float get(FloatingTaskView view) { 70 return ((RecentsView) view.mActivity.getOverviewPanel()) 71 .getPagedOrientationHandler() 72 .getFloatingTaskPrimaryTranslation( 73 view, 74 view.mActivity.getDeviceProfile() 75 ); 76 } 77 }; 78 79 private int mSplitHolderSize; 80 private FloatingTaskThumbnailView mThumbnailView; 81 private SplitPlaceholderView mSplitPlaceholderView; 82 private RectF mStartingPosition; 83 private final StatefulActivity mActivity; 84 private final boolean mIsRtl; 85 private final FullscreenDrawParams mFullscreenParams; 86 private PagedOrientationHandler mOrientationHandler; 87 @SplitConfigurationOptions.StagePosition 88 private int mStagePosition; 89 private final Rect mTmpRect = new Rect(); 90 FloatingTaskView(Context context)91 public FloatingTaskView(Context context) { 92 this(context, null); 93 } 94 FloatingTaskView(Context context, @Nullable AttributeSet attrs)95 public FloatingTaskView(Context context, @Nullable AttributeSet attrs) { 96 this(context, attrs, 0); 97 } 98 FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)99 public FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 100 super(context, attrs, defStyleAttr); 101 mActivity = BaseActivity.fromContext(context); 102 mIsRtl = Utilities.isRtl(getResources()); 103 mFullscreenParams = new FullscreenDrawParams(context); 104 105 mSplitHolderSize = context.getResources().getDimensionPixelSize( 106 R.dimen.split_placeholder_icon_size); 107 } 108 109 @Override onFinishInflate()110 protected void onFinishInflate() { 111 super.onFinishInflate(); 112 mThumbnailView = findViewById(R.id.thumbnail); 113 mSplitPlaceholderView = findViewById(R.id.split_placeholder); 114 mSplitPlaceholderView.setAlpha(0); 115 } 116 init(StatefulActivity launcher, View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut)117 private void init(StatefulActivity launcher, View originalView, @Nullable Bitmap thumbnail, 118 Drawable icon, RectF positionOut) { 119 mStartingPosition = positionOut; 120 updateInitialPositionForView(originalView); 121 final InsettableFrameLayout.LayoutParams lp = 122 (InsettableFrameLayout.LayoutParams) getLayoutParams(); 123 124 mSplitPlaceholderView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height)); 125 setPivotX(0); 126 setPivotY(0); 127 128 // Copy bounds of exiting thumbnail into ImageView 129 mThumbnailView.setThumbnail(thumbnail); 130 131 mThumbnailView.setVisibility(VISIBLE); 132 133 RecentsView recentsView = launcher.getOverviewPanel(); 134 mOrientationHandler = recentsView.getPagedOrientationHandler(); 135 mStagePosition = recentsView.getSplitSelectController().getActiveSplitStagePosition(); 136 mSplitPlaceholderView.setIcon(icon, mSplitHolderSize); 137 mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated()); 138 } 139 140 /** 141 * Configures and returns a an instance of {@link FloatingTaskView} initially matching the 142 * appearance of {@code originalView}. 143 */ getFloatingTaskView(StatefulActivity launcher, View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut)144 public static FloatingTaskView getFloatingTaskView(StatefulActivity launcher, 145 View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut) { 146 final ViewGroup dragLayer = launcher.getDragLayer(); 147 final FloatingTaskView floatingView = (FloatingTaskView) launcher.getLayoutInflater() 148 .inflate(R.layout.floating_split_select_view, dragLayer, false); 149 150 floatingView.init(launcher, originalView, thumbnail, icon, positionOut); 151 // Add this animating view underneath the existing open task menu view (if there is one) 152 View openTaskView = AbstractFloatingView.getOpenView(launcher, TYPE_TASK_MENU); 153 int openTaskViewIndex = dragLayer.indexOfChild(openTaskView); 154 if (openTaskViewIndex == -1) { 155 // Add to top if not 156 openTaskViewIndex = dragLayer.getChildCount(); 157 } 158 dragLayer.addView(floatingView, openTaskViewIndex - 1); 159 return floatingView; 160 } 161 updateInitialPositionForView(View originalView)162 public void updateInitialPositionForView(View originalView) { 163 if (originalView.getContext() instanceof TaskbarActivityContext) { 164 // If original View is a button on the Taskbar, find the on-screen bounds and calculate 165 // the equivalent bounds in the DragLayer, so we can set the initial position of 166 // this FloatingTaskView and start the split animation at the correct spot. 167 originalView.getBoundsOnScreen(mTmpRect); 168 mStartingPosition.set(mTmpRect); 169 int[] dragLayerPositionRelativeToScreen = 170 mActivity.getDragLayer().getLocationOnScreen(); 171 mStartingPosition.offset( 172 -dragLayerPositionRelativeToScreen[0], 173 -dragLayerPositionRelativeToScreen[1]); 174 } else { 175 Rect viewBounds = new Rect(0, 0, originalView.getWidth(), originalView.getHeight()); 176 Utilities.getBoundsForViewInDragLayer(mActivity.getDragLayer(), originalView, 177 viewBounds, false /* ignoreTransform */, null /* recycle */, 178 mStartingPosition); 179 } 180 181 final BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams( 182 Math.round(mStartingPosition.width()), 183 Math.round(mStartingPosition.height())); 184 initPosition(mStartingPosition, lp); 185 setLayoutParams(lp); 186 } 187 update(RectF bounds, float progress)188 public void update(RectF bounds, float progress) { 189 MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams(); 190 191 float dX = bounds.left - mStartingPosition.left; 192 float dY = bounds.top - lp.topMargin; 193 float scaleX = bounds.width() / lp.width; 194 float scaleY = bounds.height() / lp.height; 195 196 mFullscreenParams.updateParams(bounds, progress, scaleX, scaleY); 197 198 setTranslationX(dX); 199 setTranslationY(dY); 200 setScaleX(scaleX); 201 setScaleY(scaleY); 202 mSplitPlaceholderView.invalidate(); 203 mThumbnailView.invalidate(); 204 205 float childScaleX = 1f / scaleX; 206 float childScaleY = 1f / scaleY; 207 mOrientationHandler.setPrimaryScale(mSplitPlaceholderView.getIconView(), childScaleX); 208 mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY); 209 } 210 updateOrientationHandler(PagedOrientationHandler orientationHandler)211 public void updateOrientationHandler(PagedOrientationHandler orientationHandler) { 212 mOrientationHandler = orientationHandler; 213 mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated()); 214 } 215 setIcon(Bitmap icon)216 public void setIcon(Bitmap icon) { 217 mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize); 218 } 219 initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp)220 protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) { 221 mStartingPosition.set(pos); 222 lp.ignoreInsets = true; 223 // Position the floating view exactly on top of the original 224 lp.topMargin = Math.round(pos.top); 225 if (mIsRtl) { 226 lp.setMarginStart(mActivity.getDeviceProfile().widthPx - Math.round(pos.right)); 227 } else { 228 lp.setMarginStart(Math.round(pos.left)); 229 } 230 231 // Set the properties here already to make sure they are available when running the first 232 // animation frame. 233 int left = (int) pos.left; 234 layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); 235 } 236 237 /** 238 * Animates a FloatingTaskThumbnailView and its overlapping SplitPlaceholderView when a split 239 * is staged. 240 */ addStagingAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask)241 public void addStagingAnimation(PendingAnimation animation, RectF startingBounds, 242 Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) { 243 boolean isTablet = mActivity.getDeviceProfile().isTablet; 244 boolean splittingFromOverview = fadeWithThumbnail; 245 SplitAnimationTimings timings; 246 247 if (isTablet && splittingFromOverview) { 248 timings = SplitAnimationTimings.TABLET_OVERVIEW_TO_SPLIT; 249 } else if (!isTablet && splittingFromOverview) { 250 timings = SplitAnimationTimings.PHONE_OVERVIEW_TO_SPLIT; 251 } else { 252 // Splitting from Home is currently only available on tablets 253 timings = SplitAnimationTimings.TABLET_HOME_TO_SPLIT; 254 } 255 256 addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask, 257 timings); 258 } 259 260 /** 261 * Animates the FloatingTaskThumbnailView and SplitPlaceholderView for the two thumbnails 262 * when a split is confirmed. 263 */ addConfirmAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask)264 public void addConfirmAnimation(PendingAnimation animation, RectF startingBounds, 265 Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) { 266 SplitAnimationTimings timings = 267 AnimUtils.getDeviceSplitToConfirmTimings(mActivity.getDeviceProfile().isTablet); 268 269 addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask, 270 timings); 271 } 272 273 /** 274 * Sets up and builds a split staging animation. 275 * Called by {@link #addStagingAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} and 276 * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)}. 277 */ addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask, SplitAnimationTimings timings)278 public void addAnimation(PendingAnimation animation, RectF startingBounds, 279 Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask, 280 SplitAnimationTimings timings) { 281 mFullscreenParams.setIsStagedTask(isStagedTask); 282 final BaseDragLayer dragLayer = mActivity.getDragLayer(); 283 int[] dragLayerBounds = new int[2]; 284 dragLayer.getLocationOnScreen(dragLayerBounds); 285 SplitOverlayProperties prop = new SplitOverlayProperties(endBounds, 286 startingBounds, dragLayerBounds[0], dragLayerBounds[1]); 287 288 ValueAnimator transitionAnimator = ValueAnimator.ofFloat(0, 1); 289 animation.add(transitionAnimator); 290 long animDuration = animation.getDuration(); 291 RectF floatingTaskViewBounds = new RectF(); 292 293 if (fadeWithThumbnail) { 294 // This code block runs for the placeholder view during Overview > OverviewSplitSelect 295 // and for the selected (secondary) thumbnail during OverviewSplitSelect > Confirmed 296 297 // FloatingTaskThumbnailView: thumbnail fades out to transparent 298 animation.setViewAlpha(mThumbnailView, 0, clampToProgress(LINEAR, 299 timings.getPlaceholderFadeInStartOffset(), 300 timings.getPlaceholderFadeInEndOffset())); 301 302 // SplitPlaceholderView: gray background fades in at same time, then new icon fades in 303 fadeInSplitPlaceholder(animation, timings); 304 } else if (isStagedTask) { 305 // This code block runs for the placeholder view during Normal > OverviewSplitSelect 306 // and for the placeholder (primary) thumbnail during OverviewSplitSelect > Confirmed 307 308 // Fade in the placeholder view during Normal > OverviewSplitSelect 309 if (mSplitPlaceholderView.getAlpha() == 0) { 310 mSplitPlaceholderView.getIconView().setAlpha(0); 311 fadeInSplitPlaceholder(animation, timings); 312 } 313 314 // No-op for placeholder during OverviewSplitSelect > Confirmed, alpha should be set 315 } 316 317 MultiValueUpdateListener listener = new MultiValueUpdateListener() { 318 // SplitPlaceholderView: rectangle translates and stretches to new position 319 final FloatProp mDx = new FloatProp(0, prop.dX, 0, animDuration, 320 clampToProgress(timings.getStagedRectXInterpolator(), 321 timings.getStagedRectSlideStartOffset(), 322 timings.getStagedRectSlideEndOffset())); 323 final FloatProp mDy = new FloatProp(0, prop.dY, 0, animDuration, 324 clampToProgress(timings.getStagedRectYInterpolator(), 325 timings.getStagedRectSlideStartOffset(), 326 timings.getStagedRectSlideEndOffset())); 327 final FloatProp mTaskViewScaleX = new FloatProp(1f, prop.finalTaskViewScaleX, 0, 328 animDuration, clampToProgress(timings.getStagedRectScaleXInterpolator(), 329 timings.getStagedRectSlideStartOffset(), 330 timings.getStagedRectSlideEndOffset())); 331 final FloatProp mTaskViewScaleY = new FloatProp(1f, prop.finalTaskViewScaleY, 0, 332 animDuration, clampToProgress(timings.getStagedRectScaleYInterpolator(), 333 timings.getStagedRectSlideStartOffset(), 334 timings.getStagedRectSlideEndOffset())); 335 @Override 336 public void onUpdate(float percent, boolean initOnly) { 337 // Calculate the icon position. 338 floatingTaskViewBounds.set(startingBounds); 339 floatingTaskViewBounds.offset(mDx.value, mDy.value); 340 Utilities.scaleRectFAboutCenter(floatingTaskViewBounds, mTaskViewScaleX.value, 341 mTaskViewScaleY.value); 342 343 update(floatingTaskViewBounds, percent); 344 } 345 }; 346 347 transitionAnimator.addUpdateListener(listener); 348 } 349 fadeInSplitPlaceholder(PendingAnimation animation, SplitAnimationTimings timings)350 void fadeInSplitPlaceholder(PendingAnimation animation, SplitAnimationTimings timings) { 351 animation.setViewAlpha(mSplitPlaceholderView, 1, clampToProgress(LINEAR, 352 timings.getPlaceholderFadeInStartOffset(), 353 timings.getPlaceholderFadeInEndOffset())); 354 animation.setViewAlpha(mSplitPlaceholderView.getIconView(), 1, clampToProgress(LINEAR, 355 timings.getPlaceholderIconFadeInStartOffset(), 356 timings.getPlaceholderIconFadeInEndOffset())); 357 } 358 drawRoundedRect(Canvas canvas, Paint paint)359 void drawRoundedRect(Canvas canvas, Paint paint) { 360 if (mFullscreenParams == null) { 361 return; 362 } 363 364 canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), 365 mFullscreenParams.mCurrentDrawnCornerRadius / mFullscreenParams.mScaleX, 366 mFullscreenParams.mCurrentDrawnCornerRadius / mFullscreenParams.mScaleY, 367 paint); 368 } 369 370 /** 371 * When a split is staged, center the icon in the staging area. Accounts for device insets. 372 * @param iconView The icon that should be centered. 373 * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is 374 * offscreen). 375 * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is 376 * offscreen). 377 */ centerIconView(IconView iconView, float onScreenRectCenterX, float onScreenRectCenterY)378 void centerIconView(IconView iconView, float onScreenRectCenterX, float onScreenRectCenterY) { 379 mOrientationHandler.updateSplitIconParams(iconView, onScreenRectCenterX, 380 onScreenRectCenterY, mFullscreenParams.mScaleX, mFullscreenParams.mScaleY, 381 iconView.getDrawableWidth(), iconView.getDrawableHeight(), 382 mActivity.getDeviceProfile(), mStagePosition); 383 } 384 getStagePosition()385 public int getStagePosition() { 386 return mStagePosition; 387 } 388 389 private static class SplitOverlayProperties { 390 391 private final float finalTaskViewScaleX; 392 private final float finalTaskViewScaleY; 393 private final float dX; 394 private final float dY; 395 SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, int dragLayerLeft, int dragLayerTop)396 SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, 397 int dragLayerLeft, int dragLayerTop) { 398 float maxScaleX = endBounds.width() / startTaskViewBounds.width(); 399 float maxScaleY = endBounds.height() / startTaskViewBounds.height(); 400 401 finalTaskViewScaleX = maxScaleX; 402 finalTaskViewScaleY = maxScaleY; 403 404 // Animate to the center of the window bounds in screen coordinates. 405 float centerX = endBounds.centerX() - dragLayerLeft; 406 float centerY = endBounds.centerY() - dragLayerTop; 407 408 dX = centerX - startTaskViewBounds.centerX(); 409 dY = centerY - startTaskViewBounds.centerY(); 410 } 411 } 412 413 public static class FullscreenDrawParams { 414 415 private final float mCornerRadius; 416 private final float mWindowCornerRadius; 417 public boolean mIsStagedTask; 418 public final RectF mBounds = new RectF(); 419 public float mCurrentDrawnCornerRadius; 420 public float mScaleX = 1; 421 public float mScaleY = 1; 422 FullscreenDrawParams(Context context)423 public FullscreenDrawParams(Context context) { 424 mCornerRadius = TaskCornerRadius.get(context); 425 mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context); 426 427 mCurrentDrawnCornerRadius = mCornerRadius; 428 } 429 updateParams(RectF bounds, float progress, float scaleX, float scaleY)430 public void updateParams(RectF bounds, float progress, float scaleX, float scaleY) { 431 mBounds.set(bounds); 432 mScaleX = scaleX; 433 mScaleY = scaleY; 434 mCurrentDrawnCornerRadius = mIsStagedTask ? mWindowCornerRadius : 435 Utilities.mapRange(progress, mCornerRadius, mWindowCornerRadius); 436 } 437 setIsStagedTask(boolean isStagedTask)438 public void setIsStagedTask(boolean isStagedTask) { 439 mIsStagedTask = isStagedTask; 440 } 441 } 442 } 443