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