1 /* 2 * Copyright (C) 2017 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 17 package com.android.quickstep.views; 18 19 import static android.view.Gravity.BOTTOM; 20 import static android.view.Gravity.CENTER_HORIZONTAL; 21 import static android.view.Gravity.CENTER_VERTICAL; 22 import static android.view.Gravity.END; 23 import static android.view.Gravity.START; 24 import static android.view.Gravity.TOP; 25 import static android.widget.Toast.LENGTH_SHORT; 26 27 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; 28 import static com.android.launcher3.Utilities.comp; 29 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 30 import static com.android.launcher3.anim.Interpolators.LINEAR; 31 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; 32 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 33 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent 34 .LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS; 35 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP; 36 37 import android.animation.Animator; 38 import android.animation.AnimatorListenerAdapter; 39 import android.animation.ObjectAnimator; 40 import android.animation.TimeInterpolator; 41 import android.animation.ValueAnimator; 42 import android.app.ActivityOptions; 43 import android.content.Context; 44 import android.content.Intent; 45 import android.graphics.Outline; 46 import android.graphics.Rect; 47 import android.graphics.RectF; 48 import android.graphics.drawable.Drawable; 49 import android.graphics.drawable.GradientDrawable; 50 import android.graphics.drawable.InsetDrawable; 51 import android.os.Bundle; 52 import android.os.Handler; 53 import android.util.AttributeSet; 54 import android.util.FloatProperty; 55 import android.util.Log; 56 import android.view.Surface; 57 import android.view.View; 58 import android.view.ViewOutlineProvider; 59 import android.view.accessibility.AccessibilityNodeInfo; 60 import android.widget.FrameLayout; 61 import android.widget.Toast; 62 63 import com.android.launcher3.BaseDraggingActivity; 64 import com.android.launcher3.DeviceProfile; 65 import com.android.launcher3.LauncherSettings; 66 import com.android.launcher3.R; 67 import com.android.launcher3.Utilities; 68 import com.android.launcher3.anim.AnimatorPlaybackController; 69 import com.android.launcher3.anim.Interpolators; 70 import com.android.launcher3.anim.PendingAnimation; 71 import com.android.launcher3.logging.UserEventDispatcher; 72 import com.android.launcher3.model.data.WorkspaceItemInfo; 73 import com.android.launcher3.popup.SystemShortcut; 74 import com.android.launcher3.testing.TestLogging; 75 import com.android.launcher3.testing.TestProtocol; 76 import com.android.launcher3.touch.PagedOrientationHandler; 77 import com.android.launcher3.userevent.nano.LauncherLogProto; 78 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 79 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 80 import com.android.launcher3.util.ComponentKey; 81 import com.android.launcher3.util.ViewPool.Reusable; 82 import com.android.quickstep.RecentsModel; 83 import com.android.quickstep.TaskIconCache; 84 import com.android.quickstep.TaskOverlayFactory; 85 import com.android.quickstep.TaskThumbnailCache; 86 import com.android.quickstep.TaskUtils; 87 import com.android.quickstep.util.RecentsOrientedState; 88 import com.android.quickstep.util.TaskCornerRadius; 89 import com.android.quickstep.views.RecentsView.PageCallbacks; 90 import com.android.quickstep.views.RecentsView.ScrollState; 91 import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper; 92 import com.android.systemui.shared.recents.model.Task; 93 import com.android.systemui.shared.system.ActivityManagerWrapper; 94 import com.android.systemui.shared.system.ActivityOptionsCompat; 95 import com.android.systemui.shared.system.QuickStepContract; 96 97 import java.util.Collections; 98 import java.util.List; 99 import java.util.function.Consumer; 100 101 /** 102 * A task in the Recents view. 103 */ 104 public class TaskView extends FrameLayout implements PageCallbacks, Reusable { 105 106 private static final String TAG = TaskView.class.getSimpleName(); 107 108 /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */ 109 private static final TimeInterpolator CURVE_INTERPOLATOR 110 = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f; 111 112 /** 113 * The alpha of a black scrim on a page in the carousel as it leaves the screen. 114 * In the resting position of the carousel, the adjacent pages have about half this scrim. 115 */ 116 public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; 117 118 /** 119 * How much to scale down pages near the edge of the screen. 120 */ 121 public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; 122 123 public static final long SCALE_ICON_DURATION = 120; 124 private static final long DIM_ANIM_DURATION = 700; 125 126 private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT = 127 Collections.singletonList(new Rect()); 128 129 private static final FloatProperty<TaskView> FOCUS_TRANSITION = 130 new FloatProperty<TaskView>("focusTransition") { 131 @Override 132 public void setValue(TaskView taskView, float v) { 133 taskView.setIconAndDimTransitionProgress(v, false /* invert */); 134 } 135 136 @Override 137 public Float get(TaskView taskView) { 138 return taskView.mFocusTransitionProgress; 139 } 140 }; 141 142 private final OnAttachStateChangeListener mTaskMenuStateListener = 143 new OnAttachStateChangeListener() { 144 @Override 145 public void onViewAttachedToWindow(View view) { 146 } 147 148 @Override 149 public void onViewDetachedFromWindow(View view) { 150 if (mMenuView != null) { 151 mMenuView.removeOnAttachStateChangeListener(this); 152 mMenuView = null; 153 } 154 } 155 }; 156 157 private final TaskOutlineProvider mOutlineProvider; 158 159 private Task mTask; 160 private TaskThumbnailView mSnapshotView; 161 private TaskMenuView mMenuView; 162 private IconView mIconView; 163 private final DigitalWellBeingToast mDigitalWellBeingToast; 164 private float mCurveScale; 165 private float mFullscreenProgress; 166 private final FullscreenDrawParams mCurrentFullscreenParams; 167 private final BaseDraggingActivity mActivity; 168 169 private ObjectAnimator mIconAndDimAnimator; 170 private float mIconScaleAnimStartProgress = 0; 171 private float mFocusTransitionProgress = 1; 172 private float mModalness = 0; 173 private float mStableAlpha = 1; 174 175 private boolean mShowScreenshot; 176 177 // The current background requests to load the task thumbnail and icon 178 private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest; 179 private TaskIconCache.IconLoadRequest mIconLoadRequest; 180 181 // Order in which the footers appear. Lower order appear below higher order. 182 public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0; 183 private final FooterWrapper[] mFooters = new FooterWrapper[2]; 184 private float mFooterVerticalOffset = 0; 185 private float mFooterAlpha = 1; 186 private int mStackHeight; 187 private View mContextualChipWrapper; 188 private View mContextualChip; 189 TaskView(Context context)190 public TaskView(Context context) { 191 this(context, null); 192 } 193 TaskView(Context context, AttributeSet attrs)194 public TaskView(Context context, AttributeSet attrs) { 195 this(context, attrs, 0); 196 } 197 TaskView(Context context, AttributeSet attrs, int defStyleAttr)198 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 199 super(context, attrs, defStyleAttr); 200 mActivity = BaseDraggingActivity.fromContext(context); 201 setOnClickListener((view) -> { 202 if (getTask() == null) { 203 return; 204 } 205 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 206 if (isRunningTask()) { 207 createLaunchAnimationForRunningTask().start(); 208 } else { 209 launchTask(true /* animate */); 210 } 211 } else { 212 launchTask(true /* animate */); 213 } 214 215 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( 216 Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this), 217 TaskUtils.getLaunchComponentKeyForTask(getTask().key)); 218 mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo()) 219 .log(LAUNCHER_TASK_LAUNCH_TAP); 220 }); 221 222 mCurrentFullscreenParams = new FullscreenDrawParams(context); 223 mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this); 224 225 mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams); 226 setOutlineProvider(mOutlineProvider); 227 } 228 229 /** 230 * Builds proto for logging 231 */ getItemInfo()232 public WorkspaceItemInfo getItemInfo() { 233 ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(getTask().key); 234 WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo(); 235 dummyInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK; 236 dummyInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER; 237 dummyInfo.user = componentKey.user; 238 dummyInfo.intent = new Intent().setComponent(componentKey.componentName); 239 dummyInfo.title = TaskUtils.getTitle(getContext(), getTask()); 240 dummyInfo.screenId = getRecentsView().indexOfChild(this); 241 return dummyInfo; 242 } 243 244 @Override onFinishInflate()245 protected void onFinishInflate() { 246 super.onFinishInflate(); 247 mSnapshotView = findViewById(R.id.snapshot); 248 mIconView = findViewById(R.id.icon); 249 } 250 251 /** 252 * The modalness of this view is how it should be displayed when it is shown on its own in the 253 * modal state of overview. 254 * 255 * @param modalness [0, 1] 0 being in context with other tasks, 1 being shown on its own. 256 */ setModalness(float modalness)257 public void setModalness(float modalness) { 258 mModalness = modalness; 259 mIconView.setAlpha(comp(modalness)); 260 if (mContextualChip != null) { 261 mContextualChip.setScaleX(comp(modalness)); 262 mContextualChip.setScaleY(comp(modalness)); 263 } 264 if (mContextualChipWrapper != null) { 265 mContextualChipWrapper.setAlpha(comp(modalness)); 266 } 267 268 updateFooterVerticalOffset(mFooterVerticalOffset); 269 } 270 getMenuView()271 public TaskMenuView getMenuView() { 272 return mMenuView; 273 } 274 getDigitalWellBeingToast()275 public DigitalWellBeingToast getDigitalWellBeingToast() { 276 return mDigitalWellBeingToast; 277 } 278 279 /** 280 * Updates this task view to the given {@param task}. 281 * 282 * TODO(b/142282126) Re-evaluate if we need to pass in isMultiWindowMode after 283 * that issue is fixed 284 */ bind(Task task, RecentsOrientedState orientedState)285 public void bind(Task task, RecentsOrientedState orientedState) { 286 cancelPendingLoadTasks(); 287 mTask = task; 288 mSnapshotView.bind(task); 289 setOrientationState(orientedState); 290 } 291 getTask()292 public Task getTask() { 293 return mTask; 294 } 295 getThumbnail()296 public TaskThumbnailView getThumbnail() { 297 return mSnapshotView; 298 } 299 getIconView()300 public IconView getIconView() { 301 return mIconView; 302 } 303 createLaunchAnimationForRunningTask()304 public AnimatorPlaybackController createLaunchAnimationForRunningTask() { 305 final PendingAnimation pendingAnimation = getRecentsView().createTaskLaunchAnimation( 306 this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR); 307 AnimatorPlaybackController currentAnimation = pendingAnimation.createPlaybackController(); 308 currentAnimation.setEndAction(() -> { 309 pendingAnimation.finish(true, Touch.SWIPE); 310 launchTask(false); 311 }); 312 return currentAnimation; 313 } 314 launchTask(boolean animate)315 public void launchTask(boolean animate) { 316 launchTask(animate, false /* freezeTaskList */); 317 } 318 launchTask(boolean animate, boolean freezeTaskList)319 public void launchTask(boolean animate, boolean freezeTaskList) { 320 launchTask(animate, freezeTaskList, (result) -> { 321 if (!result) { 322 notifyTaskLaunchFailed(TAG); 323 } 324 }, getHandler()); 325 } 326 launchTask(boolean animate, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)327 public void launchTask(boolean animate, Consumer<Boolean> resultCallback, 328 Handler resultCallbackHandler) { 329 launchTask(animate, false /* freezeTaskList */, resultCallback, resultCallbackHandler); 330 } 331 launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)332 public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback, 333 Handler resultCallbackHandler) { 334 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 335 RecentsView recentsView = getRecentsView(); 336 if (isRunningTask()) { 337 recentsView.finishRecentsAnimation(false /* toRecents */, 338 () -> resultCallbackHandler.post(() -> resultCallback.accept(true))); 339 } else { 340 // This is a workaround against the WM issue that app open is not correctly animated 341 // when recents animation is being cleaned up (b/143774568). When that's possible, 342 // we should rely on the framework side to cancel the recents animation, and we will 343 // clean up the screenshot on the launcher side while we launch the next task. 344 recentsView.switchToScreenshot(null, 345 () -> recentsView.finishRecentsAnimation(true /* toRecents */, 346 () -> launchTaskInternal(animate, freezeTaskList, resultCallback, 347 resultCallbackHandler))); 348 } 349 } else { 350 launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler); 351 } 352 } 353 launchTaskInternal(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)354 private void launchTaskInternal(boolean animate, boolean freezeTaskList, 355 Consumer<Boolean> resultCallback, Handler resultCallbackHandler) { 356 if (mTask != null) { 357 final ActivityOptions opts; 358 TestLogging.recordEvent( 359 TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask); 360 if (animate) { 361 opts = mActivity.getActivityLaunchOptions(this); 362 if (freezeTaskList) { 363 ActivityOptionsCompat.setFreezeRecentTasksList(opts); 364 } 365 ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, 366 opts, resultCallback, resultCallbackHandler); 367 } else { 368 opts = ActivityOptionsCompat.makeCustomAnimation(getContext(), 0, 0, () -> { 369 if (resultCallback != null) { 370 // Only post the animation start after the system has indicated that the 371 // transition has started 372 resultCallbackHandler.post(() -> resultCallback.accept(true)); 373 } 374 }, resultCallbackHandler); 375 if (freezeTaskList) { 376 ActivityOptionsCompat.setFreezeRecentTasksList(opts); 377 } 378 ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key, 379 opts, (success) -> { 380 if (resultCallback != null && !success) { 381 // If the call to start activity failed, then post the result 382 // immediately, otherwise, wait for the animation start callback 383 // from the activity options above 384 resultCallbackHandler.post(() -> resultCallback.accept(false)); 385 } 386 }, resultCallbackHandler); 387 } 388 getRecentsView().onTaskLaunched(mTask); 389 } 390 } 391 onTaskListVisibilityChanged(boolean visible)392 public void onTaskListVisibilityChanged(boolean visible) { 393 if (mTask == null) { 394 return; 395 } 396 cancelPendingLoadTasks(); 397 if (visible) { 398 // These calls are no-ops if the data is already loaded, try and load the high 399 // resolution thumbnail if the state permits 400 RecentsModel model = RecentsModel.INSTANCE.get(getContext()); 401 TaskThumbnailCache thumbnailCache = model.getThumbnailCache(); 402 TaskIconCache iconCache = model.getIconCache(); 403 mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground( 404 mTask, thumbnail -> mSnapshotView.setThumbnail(mTask, thumbnail)); 405 mIconLoadRequest = iconCache.updateIconInBackground(mTask, 406 (task) -> { 407 setIcon(task.icon); 408 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) { 409 getRecentsView().updateLiveTileIcon(task.icon); 410 } 411 mDigitalWellBeingToast.initialize(mTask); 412 }); 413 } else { 414 mSnapshotView.setThumbnail(null, null); 415 setIcon(null); 416 // Reset the task thumbnail reference as well (it will be fetched from the cache or 417 // reloaded next time we need it) 418 mTask.thumbnail = null; 419 } 420 } 421 cancelPendingLoadTasks()422 private void cancelPendingLoadTasks() { 423 if (mThumbnailLoadRequest != null) { 424 mThumbnailLoadRequest.cancel(); 425 mThumbnailLoadRequest = null; 426 } 427 if (mIconLoadRequest != null) { 428 mIconLoadRequest.cancel(); 429 mIconLoadRequest = null; 430 } 431 } 432 showTaskMenu(int action)433 private boolean showTaskMenu(int action) { 434 if (!getRecentsView().isClearAllHidden()) { 435 getRecentsView().snapToPage(getRecentsView().indexOfChild(this)); 436 } else { 437 mMenuView = TaskMenuView.showForTask(this); 438 mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo()) 439 .log(LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS); 440 UserEventDispatcher.newInstance(getContext()).logActionOnItem(action, Direction.NONE, 441 LauncherLogProto.ItemType.TASK_ICON); 442 if (mMenuView != null) { 443 mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener); 444 } 445 } 446 return mMenuView != null; 447 } 448 setIcon(Drawable icon)449 private void setIcon(Drawable icon) { 450 if (icon != null) { 451 mIconView.setDrawable(icon); 452 mIconView.setOnClickListener(v -> showTaskMenu(Touch.TAP)); 453 mIconView.setOnLongClickListener(v -> { 454 requestDisallowInterceptTouchEvent(true); 455 return showTaskMenu(Touch.LONGPRESS); 456 }); 457 } else { 458 mIconView.setDrawable(null); 459 mIconView.setOnClickListener(null); 460 mIconView.setOnLongClickListener(null); 461 } 462 } 463 setOrientationState(RecentsOrientedState orientationState)464 public void setOrientationState(RecentsOrientedState orientationState) { 465 PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler(); 466 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 467 LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams(); 468 int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin); 469 LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams(); 470 switch (orientationHandler.getRotation()) { 471 case Surface.ROTATION_90: 472 iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL; 473 iconParams.rightMargin = -thumbnailPadding; 474 iconParams.leftMargin = 0; 475 iconParams.topMargin = snapshotParams.topMargin / 2; 476 break; 477 case Surface.ROTATION_180: 478 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL; 479 iconParams.bottomMargin = -thumbnailPadding; 480 iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0; 481 break; 482 case Surface.ROTATION_270: 483 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL; 484 iconParams.leftMargin = -thumbnailPadding; 485 iconParams.rightMargin = 0; 486 iconParams.topMargin = snapshotParams.topMargin / 2; 487 break; 488 case Surface.ROTATION_0: 489 default: 490 iconParams.gravity = TOP | CENTER_HORIZONTAL; 491 iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0; 492 break; 493 } 494 mIconView.setLayoutParams(iconParams); 495 mIconView.setRotation(orientationHandler.getDegreesRotated()); 496 497 if (mMenuView != null) { 498 mMenuView.onRotationChanged(); 499 } 500 } 501 setIconAndDimTransitionProgress(float progress, boolean invert)502 private void setIconAndDimTransitionProgress(float progress, boolean invert) { 503 if (invert) { 504 progress = 1 - progress; 505 } 506 mFocusTransitionProgress = progress; 507 mSnapshotView.setDimAlphaMultipler(progress); 508 float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION; 509 float lowerClamp = invert ? 1f - iconScalePercentage : 0; 510 float upperClamp = invert ? 1 : iconScalePercentage; 511 float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp) 512 .getInterpolation(progress); 513 mIconView.setScaleX(scale); 514 mIconView.setScaleY(scale); 515 516 updateFooterVerticalOffset(1.0f - scale); 517 } 518 setIconScaleAnimStartProgress(float startProgress)519 public void setIconScaleAnimStartProgress(float startProgress) { 520 mIconScaleAnimStartProgress = startProgress; 521 } 522 animateIconScaleAndDimIntoView()523 public void animateIconScaleAndDimIntoView() { 524 if (mIconAndDimAnimator != null) { 525 mIconAndDimAnimator.cancel(); 526 } 527 mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1); 528 mIconAndDimAnimator.setCurrentFraction(mIconScaleAnimStartProgress); 529 mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR); 530 mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() { 531 @Override 532 public void onAnimationEnd(Animator animation) { 533 mIconAndDimAnimator = null; 534 } 535 }); 536 mIconAndDimAnimator.start(); 537 } 538 setIconScaleAndDim(float iconScale)539 protected void setIconScaleAndDim(float iconScale) { 540 setIconScaleAndDim(iconScale, false); 541 } 542 setIconScaleAndDim(float iconScale, boolean invert)543 private void setIconScaleAndDim(float iconScale, boolean invert) { 544 if (mIconAndDimAnimator != null) { 545 mIconAndDimAnimator.cancel(); 546 } 547 setIconAndDimTransitionProgress(iconScale, invert); 548 } 549 resetViewTransforms()550 protected void resetViewTransforms() { 551 setCurveScale(1); 552 setTranslationX(0f); 553 setTranslationY(0f); 554 setTranslationZ(0); 555 setAlpha(mStableAlpha); 556 setIconScaleAndDim(1); 557 } 558 setStableAlpha(float parentAlpha)559 public void setStableAlpha(float parentAlpha) { 560 mStableAlpha = parentAlpha; 561 setAlpha(mStableAlpha); 562 } 563 564 @Override onRecycle()565 public void onRecycle() { 566 resetViewTransforms(); 567 // Clear any references to the thumbnail (it will be re-read either from the cache or the 568 // system on next bind) 569 mSnapshotView.setThumbnail(mTask, null); 570 setOverlayEnabled(false); 571 onTaskListVisibilityChanged(false); 572 } 573 574 @Override onPageScroll(ScrollState scrollState)575 public void onPageScroll(ScrollState scrollState) { 576 // Don't do anything if it's modal. 577 if (mModalness > 0) { 578 return; 579 } 580 581 float curveInterpolation = 582 CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); 583 float curveScaleForCurveInterpolation = getCurveScaleForCurveInterpolation( 584 curveInterpolation); 585 mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); 586 setCurveScale(curveScaleForCurveInterpolation); 587 588 mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f); 589 for (FooterWrapper footer : mFooters) { 590 if (footer != null) { 591 footer.mView.setAlpha(mFooterAlpha); 592 } 593 } 594 595 if (mMenuView != null) { 596 PagedOrientationHandler pagedOrientationHandler = getPagedOrientationHandler(); 597 RecentsView recentsView = getRecentsView(); 598 mMenuView.setPosition(getX() - recentsView.getScrollX(), 599 getY() - recentsView.getScrollY(), pagedOrientationHandler); 600 mMenuView.setScaleX(getScaleX()); 601 mMenuView.setScaleY(getScaleY()); 602 } 603 } 604 605 /** 606 * Sets the footer at the specific index and returns the previously set footer. 607 */ setFooter(int index, View view)608 public View setFooter(int index, View view) { 609 View oldFooter = null; 610 611 // If the footer are is already collapsed, do not animate entry 612 boolean shouldAnimateEntry = mFooterVerticalOffset <= 0; 613 614 if (mFooters[index] != null) { 615 oldFooter = mFooters[index].mView; 616 mFooters[index].release(); 617 removeView(oldFooter); 618 619 // If we are replacing an existing footer, do not animate entry 620 shouldAnimateEntry = false; 621 } 622 if (view != null) { 623 int indexToAdd = getChildCount(); 624 for (int i = index - 1; i >= 0; i--) { 625 if (mFooters[i] != null) { 626 indexToAdd = indexOfChild(mFooters[i].mView); 627 break; 628 } 629 } 630 631 addView(view, indexToAdd); 632 LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); 633 layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL; 634 layoutParams.bottomMargin = 635 ((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin; 636 view.setAlpha(mFooterAlpha); 637 mFooters[index] = new FooterWrapper(view); 638 if (shouldAnimateEntry) { 639 mFooters[index].animateEntry(); 640 } 641 } else { 642 mFooters[index] = null; 643 } 644 645 mStackHeight = 0; 646 for (FooterWrapper footer : mFooters) { 647 if (footer != null) { 648 footer.setVerticalShift(mStackHeight); 649 mStackHeight += footer.mExpectedHeight; 650 } 651 } 652 653 return oldFooter; 654 } 655 656 /** 657 * Sets the contextual chip. 658 * 659 * @param view Wrapper view containing contextual chip. 660 */ setContextualChip(View view)661 public void setContextualChip(View view) { 662 if (mContextualChipWrapper != null) { 663 removeView(mContextualChipWrapper); 664 } 665 if (view != null) { 666 mContextualChipWrapper = view; 667 LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 668 LayoutParams.WRAP_CONTENT); 669 layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL; 670 int expectedChipHeight = getExpectedViewHeight(view); 671 float chipOffset = getResources().getDimension(R.dimen.chip_hint_vertical_offset); 672 layoutParams.bottomMargin = (int) 673 (((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin 674 - expectedChipHeight + chipOffset); 675 mContextualChip = ((FrameLayout) mContextualChipWrapper).getChildAt(0); 676 mContextualChip.setScaleX(0f); 677 mContextualChip.setScaleY(0f); 678 GradientDrawable scrimDrawable = (GradientDrawable) getResources().getDrawable( 679 R.drawable.chip_scrim_gradient, mActivity.getTheme()); 680 float cornerRadius = getTaskCornerRadius(); 681 scrimDrawable.setCornerRadii( 682 new float[]{0, 0, 0, 0, cornerRadius, cornerRadius, cornerRadius, 683 cornerRadius}); 684 InsetDrawable scrimDrawableInset = new InsetDrawable(scrimDrawable, 0, 0, 0, 685 (int) (expectedChipHeight - chipOffset)); 686 mContextualChipWrapper.setBackground(scrimDrawableInset); 687 mContextualChipWrapper.setPadding(0, 0, 0, 0); 688 mContextualChipWrapper.setAlpha(0f); 689 addView(view, getChildCount(), layoutParams); 690 if (mContextualChip != null) { 691 mContextualChip.animate().scaleX(1f).scaleY(1f).setDuration(50); 692 } 693 if (mContextualChipWrapper != null) { 694 mContextualChipWrapper.animate().alpha(1f).setDuration(50); 695 } 696 } 697 } 698 getTaskCornerRadius()699 public float getTaskCornerRadius() { 700 return TaskCornerRadius.get(mActivity); 701 } 702 703 /** 704 * Clears the contextual chip from TaskView. 705 * 706 * @return The contextual chip wrapper view to be recycled. 707 */ clearContextualChip()708 public View clearContextualChip() { 709 if (mContextualChipWrapper != null) { 710 removeView(mContextualChipWrapper); 711 } 712 View oldContextualChipWrapper = mContextualChipWrapper; 713 mContextualChipWrapper = null; 714 mContextualChip = null; 715 return oldContextualChipWrapper; 716 } 717 718 @Override onLayout(boolean changed, int left, int top, int right, int bottom)719 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 720 super.onLayout(changed, left, top, right, bottom); 721 setPivotX((right - left) * 0.5f); 722 setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f); 723 if (Utilities.ATLEAST_Q) { 724 SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight()); 725 setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); 726 } 727 728 mStackHeight = 0; 729 for (FooterWrapper footer : mFooters) { 730 if (footer != null) { 731 mStackHeight += footer.mView.getHeight(); 732 } 733 } 734 updateFooterVerticalOffset(0); 735 } 736 updateFooterVerticalOffset(float offset)737 private void updateFooterVerticalOffset(float offset) { 738 mFooterVerticalOffset = offset; 739 740 for (FooterWrapper footer : mFooters) { 741 if (footer != null) { 742 footer.updateFooterOffset(); 743 } 744 } 745 } 746 getCurveScaleForInterpolation(float linearInterpolation)747 public static float getCurveScaleForInterpolation(float linearInterpolation) { 748 float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); 749 return getCurveScaleForCurveInterpolation(curveInterpolation); 750 } 751 getCurveScaleForCurveInterpolation(float curveInterpolation)752 private static float getCurveScaleForCurveInterpolation(float curveInterpolation) { 753 return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; 754 } 755 setCurveScale(float curveScale)756 private void setCurveScale(float curveScale) { 757 mCurveScale = curveScale; 758 setScaleX(mCurveScale); 759 setScaleY(mCurveScale); 760 } 761 getCurveScale()762 public float getCurveScale() { 763 return mCurveScale; 764 } 765 766 @Override hasOverlappingRendering()767 public boolean hasOverlappingRendering() { 768 // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. 769 return false; 770 } 771 772 private static final class TaskOutlineProvider extends ViewOutlineProvider { 773 774 private final int mMarginTop; 775 private FullscreenDrawParams mFullscreenParams; 776 TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams)777 TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) { 778 mMarginTop = context.getResources().getDimensionPixelSize( 779 R.dimen.task_thumbnail_top_margin); 780 mFullscreenParams = fullscreenParams; 781 } 782 setFullscreenParams(FullscreenDrawParams params)783 public void setFullscreenParams(FullscreenDrawParams params) { 784 mFullscreenParams = params; 785 } 786 787 @Override getOutline(View view, Outline outline)788 public void getOutline(View view, Outline outline) { 789 RectF insets = mFullscreenParams.mCurrentDrawnInsets; 790 float scale = mFullscreenParams.mScale; 791 outline.setRoundRect(0, 792 (int) (mMarginTop * scale), 793 (int) ((insets.left + view.getWidth() + insets.right) * scale), 794 (int) ((insets.top + view.getHeight() + insets.bottom) * scale), 795 mFullscreenParams.mCurrentDrawnCornerRadius); 796 } 797 } 798 799 private class FooterWrapper extends ViewOutlineProvider { 800 801 final View mView; 802 final ViewOutlineProvider mOldOutlineProvider; 803 final ViewOutlineProvider mDelegate; 804 805 final int mExpectedHeight; 806 final int mOldPaddingBottom; 807 808 int mAnimationOffset = 0; 809 int mEntryAnimationOffset = 0; 810 FooterWrapper(View view)811 public FooterWrapper(View view) { 812 mView = view; 813 mOldOutlineProvider = view.getOutlineProvider(); 814 mDelegate = mOldOutlineProvider == null 815 ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider; 816 817 mExpectedHeight = getExpectedViewHeight(view); 818 mOldPaddingBottom = view.getPaddingBottom(); 819 820 if (mOldOutlineProvider != null) { 821 view.setOutlineProvider(this); 822 view.setClipToOutline(true); 823 } 824 } 825 setVerticalShift(int shift)826 public void setVerticalShift(int shift) { 827 mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(), 828 mView.getPaddingRight(), mOldPaddingBottom + shift); 829 } 830 831 @Override getOutline(View view, Outline outline)832 public void getOutline(View view, Outline outline) { 833 mDelegate.getOutline(view, outline); 834 outline.offset(0, -mAnimationOffset - mEntryAnimationOffset); 835 } 836 updateFooterOffset()837 void updateFooterOffset() { 838 float offset = Utilities.or(mFooterVerticalOffset, mModalness); 839 mAnimationOffset = Math.round(mStackHeight * offset); 840 mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset 841 + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom 842 + mCurrentFullscreenParams.mCurrentDrawnInsets.top); 843 mView.invalidateOutline(); 844 } 845 release()846 void release() { 847 mView.setOutlineProvider(mOldOutlineProvider); 848 setVerticalShift(0); 849 } 850 animateEntry()851 void animateEntry() { 852 ValueAnimator animator = ValueAnimator.ofFloat(0, 1); 853 animator.addUpdateListener(anim -> { 854 float factor = 1 - anim.getAnimatedFraction(); 855 int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom; 856 mEntryAnimationOffset = Math.round(factor * totalShift); 857 updateFooterOffset(); 858 }); 859 animator.setDuration(100); 860 animator.start(); 861 } 862 } 863 getExpectedViewHeight(View view)864 private int getExpectedViewHeight(View view) { 865 int expectedHeight; 866 int h = view.getLayoutParams().height; 867 if (h > 0) { 868 expectedHeight = h; 869 } else { 870 int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST); 871 view.measure(m, m); 872 expectedHeight = view.getMeasuredHeight(); 873 } 874 return expectedHeight; 875 } 876 877 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)878 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 879 super.onInitializeAccessibilityNodeInfo(info); 880 881 info.addAction( 882 new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close, 883 getContext().getText(R.string.accessibility_close))); 884 885 final Context context = getContext(); 886 for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) { 887 info.addAction(s.createAccessibilityAction(context)); 888 } 889 890 if (mDigitalWellBeingToast.hasLimit()) { 891 info.addAction( 892 new AccessibilityNodeInfo.AccessibilityAction( 893 R.string.accessibility_app_usage_settings, 894 getContext().getText(R.string.accessibility_app_usage_settings))); 895 } 896 897 final RecentsView recentsView = getRecentsView(); 898 final AccessibilityNodeInfo.CollectionItemInfo itemInfo = 899 AccessibilityNodeInfo.CollectionItemInfo.obtain( 900 0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1, 901 1, false); 902 info.setCollectionItemInfo(itemInfo); 903 } 904 905 @Override performAccessibilityAction(int action, Bundle arguments)906 public boolean performAccessibilityAction(int action, Bundle arguments) { 907 if (action == R.string.accessibility_close) { 908 getRecentsView().dismissTask(this, true /*animateTaskView*/, 909 true /*removeTask*/); 910 return true; 911 } 912 913 if (action == R.string.accessibility_app_usage_settings) { 914 mDigitalWellBeingToast.openAppUsageSettings(this); 915 return true; 916 } 917 918 for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) { 919 if (s.hasHandlerForAction(action)) { 920 s.onClick(this); 921 return true; 922 } 923 } 924 925 return super.performAccessibilityAction(action, arguments); 926 } 927 getRecentsView()928 public RecentsView getRecentsView() { 929 return (RecentsView) getParent(); 930 } 931 getPagedOrientationHandler()932 PagedOrientationHandler getPagedOrientationHandler() { 933 return getRecentsView().mOrientationState.getOrientationHandler(); 934 } 935 notifyTaskLaunchFailed(String tag)936 public void notifyTaskLaunchFailed(String tag) { 937 String msg = "Failed to launch task"; 938 if (mTask != null) { 939 msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")"; 940 } 941 Log.w(tag, msg); 942 Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); 943 } 944 945 /** 946 * Hides the icon and shows insets when this TaskView is about to be shown fullscreen. 947 * 948 * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. 949 */ setFullscreenProgress(float progress)950 public void setFullscreenProgress(float progress) { 951 progress = Utilities.boundToRange(progress, 0, 1); 952 mFullscreenProgress = progress; 953 boolean isFullscreen = mFullscreenProgress > 0; 954 mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE); 955 setClipChildren(!isFullscreen); 956 setClipToPadding(!isFullscreen); 957 958 TaskThumbnailView thumbnail = getThumbnail(); 959 updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper()); 960 961 if (!getRecentsView().isTaskIconScaledDown(this)) { 962 // Some of the items in here are dependent on the current fullscreen params, but don't 963 // update them if the icon is supposed to be scaled down. 964 setIconScaleAndDim(progress, true /* invert */); 965 } 966 967 thumbnail.setFullscreenParams(mCurrentFullscreenParams); 968 mOutlineProvider.setFullscreenParams(mCurrentFullscreenParams); 969 invalidateOutline(); 970 } 971 972 void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) { 973 if (getRecentsView() == null) { 974 return; 975 } 976 mCurrentFullscreenParams.setProgress( 977 mFullscreenProgress, 978 getRecentsView().getScaleX(), 979 getWidth(), mActivity.getDeviceProfile(), 980 previewPositionHelper); 981 } 982 983 public boolean isRunningTask() { 984 if (getRecentsView() == null) { 985 return false; 986 } 987 return this == getRecentsView().getRunningTaskView(); 988 } 989 990 public void setShowScreenshot(boolean showScreenshot) { 991 mShowScreenshot = showScreenshot; 992 } 993 994 public boolean showScreenshot() { 995 if (!isRunningTask()) { 996 return true; 997 } 998 return mShowScreenshot; 999 } 1000 1001 public void setOverlayEnabled(boolean overlayEnabled) { 1002 mSnapshotView.setOverlayEnabled(overlayEnabled); 1003 } 1004 1005 /** 1006 * We update and subsequently draw these in {@link #setFullscreenProgress(float)}. 1007 */ 1008 public static class FullscreenDrawParams { 1009 1010 private final float mCornerRadius; 1011 private final float mWindowCornerRadius; 1012 1013 public RectF mCurrentDrawnInsets = new RectF(); 1014 public float mCurrentDrawnCornerRadius; 1015 /** The current scale we apply to the thumbnail to adjust for new left/right insets. */ 1016 public float mScale = 1; 1017 1018 public FullscreenDrawParams(Context context) { 1019 mCornerRadius = TaskCornerRadius.get(context); 1020 mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources()); 1021 1022 mCurrentDrawnCornerRadius = mCornerRadius; 1023 } 1024 1025 /** 1026 * Sets the progress in range [0, 1] 1027 */ 1028 public void setProgress(float fullscreenProgress, float parentScale, int previewWidth, 1029 DeviceProfile dp, PreviewPositionHelper pph) { 1030 RectF insets = pph.getInsetsToDrawInFullscreen(); 1031 1032 float currentInsetsLeft = insets.left * fullscreenProgress; 1033 float currentInsetsRight = insets.right * fullscreenProgress; 1034 mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress, 1035 currentInsetsRight, insets.bottom * fullscreenProgress); 1036 float fullscreenCornerRadius = dp.isMultiWindowMode ? 0 : mWindowCornerRadius; 1037 1038 mCurrentDrawnCornerRadius = 1039 Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius) 1040 / parentScale; 1041 1042 // We scaled the thumbnail to fit the content (excluding insets) within task view width. 1043 // Now that we are drawing left/right insets again, we need to scale down to fit them. 1044 if (previewWidth > 0) { 1045 mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight); 1046 } 1047 } 1048 1049 } 1050 } 1051