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