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