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 androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS; 20 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; 21 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS; 22 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 23 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 24 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 25 import static com.android.launcher3.Utilities.EDGE_NAV_BAR; 26 import static com.android.launcher3.Utilities.squaredHypot; 27 import static com.android.launcher3.Utilities.squaredTouchSlop; 28 import static com.android.launcher3.anim.Interpolators.ACCEL; 29 import static com.android.launcher3.anim.Interpolators.ACCEL_2; 30 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 31 import static com.android.launcher3.anim.Interpolators.LINEAR; 32 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 33 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; 34 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS; 35 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; 36 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON; 37 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 38 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; 39 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; 40 41 import android.animation.Animator; 42 import android.animation.AnimatorSet; 43 import android.animation.LayoutTransition; 44 import android.animation.LayoutTransition.TransitionListener; 45 import android.animation.ObjectAnimator; 46 import android.animation.TimeInterpolator; 47 import android.animation.ValueAnimator; 48 import android.annotation.TargetApi; 49 import android.app.ActivityManager; 50 import android.content.ComponentName; 51 import android.content.Context; 52 import android.content.Intent; 53 import android.graphics.Canvas; 54 import android.graphics.Matrix; 55 import android.graphics.Point; 56 import android.graphics.Rect; 57 import android.graphics.RectF; 58 import android.graphics.Typeface; 59 import android.graphics.drawable.Drawable; 60 import android.os.Build; 61 import android.os.Handler; 62 import android.text.Layout; 63 import android.text.StaticLayout; 64 import android.text.TextPaint; 65 import android.util.AttributeSet; 66 import android.util.FloatProperty; 67 import android.util.SparseBooleanArray; 68 import android.view.HapticFeedbackConstants; 69 import android.view.KeyEvent; 70 import android.view.LayoutInflater; 71 import android.view.MotionEvent; 72 import android.view.View; 73 import android.view.ViewDebug; 74 import android.view.ViewGroup; 75 import android.view.WindowInsets; 76 import android.view.accessibility.AccessibilityEvent; 77 import android.view.accessibility.AccessibilityNodeInfo; 78 import android.widget.ListView; 79 80 import androidx.annotation.Nullable; 81 import androidx.dynamicanimation.animation.SpringForce; 82 83 import com.android.launcher3.BaseActivity; 84 import com.android.launcher3.DeviceProfile; 85 import com.android.launcher3.Insettable; 86 import com.android.launcher3.InvariantDeviceProfile; 87 import com.android.launcher3.LauncherState; 88 import com.android.launcher3.PagedView; 89 import com.android.launcher3.R; 90 import com.android.launcher3.Utilities; 91 import com.android.launcher3.anim.AnimatorPlaybackController; 92 import com.android.launcher3.anim.PropertyListBuilder; 93 import com.android.launcher3.anim.SpringObjectAnimator; 94 import com.android.launcher3.config.FeatureFlags; 95 import com.android.launcher3.graphics.RotationMode; 96 import com.android.launcher3.userevent.nano.LauncherLogProto; 97 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 98 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 99 import com.android.launcher3.util.ComponentKey; 100 import com.android.launcher3.util.OverScroller; 101 import com.android.launcher3.util.PendingAnimation; 102 import com.android.launcher3.util.Themes; 103 import com.android.launcher3.util.ViewPool; 104 import com.android.quickstep.RecentsAnimationWrapper; 105 import com.android.quickstep.RecentsModel; 106 import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener; 107 import com.android.quickstep.TaskThumbnailCache; 108 import com.android.quickstep.TaskUtils; 109 import com.android.quickstep.util.ClipAnimationHelper; 110 import com.android.systemui.shared.recents.model.Task; 111 import com.android.systemui.shared.recents.model.ThumbnailData; 112 import com.android.systemui.shared.system.ActivityManagerWrapper; 113 import com.android.systemui.shared.system.LauncherEventUtil; 114 import com.android.systemui.shared.system.PackageManagerWrapper; 115 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; 116 import com.android.systemui.shared.system.TaskStackChangeListener; 117 118 import java.util.ArrayList; 119 import java.util.function.Consumer; 120 121 /** 122 * A list of recent tasks. 123 */ 124 @TargetApi(Build.VERSION_CODES.P) 125 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable, 126 TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, 127 InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener { 128 129 private static final String TAG = RecentsView.class.getSimpleName(); 130 131 public static final FloatProperty<RecentsView> CONTENT_ALPHA = 132 new FloatProperty<RecentsView>("contentAlpha") { 133 @Override 134 public void setValue(RecentsView view, float v) { 135 view.setContentAlpha(v); 136 } 137 138 @Override 139 public Float get(RecentsView view) { 140 return view.getContentAlpha(); 141 } 142 }; 143 144 public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS = 145 new FloatProperty<RecentsView>("fullscreenProgress") { 146 @Override 147 public void setValue(RecentsView recentsView, float v) { 148 recentsView.setFullscreenProgress(v); 149 } 150 151 @Override 152 public Float get(RecentsView recentsView) { 153 return recentsView.mFullscreenProgress; 154 } 155 }; 156 157 protected RecentsAnimationWrapper mRecentsAnimationWrapper; 158 protected ClipAnimationHelper mClipAnimationHelper; 159 protected SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier; 160 protected int mTaskWidth; 161 protected int mTaskHeight; 162 protected boolean mEnableDrawingLiveTile = false; 163 protected final Rect mTempRect = new Rect(); 164 protected final RectF mTempRectF = new RectF(); 165 166 private static final int DISMISS_TASK_DURATION = 300; 167 private static final int ADDITION_TASK_DURATION = 200; 168 // The threshold at which we update the SystemUI flags when animating from the task into the app 169 public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; 170 171 protected final T mActivity; 172 private final float mFastFlingVelocity; 173 private final RecentsModel mModel; 174 private final int mTaskTopMargin; 175 private final ClearAllButton mClearAllButton; 176 private final Rect mClearAllButtonDeadZoneRect = new Rect(); 177 private final Rect mTaskViewDeadZoneRect = new Rect(); 178 protected final ClipAnimationHelper mTempClipAnimationHelper; 179 180 private final ScrollState mScrollState = new ScrollState(); 181 // Keeps track of the previously known visible tasks for purposes of loading/unloading task data 182 private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); 183 184 private final InvariantDeviceProfile mIdp; 185 186 private final ViewPool<TaskView> mTaskViewPool; 187 188 private boolean mDwbToastShown; 189 protected boolean mDisallowScrollToClearAll; 190 private boolean mOverlayEnabled; 191 private boolean mFreezeViewVisibility; 192 193 /** 194 * TODO: Call reloadIdNeeded in onTaskStackChanged. 195 */ 196 private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { 197 @Override 198 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { 199 if (!mHandleTaskStackChanges) { 200 return; 201 } 202 // Check this is for the right user 203 if (!checkCurrentOrManagedUserId(userId, getContext())) { 204 return; 205 } 206 207 // Remove the task immediately from the task list 208 TaskView taskView = getTaskView(taskId); 209 if (taskView != null) { 210 removeView(taskView); 211 } 212 } 213 214 @Override 215 public void onActivityUnpinned() { 216 if (!mHandleTaskStackChanges) { 217 return; 218 } 219 220 reloadIfNeeded(); 221 enableLayoutTransitions(); 222 } 223 224 @Override 225 public void onTaskRemoved(int taskId) { 226 if (!mHandleTaskStackChanges) { 227 return; 228 } 229 230 UI_HELPER_EXECUTOR.execute(() -> { 231 TaskView taskView = getTaskView(taskId); 232 if (taskView == null) { 233 return; 234 } 235 Handler handler = taskView.getHandler(); 236 if (handler == null) { 237 return; 238 } 239 240 // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and 241 // remove all these checks 242 Task.TaskKey taskKey = taskView.getTask().key; 243 if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), 244 taskKey.userId) == null) { 245 // The package was uninstalled 246 handler.post(() -> 247 dismissTask(taskView, true /* animate */, false /* removeTask */)); 248 } else { 249 mModel.findTaskWithId(taskKey.id, (key) -> { 250 if (key == null) { 251 // The task was removed from the recents list 252 handler.post(() -> dismissTask(taskView, true /* animate */, 253 false /* removeTask */)); 254 } 255 }); 256 } 257 }); 258 } 259 260 @Override 261 public void onPinnedStackAnimationStarted() { 262 // Needed for activities that auto-enter PiP, which will not trigger a remote 263 // animation to be created 264 mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); 265 } 266 }; 267 268 // Used to keep track of the last requested task list id, so that we do not request to load the 269 // tasks again if we have already requested it and the task list has not changed 270 private int mTaskListChangeId = -1; 271 272 // Only valid until the launcher state changes to NORMAL 273 protected int mRunningTaskId = -1; 274 protected boolean mRunningTaskTileHidden; 275 private Task mTmpRunningTask; 276 277 private boolean mRunningTaskIconScaledDown = false; 278 279 private boolean mOverviewStateEnabled; 280 private boolean mHandleTaskStackChanges; 281 private boolean mSwipeDownShouldLaunchApp; 282 private boolean mTouchDownToStartHome; 283 private final float mSquaredTouchSlop; 284 private int mDownX; 285 private int mDownY; 286 287 private PendingAnimation mPendingAnimation; 288 private LayoutTransition mLayoutTransition; 289 290 @ViewDebug.ExportedProperty(category = "launcher") 291 protected float mContentAlpha = 1; 292 @ViewDebug.ExportedProperty(category = "launcher") 293 protected float mFullscreenProgress = 0; 294 295 // Keeps track of task id whose visual state should not be reset 296 private int mIgnoreResetTaskId = -1; 297 298 // Variables for empty state 299 private final Drawable mEmptyIcon; 300 private final CharSequence mEmptyMessage; 301 private final TextPaint mEmptyMessagePaint; 302 private final Point mLastMeasureSize = new Point(); 303 private final int mEmptyMessagePadding; 304 private boolean mShowEmptyMessage; 305 private Layout mEmptyTextLayout; 306 private LiveTileOverlay mLiveTileOverlay; 307 308 // Keeps track of the index where the first TaskView should be 309 private int mTaskViewStartIndex = 0; 310 311 private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = 312 (inMultiWindowMode) -> { 313 if (!inMultiWindowMode && mOverviewStateEnabled) { 314 // TODO: Re-enable layout transitions for addition of the unpinned task 315 reloadIfNeeded(); 316 } 317 }; 318 RecentsView(Context context, AttributeSet attrs, int defStyleAttr)319 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { 320 super(context, attrs, defStyleAttr); 321 setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); 322 setEnableFreeScroll(true); 323 324 mFastFlingVelocity = getResources() 325 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); 326 mActivity = (T) BaseActivity.fromContext(context); 327 mModel = RecentsModel.INSTANCE.get(context); 328 mIdp = InvariantDeviceProfile.INSTANCE.get(context); 329 mTempClipAnimationHelper = new ClipAnimationHelper(context); 330 331 mClearAllButton = (ClearAllButton) LayoutInflater.from(context) 332 .inflate(R.layout.overview_clear_all_button, this, false); 333 mClearAllButton.setOnClickListener(this::dismissAllTasks); 334 mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, 335 10 /* initial size */); 336 337 mIsRtl = !Utilities.isRtl(getResources()); 338 setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); 339 mTaskTopMargin = getResources() 340 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); 341 mSquaredTouchSlop = squaredTouchSlop(context); 342 343 mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); 344 mEmptyIcon.setCallback(this); 345 mEmptyMessage = context.getText(R.string.recents_empty_message); 346 mEmptyMessagePaint = new TextPaint(); 347 mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); 348 mEmptyMessagePaint.setTextSize(getResources() 349 .getDimension(R.dimen.recents_empty_message_text_size)); 350 mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context), 351 Typeface.NORMAL)); 352 mEmptyMessagePadding = getResources() 353 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); 354 setWillNotDraw(false); 355 updateEmptyMessage(); 356 357 // Initialize quickstep specific cache params here, as this is constructed only once 358 mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5); 359 } 360 getScroller()361 public OverScroller getScroller() { 362 return mScroller; 363 } 364 isRtl()365 public boolean isRtl() { 366 return mIsRtl; 367 } 368 369 @Override onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData)370 public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) { 371 if (mHandleTaskStackChanges) { 372 TaskView taskView = getTaskView(taskId); 373 if (taskView != null) { 374 Task task = taskView.getTask(); 375 taskView.getThumbnail().setThumbnail(task, thumbnailData); 376 return task; 377 } 378 } 379 return null; 380 } 381 updateThumbnail(int taskId, ThumbnailData thumbnailData)382 public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { 383 TaskView taskView = getTaskView(taskId); 384 if (taskView != null) { 385 taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData); 386 } 387 return taskView; 388 } 389 390 @Override onWindowVisibilityChanged(int visibility)391 protected void onWindowVisibilityChanged(int visibility) { 392 super.onWindowVisibilityChanged(visibility); 393 updateTaskStackListenerState(); 394 } 395 396 @Override onIdpChanged(int changeFlags, InvariantDeviceProfile idp)397 public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) { 398 if ((changeFlags & CHANGE_FLAG_ICON_PARAMS) == 0) { 399 return; 400 } 401 mModel.getIconCache().clear(); 402 reset(); 403 } 404 405 @Override onAttachedToWindow()406 protected void onAttachedToWindow() { 407 super.onAttachedToWindow(); 408 updateTaskStackListenerState(); 409 mModel.getThumbnailCache().getHighResLoadingState().addCallback(this); 410 mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); 411 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); 412 mSyncTransactionApplier = new SyncRtSurfaceTransactionApplierCompat(this); 413 RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this); 414 mIdp.addOnChangeListener(this); 415 } 416 417 @Override onDetachedFromWindow()418 protected void onDetachedFromWindow() { 419 super.onDetachedFromWindow(); 420 updateTaskStackListenerState(); 421 mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this); 422 mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); 423 ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); 424 mSyncTransactionApplier = null; 425 RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this); 426 mIdp.removeOnChangeListener(this); 427 } 428 429 @Override onViewRemoved(View child)430 public void onViewRemoved(View child) { 431 super.onViewRemoved(child); 432 433 // Clear the task data for the removed child if it was visible 434 if (child instanceof TaskView) { 435 TaskView taskView = (TaskView) child; 436 mHasVisibleTaskData.delete(taskView.getTask().key.id); 437 mTaskViewPool.recycle(taskView); 438 } 439 } 440 isTaskViewVisible(TaskView tv)441 public boolean isTaskViewVisible(TaskView tv) { 442 // For now, just check if it's the active task or an adjacent task 443 return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; 444 } 445 getTaskView(int taskId)446 public TaskView getTaskView(int taskId) { 447 for (int i = 0; i < getTaskViewCount(); i++) { 448 TaskView tv = getTaskViewAt(i); 449 if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) { 450 return tv; 451 } 452 } 453 return null; 454 } 455 setOverviewStateEnabled(boolean enabled)456 public void setOverviewStateEnabled(boolean enabled) { 457 mOverviewStateEnabled = enabled; 458 updateTaskStackListenerState(); 459 } 460 onDigitalWellbeingToastShown()461 public void onDigitalWellbeingToastShown() { 462 if (!mDwbToastShown) { 463 mDwbToastShown = true; 464 mActivity.getUserEventDispatcher().logActionTip( 465 LauncherEventUtil.VISIBLE, 466 LauncherLogProto.TipType.DWB_TOAST); 467 } 468 } 469 470 @Override onPageEndTransition()471 protected void onPageEndTransition() { 472 super.onPageEndTransition(); 473 if (getNextPage() > 0) { 474 setSwipeDownShouldLaunchApp(true); 475 } 476 } 477 478 @Override onTouchEvent(MotionEvent ev)479 public boolean onTouchEvent(MotionEvent ev) { 480 super.onTouchEvent(ev); 481 final int x = (int) ev.getX(); 482 final int y = (int) ev.getY(); 483 switch (ev.getAction()) { 484 case MotionEvent.ACTION_UP: 485 if (mTouchDownToStartHome) { 486 startHome(); 487 } 488 mTouchDownToStartHome = false; 489 break; 490 case MotionEvent.ACTION_CANCEL: 491 mTouchDownToStartHome = false; 492 break; 493 case MotionEvent.ACTION_MOVE: 494 // Passing the touch slop will not allow dismiss to home 495 if (mTouchDownToStartHome && 496 (isHandlingTouch() || 497 squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) { 498 mTouchDownToStartHome = false; 499 } 500 break; 501 case MotionEvent.ACTION_DOWN: 502 // Touch down anywhere but the deadzone around the visible clear all button and 503 // between the task views will start home on touch up 504 if (!isHandlingTouch()) { 505 if (mShowEmptyMessage) { 506 mTouchDownToStartHome = true; 507 } else { 508 updateDeadZoneRects(); 509 final boolean clearAllButtonDeadZoneConsumed = 510 mClearAllButton.getAlpha() == 1 511 && mClearAllButtonDeadZoneRect.contains(x, y); 512 final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; 513 if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar 514 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { 515 mTouchDownToStartHome = true; 516 } 517 } 518 } 519 mDownX = x; 520 mDownY = y; 521 break; 522 } 523 524 525 // Do not let touch escape to siblings below this view. 526 return isHandlingTouch() || shouldStealTouchFromSiblingsBelow(ev); 527 } 528 shouldStealTouchFromSiblingsBelow(MotionEvent ev)529 protected boolean shouldStealTouchFromSiblingsBelow(MotionEvent ev) { 530 return true; 531 } 532 applyLoadPlan(ArrayList<Task> tasks)533 protected void applyLoadPlan(ArrayList<Task> tasks) { 534 if (mPendingAnimation != null) { 535 mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks)); 536 return; 537 } 538 539 if (tasks == null || tasks.isEmpty()) { 540 removeTasksViewsAndClearAllButton(); 541 onTaskStackUpdated(); 542 return; 543 } 544 545 // Unload existing visible task data 546 unloadVisibleTaskData(); 547 548 TaskView ignoreResetTaskView = 549 mIgnoreResetTaskId == -1 ? null : getTaskView(mIgnoreResetTaskId); 550 551 final int requiredTaskCount = tasks.size(); 552 if (getTaskViewCount() != requiredTaskCount) { 553 if (indexOfChild(mClearAllButton) != -1) { 554 removeView(mClearAllButton); 555 } 556 for (int i = getTaskViewCount(); i < requiredTaskCount; i++) { 557 addView(mTaskViewPool.getView()); 558 } 559 while (getTaskViewCount() > requiredTaskCount) { 560 removeView(getChildAt(getChildCount() - 1)); 561 } 562 if (requiredTaskCount > 0) { 563 addView(mClearAllButton); 564 } 565 } 566 567 // Rebind and reset all task views 568 for (int i = requiredTaskCount - 1; i >= 0; i--) { 569 final int pageIndex = requiredTaskCount - i - 1 + mTaskViewStartIndex; 570 final Task task = tasks.get(i); 571 final TaskView taskView = (TaskView) getChildAt(pageIndex); 572 taskView.bind(task); 573 } 574 575 if (mNextPage == INVALID_PAGE) { 576 // Set the current page to the running task, but not if settling on new task. 577 TaskView runningTaskView = getRunningTaskView(); 578 if (runningTaskView != null) { 579 setCurrentPage(indexOfChild(runningTaskView)); 580 } else if (getTaskViewCount() > 0) { 581 setCurrentPage(indexOfChild(getTaskViewAt(0))); 582 } 583 } 584 585 if (mIgnoreResetTaskId != -1 && getTaskView(mIgnoreResetTaskId) != ignoreResetTaskView) { 586 // If the taskView mapping is changing, do not preserve the visuals. Since we are 587 // mostly preserving the first task, and new taskViews are added to the end, it should 588 // generally map to the same task. 589 mIgnoreResetTaskId = -1; 590 } 591 resetTaskVisuals(); 592 onTaskStackUpdated(); 593 updateEnabledOverlays(); 594 } 595 removeTasksViewsAndClearAllButton()596 private void removeTasksViewsAndClearAllButton() { 597 for (int i = getTaskViewCount() - 1; i >= 0; i--) { 598 removeView(getTaskViewAt(i)); 599 } 600 if (indexOfChild(mClearAllButton) != -1) { 601 removeView(mClearAllButton); 602 } 603 } 604 getTaskViewCount()605 public int getTaskViewCount() { 606 int taskViewCount = getChildCount() - mTaskViewStartIndex; 607 if (indexOfChild(mClearAllButton) != -1) { 608 taskViewCount--; 609 } 610 return taskViewCount; 611 } 612 onTaskStackUpdated()613 protected void onTaskStackUpdated() { } 614 resetTaskVisuals()615 public void resetTaskVisuals() { 616 for (int i = getTaskViewCount() - 1; i >= 0; i--) { 617 TaskView taskView = getTaskViewAt(i); 618 if (mIgnoreResetTaskId != taskView.getTask().key.id) { 619 taskView.resetVisualProperties(); 620 taskView.setStableAlpha(mContentAlpha); 621 } 622 } 623 if (mRunningTaskTileHidden) { 624 setRunningTaskHidden(mRunningTaskTileHidden); 625 } 626 627 // Force apply the scale. 628 if (mIgnoreResetTaskId != mRunningTaskId) { 629 applyRunningTaskIconScale(); 630 } 631 632 updateCurveProperties(); 633 // Update the set of visible task's data 634 loadVisibleTaskData(); 635 } 636 setFullscreenProgress(float fullscreenProgress)637 public void setFullscreenProgress(float fullscreenProgress) { 638 mFullscreenProgress = fullscreenProgress; 639 int taskCount = getTaskViewCount(); 640 for (int i = 0; i < taskCount; i++) { 641 getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress); 642 } 643 } 644 updateTaskStackListenerState()645 private void updateTaskStackListenerState() { 646 boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() 647 && getWindowVisibility() == VISIBLE; 648 if (handleTaskStackChanges != mHandleTaskStackChanges) { 649 mHandleTaskStackChanges = handleTaskStackChanges; 650 if (handleTaskStackChanges) { 651 reloadIfNeeded(); 652 } 653 } 654 } 655 656 @Override setInsets(Rect insets)657 public void setInsets(Rect insets) { 658 mInsets.set(insets); 659 DeviceProfile dp = mActivity.getDeviceProfile(); 660 getTaskSize(dp, mTempRect); 661 mTaskWidth = mTempRect.width(); 662 mTaskHeight = mTempRect.height(); 663 664 mTempRect.top -= mTaskTopMargin; 665 setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, 666 dp.widthPx - mInsets.right - mTempRect.right, 667 dp.heightPx - mInsets.bottom - mTempRect.bottom); 668 } 669 getTaskSize(DeviceProfile dp, Rect outRect)670 protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); 671 getTaskSize(Rect outRect)672 public void getTaskSize(Rect outRect) { 673 getTaskSize(mActivity.getDeviceProfile(), outRect); 674 } 675 676 @Override computeScrollHelper()677 protected boolean computeScrollHelper() { 678 boolean scrolling = super.computeScrollHelper(); 679 boolean isFlingingFast = false; 680 updateCurveProperties(); 681 if (scrolling || isHandlingTouch()) { 682 if (scrolling) { 683 // Check if we are flinging quickly to disable high res thumbnail loading 684 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; 685 } 686 687 // After scrolling, update the visible task's data 688 loadVisibleTaskData(); 689 } 690 691 // Update the high res thumbnail loader state 692 mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast); 693 return scrolling; 694 } 695 696 /** 697 * Scales and adjusts translation of adjacent pages as if on a curved carousel. 698 */ updateCurveProperties()699 public void updateCurveProperties() { 700 if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { 701 return; 702 } 703 int scrollX = getScrollX(); 704 final int halfPageWidth = getNormalChildWidth() / 2; 705 final int screenCenter = mInsets.left + getPaddingLeft() + scrollX + halfPageWidth; 706 final int halfScreenWidth = getMeasuredWidth() / 2; 707 final int pageSpacing = mPageSpacing; 708 mScrollState.scrollFromEdge = mIsRtl ? scrollX : (mMaxScrollX - scrollX); 709 710 final int pageCount = getPageCount(); 711 for (int i = 0; i < pageCount; i++) { 712 View page = getPageAt(i); 713 float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; 714 float distanceFromScreenCenter = screenCenter - pageCenter; 715 float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; 716 mScrollState.linearInterpolation = Math.min(1, 717 Math.abs(distanceFromScreenCenter) / distanceToReachEdge); 718 ((PageCallbacks) page).onPageScroll(mScrollState); 719 } 720 } 721 722 /** 723 * Iterates through all the tasks, and loads the associated task data for newly visible tasks, 724 * and unloads the associated task data for tasks that are no longer visible. 725 */ loadVisibleTaskData()726 public void loadVisibleTaskData() { 727 if (!mOverviewStateEnabled || mTaskListChangeId == -1) { 728 // Skip loading visible task data if we've already left the overview state, or if the 729 // task list hasn't been loaded yet (the task views will not reflect the task list) 730 return; 731 } 732 733 int centerPageIndex = getPageNearestToCenterOfScreen(); 734 int numChildren = getChildCount(); 735 int lower = Math.max(0, centerPageIndex - 2); 736 int upper = Math.min(centerPageIndex + 2, numChildren - 1); 737 738 // Update the task data for the in/visible children 739 for (int i = 0; i < getTaskViewCount(); i++) { 740 TaskView taskView = getTaskViewAt(i); 741 Task task = taskView.getTask(); 742 int index = indexOfChild(taskView); 743 boolean visible = lower <= index && index <= upper; 744 if (visible) { 745 if (task == mTmpRunningTask) { 746 // Skip loading if this is the task that we are animating into 747 continue; 748 } 749 if (!mHasVisibleTaskData.get(task.key.id)) { 750 taskView.onTaskListVisibilityChanged(true /* visible */); 751 } 752 mHasVisibleTaskData.put(task.key.id, visible); 753 } else { 754 if (mHasVisibleTaskData.get(task.key.id)) { 755 taskView.onTaskListVisibilityChanged(false /* visible */); 756 } 757 mHasVisibleTaskData.delete(task.key.id); 758 } 759 } 760 } 761 762 /** 763 * Unloads any associated data from the currently visible tasks 764 */ unloadVisibleTaskData()765 private void unloadVisibleTaskData() { 766 for (int i = 0; i < mHasVisibleTaskData.size(); i++) { 767 if (mHasVisibleTaskData.valueAt(i)) { 768 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); 769 if (taskView != null) { 770 taskView.onTaskListVisibilityChanged(false /* visible */); 771 } 772 } 773 } 774 mHasVisibleTaskData.clear(); 775 } 776 777 @Override onHighResLoadingStateChanged(boolean enabled)778 public void onHighResLoadingStateChanged(boolean enabled) { 779 // Whenever the high res loading state changes, poke each of the visible tasks to see if 780 // they want to updated their thumbnail state 781 for (int i = 0; i < mHasVisibleTaskData.size(); i++) { 782 if (mHasVisibleTaskData.valueAt(i)) { 783 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); 784 if (taskView != null) { 785 // Poke the view again, which will trigger it to load high res if the state 786 // is enabled 787 taskView.onTaskListVisibilityChanged(true /* visible */); 788 } 789 } 790 } 791 } 792 startHome()793 public abstract void startHome(); 794 reset()795 public void reset() { 796 setCurrentTask(-1); 797 mIgnoreResetTaskId = -1; 798 mTaskListChangeId = -1; 799 800 mRecentsAnimationWrapper = null; 801 mClipAnimationHelper = null; 802 803 unloadVisibleTaskData(); 804 setCurrentPage(0); 805 mDwbToastShown = false; 806 mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0); 807 } 808 getRunningTaskView()809 public @Nullable TaskView getRunningTaskView() { 810 return getTaskView(mRunningTaskId); 811 } 812 getRunningTaskIndex()813 public int getRunningTaskIndex() { 814 TaskView tv = getRunningTaskView(); 815 return tv == null ? -1 : indexOfChild(tv); 816 } 817 getTaskViewStartIndex()818 public int getTaskViewStartIndex() { 819 return mTaskViewStartIndex; 820 } 821 822 /** 823 * Reloads the view if anything in recents changed. 824 */ reloadIfNeeded()825 public void reloadIfNeeded() { 826 if (!mModel.isTaskListValid(mTaskListChangeId)) { 827 mTaskListChangeId = mModel.getTasks(this::applyLoadPlan); 828 } 829 } 830 831 /** 832 * Called when a gesture from an app is starting. 833 */ onGestureAnimationStart(int runningTaskId)834 public void onGestureAnimationStart(int runningTaskId) { 835 // This needs to be called before the other states are set since it can create the task view 836 showCurrentTask(runningTaskId); 837 setEnableFreeScroll(false); 838 setEnableDrawingLiveTile(false); 839 setRunningTaskHidden(true); 840 setRunningTaskIconScaledDown(true); 841 } 842 843 /** 844 * Called only when a swipe-up gesture from an app has completed. Only called after 845 * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}. 846 */ onSwipeUpAnimationSuccess()847 public void onSwipeUpAnimationSuccess() { 848 if (getRunningTaskView() != null) { 849 float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlay != null 850 ? mLiveTileOverlay.cancelIconAnimation() 851 : 0f; 852 animateUpRunningTaskIconScale(startProgress); 853 } 854 setSwipeDownShouldLaunchApp(true); 855 } 856 857 /** 858 * Called when a gesture from an app has finished. 859 */ onGestureAnimationEnd()860 public void onGestureAnimationEnd() { 861 setEnableFreeScroll(true); 862 setEnableDrawingLiveTile(true); 863 setOnScrollChangeListener(null); 864 setRunningTaskViewShowScreenshot(true); 865 setRunningTaskHidden(false); 866 animateUpRunningTaskIconScale(); 867 } 868 869 /** 870 * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}. 871 * 872 * All subsequent calls to reload will keep the task as the first item until {@link #reset()} 873 * is called. Also scrolls the view to this task. 874 */ showCurrentTask(int runningTaskId)875 public void showCurrentTask(int runningTaskId) { 876 if (getTaskView(runningTaskId) == null) { 877 boolean wasEmpty = getTaskViewCount() == 0; 878 // Add an empty view for now until the task plan is loaded and applied 879 final TaskView taskView = mTaskViewPool.getView(); 880 addView(taskView, mTaskViewStartIndex); 881 if (wasEmpty) { 882 addView(mClearAllButton); 883 } 884 // The temporary running task is only used for the duration between the start of the 885 // gesture and the task list is loaded and applied 886 mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), 887 new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0, 888 false, true, false, false, new ActivityManager.TaskDescription(), 0, 889 new ComponentName("", ""), false); 890 taskView.bind(mTmpRunningTask); 891 } 892 893 boolean runningTaskTileHidden = mRunningTaskTileHidden; 894 setCurrentTask(runningTaskId); 895 setCurrentPage(getRunningTaskIndex()); 896 setRunningTaskViewShowScreenshot(false); 897 setRunningTaskHidden(runningTaskTileHidden); 898 899 // Reload the task list 900 mTaskListChangeId = mModel.getTasks(this::applyLoadPlan); 901 } 902 903 /** 904 * Sets the running task id, cleaning up the old running task if necessary. 905 * @param runningTaskId 906 */ setCurrentTask(int runningTaskId)907 public void setCurrentTask(int runningTaskId) { 908 if (mRunningTaskId == runningTaskId) { 909 return; 910 } 911 912 if (mRunningTaskId != -1) { 913 // Reset the state on the old running task view 914 setRunningTaskIconScaledDown(false); 915 setRunningTaskViewShowScreenshot(true); 916 setRunningTaskHidden(false); 917 } 918 mRunningTaskId = runningTaskId; 919 } 920 921 /** 922 * Hides the tile associated with {@link #mRunningTaskId} 923 */ setRunningTaskHidden(boolean isHidden)924 public void setRunningTaskHidden(boolean isHidden) { 925 mRunningTaskTileHidden = isHidden; 926 TaskView runningTask = getRunningTaskView(); 927 if (runningTask != null) { 928 runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha); 929 } 930 } 931 setRunningTaskViewShowScreenshot(boolean showScreenshot)932 private void setRunningTaskViewShowScreenshot(boolean showScreenshot) { 933 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 934 TaskView runningTaskView = getRunningTaskView(); 935 if (runningTaskView != null) { 936 runningTaskView.setShowScreenshot(showScreenshot); 937 } 938 } 939 } 940 showNextTask()941 public void showNextTask() { 942 TaskView runningTaskView = getRunningTaskView(); 943 if (runningTaskView == null) { 944 // Launch the first task 945 if (getTaskViewCount() > 0) { 946 getTaskViewAt(0).launchTask(true); 947 } 948 } else { 949 if (getNextTaskView() != null) { 950 getNextTaskView().launchTask(true); 951 } else { 952 runningTaskView.launchTask(true); 953 } 954 } 955 } 956 setRunningTaskIconScaledDown(boolean isScaledDown)957 public void setRunningTaskIconScaledDown(boolean isScaledDown) { 958 if (mRunningTaskIconScaledDown != isScaledDown) { 959 mRunningTaskIconScaledDown = isScaledDown; 960 applyRunningTaskIconScale(); 961 } 962 } 963 isTaskIconScaledDown(TaskView taskView)964 public boolean isTaskIconScaledDown(TaskView taskView) { 965 return mRunningTaskIconScaledDown && getRunningTaskView() == taskView; 966 } 967 applyRunningTaskIconScale()968 private void applyRunningTaskIconScale() { 969 TaskView firstTask = getRunningTaskView(); 970 if (firstTask != null) { 971 firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1); 972 } 973 } 974 animateUpRunningTaskIconScale()975 public void animateUpRunningTaskIconScale() { 976 animateUpRunningTaskIconScale(0); 977 } 978 animateUpRunningTaskIconScale(float startProgress)979 public void animateUpRunningTaskIconScale(float startProgress) { 980 mRunningTaskIconScaledDown = false; 981 TaskView firstTask = getRunningTaskView(); 982 if (firstTask != null) { 983 firstTask.animateIconScaleAndDimIntoView(); 984 firstTask.setIconScaleAnimStartProgress(startProgress); 985 } 986 } 987 enableLayoutTransitions()988 private void enableLayoutTransitions() { 989 if (mLayoutTransition == null) { 990 mLayoutTransition = new LayoutTransition(); 991 mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING); 992 mLayoutTransition.setDuration(ADDITION_TASK_DURATION); 993 mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0); 994 995 mLayoutTransition.addTransitionListener(new TransitionListener() { 996 @Override 997 public void startTransition(LayoutTransition transition, ViewGroup viewGroup, 998 View view, int i) { 999 } 1000 1001 @Override 1002 public void endTransition(LayoutTransition transition, ViewGroup viewGroup, 1003 View view, int i) { 1004 // When the unpinned task is added, snap to first page and disable transitions 1005 if (view instanceof TaskView) { 1006 snapToPage(0); 1007 disableLayoutTransitions(); 1008 } 1009 1010 } 1011 }); 1012 } 1013 setLayoutTransition(mLayoutTransition); 1014 } 1015 disableLayoutTransitions()1016 private void disableLayoutTransitions() { 1017 setLayoutTransition(null); 1018 } 1019 setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp)1020 public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { 1021 mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; 1022 } 1023 shouldSwipeDownLaunchApp()1024 public boolean shouldSwipeDownLaunchApp() { 1025 return mSwipeDownShouldLaunchApp; 1026 } 1027 1028 public interface PageCallbacks { 1029 1030 /** 1031 * Updates the page UI based on scroll params. 1032 */ onPageScroll(ScrollState scrollState)1033 default void onPageScroll(ScrollState scrollState) {} 1034 } 1035 1036 public static class ScrollState { 1037 1038 /** 1039 * The progress from 0 to 1, where 0 is the center 1040 * of the screen and 1 is the edge of the screen. 1041 */ 1042 public float linearInterpolation; 1043 1044 /** 1045 * The amount by which all the content is scrolled relative to the end of the list. 1046 */ 1047 public float scrollFromEdge; 1048 } 1049 setIgnoreResetTask(int taskId)1050 public void setIgnoreResetTask(int taskId) { 1051 mIgnoreResetTaskId = taskId; 1052 } 1053 clearIgnoreResetTask(int taskId)1054 public void clearIgnoreResetTask(int taskId) { 1055 if (mIgnoreResetTaskId == taskId) { 1056 mIgnoreResetTaskId = -1; 1057 } 1058 } 1059 addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration)1060 private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { 1061 addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); 1062 if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView) 1063 addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y, 1064 MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, 1065 SpringForce.STIFFNESS_MEDIUM, 1066 0, -taskView.getHeight()), 1067 duration, LINEAR, anim); 1068 else { 1069 addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), 1070 duration, LINEAR, anim); 1071 } 1072 } 1073 removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, boolean shouldLog)1074 private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, 1075 boolean shouldLog) { 1076 if (task != null) { 1077 ActivityManagerWrapper.getInstance().removeTask(task.key.id); 1078 if (shouldLog) { 1079 ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key); 1080 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( 1081 onEndListener.logAction, Direction.UP, index, componentKey); 1082 mActivity.getStatsLogManager().logTaskDismiss(this, componentKey); 1083 } 1084 } 1085 } 1086 createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, boolean shouldRemoveTask, long duration)1087 public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, 1088 boolean shouldRemoveTask, long duration) { 1089 if (mPendingAnimation != null) { 1090 mPendingAnimation.finish(false, Touch.SWIPE); 1091 } 1092 AnimatorSet anim = new AnimatorSet(); 1093 PendingAnimation pendingAnimation = new PendingAnimation(anim); 1094 1095 int count = getPageCount(); 1096 if (count == 0) { 1097 return pendingAnimation; 1098 } 1099 1100 int[] oldScroll = new int[count]; 1101 getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); 1102 1103 int[] newScroll = new int[count]; 1104 getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); 1105 1106 int taskCount = getTaskViewCount(); 1107 int scrollDiffPerPage = 0; 1108 if (count > 1) { 1109 scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); 1110 } 1111 int draggedIndex = indexOfChild(taskView); 1112 1113 boolean needsCurveUpdates = false; 1114 for (int i = 0; i < count; i++) { 1115 View child = getChildAt(i); 1116 if (child == taskView) { 1117 if (animateTaskView) { 1118 addDismissedTaskAnimations(taskView, anim, duration); 1119 } 1120 } else { 1121 // If we just take newScroll - oldScroll, everything to the right of dragged task 1122 // translates to the left. We need to offset this in some cases: 1123 // - In RTL, add page offset to all pages, since we want pages to move to the right 1124 // Additionally, add a page offset if: 1125 // - Current page is rightmost page (leftmost for RTL) 1126 // - Dragging an adjacent page on the left side (right side for RTL) 1127 int offset = mIsRtl ? scrollDiffPerPage : 0; 1128 if (mCurrentPage == draggedIndex) { 1129 int lastPage = taskCount - 1; 1130 if (mCurrentPage == lastPage) { 1131 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; 1132 } 1133 } else { 1134 // Dragging an adjacent page. 1135 int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) 1136 if (draggedIndex == negativeAdjacent) { 1137 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; 1138 } 1139 } 1140 int scrollDiff = newScroll[i] - oldScroll[i] + offset; 1141 if (scrollDiff != 0) { 1142 if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) { 1143 addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X, 1144 MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY, 1145 SpringForce.STIFFNESS_MEDIUM, 1146 0, scrollDiff), duration, ACCEL, anim); 1147 } else { 1148 addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration, 1149 ACCEL, anim); 1150 } 1151 1152 needsCurveUpdates = true; 1153 } 1154 } 1155 } 1156 1157 if (needsCurveUpdates) { 1158 ValueAnimator va = ValueAnimator.ofFloat(0, 1); 1159 va.addUpdateListener((a) -> updateCurveProperties()); 1160 anim.play(va); 1161 } 1162 1163 // Add a tiny bit of translation Z, so that it draws on top of other views 1164 if (animateTaskView) { 1165 taskView.setTranslationZ(0.1f); 1166 } 1167 1168 mPendingAnimation = pendingAnimation; 1169 mPendingAnimation.addEndListener(new Consumer<PendingAnimation.OnEndListener>() { 1170 @Override 1171 public void accept(PendingAnimation.OnEndListener onEndListener) { 1172 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && 1173 taskView.isRunningTask() && onEndListener.isSuccess) { 1174 finishRecentsAnimation(true /* toHome */, () -> onEnd(onEndListener)); 1175 } else { 1176 onEnd(onEndListener); 1177 } 1178 } 1179 1180 private void onEnd(PendingAnimation.OnEndListener onEndListener) { 1181 if (onEndListener.isSuccess) { 1182 if (shouldRemoveTask) { 1183 removeTask(taskView.getTask(), draggedIndex, onEndListener, true); 1184 } 1185 1186 int pageToSnapTo = mCurrentPage; 1187 if (draggedIndex < pageToSnapTo || 1188 pageToSnapTo == (getTaskViewCount() - 1)) { 1189 pageToSnapTo -= 1; 1190 } 1191 removeView(taskView); 1192 1193 if (getTaskViewCount() == 0) { 1194 removeView(mClearAllButton); 1195 startHome(); 1196 } else { 1197 snapToPageImmediately(pageToSnapTo); 1198 } 1199 } 1200 resetTaskVisuals(); 1201 mPendingAnimation = null; 1202 } 1203 }); 1204 return pendingAnimation; 1205 } 1206 createAllTasksDismissAnimation(long duration)1207 public PendingAnimation createAllTasksDismissAnimation(long duration) { 1208 if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { 1209 throw new IllegalStateException("Another pending animation is still running"); 1210 } 1211 AnimatorSet anim = new AnimatorSet(); 1212 PendingAnimation pendingAnimation = new PendingAnimation(anim); 1213 1214 int count = getTaskViewCount(); 1215 for (int i = 0; i < count; i++) { 1216 addDismissedTaskAnimations(getTaskViewAt(i), anim, duration); 1217 } 1218 1219 mPendingAnimation = pendingAnimation; 1220 mPendingAnimation.addEndListener((onEndListener) -> { 1221 if (onEndListener.isSuccess) { 1222 // Remove all the task views now 1223 ActivityManagerWrapper.getInstance().removeAllRecentTasks(); 1224 removeTasksViewsAndClearAllButton(); 1225 startHome(); 1226 } 1227 mPendingAnimation = null; 1228 }); 1229 return pendingAnimation; 1230 } 1231 addAnim(Animator anim, long duration, TimeInterpolator interpolator, AnimatorSet set)1232 private static void addAnim(Animator anim, long duration, 1233 TimeInterpolator interpolator, AnimatorSet set) { 1234 anim.setDuration(duration).setInterpolator(interpolator); 1235 set.play(anim); 1236 } 1237 snapToPageRelative(int pageCount, int delta, boolean cycle)1238 private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) { 1239 if (pageCount == 0) { 1240 return false; 1241 } 1242 final int newPageUnbound = getNextPage() + delta; 1243 if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) { 1244 return false; 1245 } 1246 snapToPage((newPageUnbound + pageCount) % pageCount); 1247 getChildAt(getNextPage()).requestFocus(); 1248 return true; 1249 } 1250 runDismissAnimation(PendingAnimation pendingAnim)1251 private void runDismissAnimation(PendingAnimation pendingAnim) { 1252 AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( 1253 pendingAnim.anim, DISMISS_TASK_DURATION); 1254 controller.dispatchOnStart(); 1255 controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE)); 1256 controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); 1257 controller.start(); 1258 } 1259 dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask)1260 public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { 1261 runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, 1262 DISMISS_TASK_DURATION)); 1263 } 1264 1265 @SuppressWarnings("unused") dismissAllTasks(View view)1266 private void dismissAllTasks(View view) { 1267 runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); 1268 mActivity.getUserEventDispatcher().logActionOnControl(TAP, CLEAR_ALL_BUTTON); 1269 } 1270 dismissCurrentTask()1271 private void dismissCurrentTask() { 1272 TaskView taskView = getTaskView(getNextPage()); 1273 if (taskView != null) { 1274 dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/); 1275 } 1276 } 1277 1278 @Override dispatchKeyEvent(KeyEvent event)1279 public boolean dispatchKeyEvent(KeyEvent event) { 1280 if (event.getAction() == KeyEvent.ACTION_DOWN) { 1281 switch (event.getKeyCode()) { 1282 case KeyEvent.KEYCODE_TAB: 1283 return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1, 1284 event.isAltPressed() /* cycle */); 1285 case KeyEvent.KEYCODE_DPAD_RIGHT: 1286 return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */); 1287 case KeyEvent.KEYCODE_DPAD_LEFT: 1288 return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */); 1289 case KeyEvent.KEYCODE_DEL: 1290 case KeyEvent.KEYCODE_FORWARD_DEL: 1291 dismissCurrentTask(); 1292 return true; 1293 case KeyEvent.KEYCODE_NUMPAD_DOT: 1294 if (event.isAltPressed()) { 1295 // Numpad DEL pressed while holding Alt. 1296 dismissCurrentTask(); 1297 return true; 1298 } 1299 } 1300 } 1301 return super.dispatchKeyEvent(event); 1302 } 1303 1304 @Override onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect)1305 protected void onFocusChanged(boolean gainFocus, int direction, 1306 @Nullable Rect previouslyFocusedRect) { 1307 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1308 if (gainFocus && getChildCount() > 0) { 1309 switch (direction) { 1310 case FOCUS_FORWARD: 1311 setCurrentPage(0); 1312 break; 1313 case FOCUS_BACKWARD: 1314 case FOCUS_RIGHT: 1315 case FOCUS_LEFT: 1316 setCurrentPage(getChildCount() - 1); 1317 break; 1318 } 1319 } 1320 } 1321 getContentAlpha()1322 public float getContentAlpha() { 1323 return mContentAlpha; 1324 } 1325 setContentAlpha(float alpha)1326 public void setContentAlpha(float alpha) { 1327 if (alpha == mContentAlpha) { 1328 return; 1329 } 1330 alpha = Utilities.boundToRange(alpha, 0, 1); 1331 mContentAlpha = alpha; 1332 for (int i = getTaskViewCount() - 1; i >= 0; i--) { 1333 TaskView child = getTaskViewAt(i); 1334 if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { 1335 child.setStableAlpha(alpha); 1336 } 1337 } 1338 mClearAllButton.setContentAlpha(mContentAlpha); 1339 1340 int alphaInt = Math.round(alpha * 255); 1341 mEmptyMessagePaint.setAlpha(alphaInt); 1342 mEmptyIcon.setAlpha(alphaInt); 1343 1344 if (alpha > 0) { 1345 setVisibility(VISIBLE); 1346 } else if (!mFreezeViewVisibility) { 1347 setVisibility(GONE); 1348 } 1349 } 1350 1351 /** 1352 * Freezes the view visibility change. When frozen, the view will not change its visibility 1353 * to gone due to alpha changes. 1354 */ setFreezeViewVisibility(boolean freezeViewVisibility)1355 public void setFreezeViewVisibility(boolean freezeViewVisibility) { 1356 if (mFreezeViewVisibility != freezeViewVisibility) { 1357 mFreezeViewVisibility = freezeViewVisibility; 1358 1359 if (!mFreezeViewVisibility) { 1360 setVisibility(mContentAlpha > 0 ? VISIBLE : GONE); 1361 } 1362 } 1363 } 1364 1365 @Override onViewAdded(View child)1366 public void onViewAdded(View child) { 1367 super.onViewAdded(child); 1368 child.setAlpha(mContentAlpha); 1369 } 1370 1371 @Nullable getNextTaskView()1372 public TaskView getNextTaskView() { 1373 return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() + 1); 1374 } 1375 1376 @Nullable getPreviousTaskView()1377 public TaskView getPreviousTaskView() { 1378 return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1); 1379 } 1380 1381 @Nullable getCurrentPageTaskView()1382 public TaskView getCurrentPageTaskView() { 1383 return getTaskViewAtByAbsoluteIndex(getCurrentPage()); 1384 } 1385 1386 @Nullable getNextPageTaskView()1387 public TaskView getNextPageTaskView() { 1388 return getTaskViewAtByAbsoluteIndex(getNextPage()); 1389 } 1390 1391 @Nullable getTaskViewNearestToCenterOfScreen()1392 public TaskView getTaskViewNearestToCenterOfScreen() { 1393 return getTaskViewAtByAbsoluteIndex(getPageNearestToCenterOfScreen()); 1394 } 1395 1396 /** 1397 * Returns null instead of indexOutOfBoundsError when index is not in range 1398 */ 1399 @Nullable getTaskViewAt(int index)1400 public TaskView getTaskViewAt(int index) { 1401 return getTaskViewAtByAbsoluteIndex(index + mTaskViewStartIndex); 1402 } 1403 1404 @Nullable getTaskViewAtByAbsoluteIndex(int index)1405 private TaskView getTaskViewAtByAbsoluteIndex(int index) { 1406 if (index < getChildCount() && index >= 0) { 1407 View child = getChildAt(index); 1408 return child instanceof TaskView ? (TaskView) child : null; 1409 } 1410 return null; 1411 } 1412 updateEmptyMessage()1413 public void updateEmptyMessage() { 1414 boolean isEmpty = getTaskViewCount() == 0; 1415 boolean hasSizeChanged = mLastMeasureSize.x != getWidth() 1416 || mLastMeasureSize.y != getHeight(); 1417 if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { 1418 return; 1419 } 1420 setContentDescription(isEmpty ? mEmptyMessage : ""); 1421 mShowEmptyMessage = isEmpty; 1422 updateEmptyStateUi(hasSizeChanged); 1423 invalidate(); 1424 } 1425 1426 @Override onLayout(boolean changed, int left, int top, int right, int bottom)1427 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1428 super.onLayout(changed, left, top, right, bottom); 1429 updateEmptyStateUi(changed); 1430 1431 // Set the pivot points to match the task preview center 1432 setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin) 1433 + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2); 1434 setPivotX(((mInsets.left + getPaddingLeft()) 1435 + (getWidth() - mInsets.right - getPaddingRight())) / 2); 1436 } 1437 updateDeadZoneRects()1438 private void updateDeadZoneRects() { 1439 // Get the deadzone rect surrounding the clear all button to not dismiss overview to home 1440 mClearAllButtonDeadZoneRect.setEmpty(); 1441 if (mClearAllButton.getWidth() > 0) { 1442 int verticalMargin = getResources() 1443 .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin); 1444 mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect); 1445 mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin); 1446 } 1447 1448 // Get the deadzone rect between the task views 1449 mTaskViewDeadZoneRect.setEmpty(); 1450 int count = getTaskViewCount(); 1451 if (count > 0) { 1452 final View taskView = getTaskViewAt(0); 1453 getTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); 1454 mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), 1455 taskView.getBottom()); 1456 } 1457 } 1458 updateEmptyStateUi(boolean sizeChanged)1459 private void updateEmptyStateUi(boolean sizeChanged) { 1460 boolean hasValidSize = getWidth() > 0 && getHeight() > 0; 1461 if (sizeChanged && hasValidSize) { 1462 mEmptyTextLayout = null; 1463 mLastMeasureSize.set(getWidth(), getHeight()); 1464 } 1465 1466 if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { 1467 int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; 1468 mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), 1469 mEmptyMessagePaint, availableWidth) 1470 .setAlignment(Layout.Alignment.ALIGN_CENTER) 1471 .build(); 1472 int totalHeight = mEmptyTextLayout.getHeight() 1473 + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); 1474 1475 int top = (mLastMeasureSize.y - totalHeight) / 2; 1476 int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; 1477 mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), 1478 top + mEmptyIcon.getIntrinsicHeight()); 1479 } 1480 } 1481 1482 @Override verifyDrawable(Drawable who)1483 protected boolean verifyDrawable(Drawable who) { 1484 return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); 1485 } 1486 maybeDrawEmptyMessage(Canvas canvas)1487 protected void maybeDrawEmptyMessage(Canvas canvas) { 1488 if (mShowEmptyMessage && mEmptyTextLayout != null) { 1489 // Offset to center in the visible (non-padded) part of RecentsView 1490 mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), 1491 mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); 1492 canvas.save(); 1493 canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, 1494 (mTempRect.top - mTempRect.bottom) / 2); 1495 mEmptyIcon.draw(canvas); 1496 canvas.translate(mEmptyMessagePadding, 1497 mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); 1498 mEmptyTextLayout.draw(canvas); 1499 canvas.restore(); 1500 } 1501 } 1502 1503 /** 1504 * Animate adjacent tasks off screen while scaling up. 1505 * 1506 * If launching one of the adjacent tasks, parallax the center task and other adjacent task 1507 * to the right. 1508 */ createAdjacentPageAnimForTaskLaunch( TaskView tv, ClipAnimationHelper clipAnimationHelper)1509 public AnimatorSet createAdjacentPageAnimForTaskLaunch( 1510 TaskView tv, ClipAnimationHelper clipAnimationHelper) { 1511 AnimatorSet anim = new AnimatorSet(); 1512 1513 int taskIndex = indexOfChild(tv); 1514 int centerTaskIndex = getCurrentPage(); 1515 boolean launchingCenterTask = taskIndex == centerTaskIndex; 1516 1517 LauncherState.ScaleAndTranslation toScaleAndTranslation = clipAnimationHelper 1518 .getScaleAndTranslation(); 1519 float toScale = toScaleAndTranslation.scale; 1520 float toTranslationY = toScaleAndTranslation.translationY; 1521 if (launchingCenterTask) { 1522 RecentsView recentsView = tv.getRecentsView(); 1523 anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale)); 1524 anim.play(ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, toTranslationY)); 1525 anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1)); 1526 } else { 1527 // We are launching an adjacent task, so parallax the center and other adjacent task. 1528 float displacementX = tv.getWidth() * (toScale - tv.getCurveScale()); 1529 anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X, 1530 mIsRtl ? -displacementX : displacementX)); 1531 1532 int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); 1533 if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { 1534 anim.play(new PropertyListBuilder() 1535 .translationX(mIsRtl ? -displacementX : displacementX) 1536 .scale(1) 1537 .build(getPageAt(otherAdjacentTaskIndex))); 1538 } 1539 } 1540 return anim; 1541 } 1542 createTaskLauncherAnimation(TaskView tv, long duration)1543 public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { 1544 if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { 1545 throw new IllegalStateException("Another pending animation is still running"); 1546 } 1547 1548 int count = getTaskViewCount(); 1549 if (count == 0) { 1550 return new PendingAnimation(new AnimatorSet()); 1551 } 1552 1553 int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); 1554 final boolean[] passedOverviewThreshold = new boolean[] {false}; 1555 ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1); 1556 progressAnim.setInterpolator(LINEAR); 1557 progressAnim.addUpdateListener(animator -> { 1558 // Once we pass a certain threshold, update the sysui flags to match the target 1559 // tasks' flags 1560 mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 1561 animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD 1562 ? targetSysUiFlags 1563 : 0); 1564 1565 onTaskLaunchAnimationUpdate(animator.getAnimatedFraction(), tv); 1566 1567 // Passing the threshold from taskview to fullscreen app will vibrate 1568 final boolean passed = animator.getAnimatedFraction() >= 1569 SUCCESS_TRANSITION_PROGRESS; 1570 if (passed != passedOverviewThreshold[0]) { 1571 passedOverviewThreshold[0] = passed; 1572 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 1573 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 1574 } 1575 }); 1576 1577 ClipAnimationHelper clipAnimationHelper = new ClipAnimationHelper(mActivity); 1578 clipAnimationHelper.fromTaskThumbnailView(tv.getThumbnail(), this); 1579 clipAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */); 1580 AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, clipAnimationHelper); 1581 anim.play(progressAnim); 1582 anim.setDuration(duration); 1583 1584 Consumer<Boolean> onTaskLaunchFinish = this::onTaskLaunched; 1585 1586 mPendingAnimation = new PendingAnimation(anim); 1587 mPendingAnimation.addEndListener((onEndListener) -> { 1588 if (onEndListener.isSuccess) { 1589 Consumer<Boolean> onLaunchResult = (result) -> { 1590 onTaskLaunchFinish.accept(result); 1591 if (!result) { 1592 tv.notifyTaskLaunchFailed(TAG); 1593 } 1594 }; 1595 tv.launchTask(false, onLaunchResult, getHandler()); 1596 Task task = tv.getTask(); 1597 if (task != null) { 1598 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( 1599 onEndListener.logAction, Direction.DOWN, indexOfChild(tv), 1600 TaskUtils.getLaunchComponentKeyForTask(task.key)); 1601 } 1602 } else { 1603 onTaskLaunchFinish.accept(false); 1604 } 1605 mPendingAnimation = null; 1606 }); 1607 return mPendingAnimation; 1608 } 1609 onTaskLaunchAnimationUpdate(float progress, TaskView tv)1610 protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) { 1611 } 1612 shouldUseMultiWindowTaskSizeStrategy()1613 public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); 1614 onTaskLaunched(boolean success)1615 protected void onTaskLaunched(boolean success) { 1616 if (success) { 1617 resetTaskVisuals(); 1618 } 1619 } 1620 1621 @Override notifyPageSwitchListener(int prevPage)1622 protected void notifyPageSwitchListener(int prevPage) { 1623 super.notifyPageSwitchListener(prevPage); 1624 loadVisibleTaskData(); 1625 updateEnabledOverlays(); 1626 } 1627 1628 @Override getCurrentPageDescription()1629 protected String getCurrentPageDescription() { 1630 return ""; 1631 } 1632 1633 @Override addChildrenForAccessibility(ArrayList<View> outChildren)1634 public void addChildrenForAccessibility(ArrayList<View> outChildren) { 1635 // Add children in reverse order 1636 for (int i = getChildCount() - 1; i >= 0; --i) { 1637 outChildren.add(getChildAt(i)); 1638 } 1639 } 1640 1641 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1642 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1643 super.onInitializeAccessibilityNodeInfo(info); 1644 final AccessibilityNodeInfo.CollectionInfo 1645 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( 1646 1, getTaskViewCount(), false, 1647 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); 1648 info.setCollectionInfo(collectionInfo); 1649 } 1650 1651 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1652 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1653 super.onInitializeAccessibilityEvent(event); 1654 1655 final int taskViewCount = getTaskViewCount(); 1656 event.setScrollable(taskViewCount > 0); 1657 1658 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { 1659 final int[] visibleTasks = getVisibleChildrenRange(); 1660 event.setFromIndex(taskViewCount - visibleTasks[1] - 1); 1661 event.setToIndex(taskViewCount - visibleTasks[0] - 1); 1662 event.setItemCount(taskViewCount); 1663 } 1664 } 1665 1666 @Override getAccessibilityClassName()1667 public CharSequence getAccessibilityClassName() { 1668 // To hear position-in-list related feedback from Talkback. 1669 return ListView.class.getName(); 1670 } 1671 1672 @Override isPageOrderFlipped()1673 protected boolean isPageOrderFlipped() { 1674 return true; 1675 } 1676 setEnableDrawingLiveTile(boolean enableDrawingLiveTile)1677 public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) { 1678 mEnableDrawingLiveTile = enableDrawingLiveTile; 1679 } 1680 redrawLiveTile(boolean mightNeedToRefill)1681 public void redrawLiveTile(boolean mightNeedToRefill) { } 1682 setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper)1683 public void setRecentsAnimationWrapper(RecentsAnimationWrapper recentsAnimationWrapper) { 1684 mRecentsAnimationWrapper = recentsAnimationWrapper; 1685 } 1686 setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper)1687 public void setClipAnimationHelper(ClipAnimationHelper clipAnimationHelper) { 1688 mClipAnimationHelper = clipAnimationHelper; 1689 } 1690 setLiveTileOverlay(LiveTileOverlay liveTileOverlay)1691 public void setLiveTileOverlay(LiveTileOverlay liveTileOverlay) { 1692 mLiveTileOverlay = liveTileOverlay; 1693 } 1694 updateLiveTileIcon(Drawable icon)1695 public void updateLiveTileIcon(Drawable icon) { 1696 if (mLiveTileOverlay != null) { 1697 mLiveTileOverlay.setIcon(icon); 1698 } 1699 } 1700 finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete)1701 public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) { 1702 if (mRecentsAnimationWrapper == null) { 1703 if (onFinishComplete != null) { 1704 onFinishComplete.run(); 1705 } 1706 return; 1707 } 1708 1709 mRecentsAnimationWrapper.finish(toRecents, onFinishComplete); 1710 } 1711 setDisallowScrollToClearAll(boolean disallowScrollToClearAll)1712 public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) { 1713 if (mDisallowScrollToClearAll != disallowScrollToClearAll) { 1714 mDisallowScrollToClearAll = disallowScrollToClearAll; 1715 updateMinAndMaxScrollX(); 1716 } 1717 } 1718 1719 @Override computeMinScrollX()1720 protected int computeMinScrollX() { 1721 if (getTaskViewCount() > 0) { 1722 if (mDisallowScrollToClearAll) { 1723 // We aren't showing the clear all button, 1724 // so use the leftmost task as the min scroll. 1725 if (mIsRtl) { 1726 return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1))); 1727 } 1728 return getScrollForPage(mTaskViewStartIndex); 1729 } 1730 if (mIsRtl) { 1731 return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1); 1732 } 1733 return getScrollForPage(mTaskViewStartIndex); 1734 } 1735 return super.computeMinScrollX(); 1736 } 1737 1738 @Override computeMaxScrollX()1739 protected int computeMaxScrollX() { 1740 if (getTaskViewCount() > 0) { 1741 if (mDisallowScrollToClearAll) { 1742 // We aren't showing the clear all button, 1743 // so use the rightmost task as the min scroll. 1744 if (mIsRtl) { 1745 return getScrollForPage(mTaskViewStartIndex); 1746 } 1747 return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1))); 1748 } 1749 if (mIsRtl) { 1750 return getScrollForPage(mTaskViewStartIndex); 1751 } 1752 return getScrollForPage(indexOfChild(getTaskViewAt(getTaskViewCount() - 1)) + 1); 1753 } 1754 return super.computeMaxScrollX(); 1755 } 1756 getClearAllButton()1757 public ClearAllButton getClearAllButton() { 1758 return mClearAllButton; 1759 } 1760 1761 /** 1762 * @return How many pixels the running task is offset on the x-axis due to the current scrollX. 1763 */ getScrollOffset()1764 public float getScrollOffset() { 1765 if (getRunningTaskIndex() == -1) { 1766 return 0; 1767 } 1768 int startScroll = getScrollForPage(getRunningTaskIndex()); 1769 int offsetX = startScroll - getScrollX(); 1770 offsetX *= getScaleX(); 1771 return offsetX; 1772 } 1773 getEventDispatcher(RotationMode rotationMode)1774 public Consumer<MotionEvent> getEventDispatcher(RotationMode rotationMode) { 1775 if (rotationMode.isTransposed) { 1776 Matrix transform = new Matrix(); 1777 transform.setRotate(-rotationMode.surfaceRotation); 1778 1779 if (getWidth() > 0 && getHeight() > 0) { 1780 float scale = ((float) getWidth()) / getHeight(); 1781 transform.postScale(scale, 1 / scale); 1782 } 1783 1784 Matrix inverse = new Matrix(); 1785 transform.invert(inverse); 1786 return e -> { 1787 e.transform(transform); 1788 super.onTouchEvent(e); 1789 e.transform(inverse); 1790 }; 1791 } else { 1792 return super::onTouchEvent; 1793 } 1794 } 1795 getTempClipAnimationHelper()1796 public ClipAnimationHelper getTempClipAnimationHelper() { 1797 return mTempClipAnimationHelper; 1798 } 1799 updateEnabledOverlays()1800 private void updateEnabledOverlays() { 1801 int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1; 1802 int taskCount = getTaskViewCount(); 1803 for (int i = 0; i < taskCount; i++) { 1804 getTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage); 1805 } 1806 } 1807 setOverlayEnabled(boolean overlayEnabled)1808 public void setOverlayEnabled(boolean overlayEnabled) { 1809 if (mOverlayEnabled != overlayEnabled) { 1810 mOverlayEnabled = overlayEnabled; 1811 updateEnabledOverlays(); 1812 } 1813 } 1814 getLeftGestureMargin()1815 public int getLeftGestureMargin() { 1816 final WindowInsets insets = getRootWindowInsets(); 1817 return Math.max(insets.getSystemGestureInsets().left, insets.getSystemWindowInsetLeft()); 1818 } 1819 getRightGestureMargin()1820 public int getRightGestureMargin() { 1821 final WindowInsets insets = getRootWindowInsets(); 1822 return Math.max(insets.getSystemGestureInsets().right, insets.getSystemWindowInsetRight()); 1823 } 1824 1825 @Override addView(View child, int index)1826 public void addView(View child, int index) { 1827 super.addView(child, index); 1828 if (isExtraCardView(child, index)) { 1829 mTaskViewStartIndex++; 1830 } 1831 } 1832 1833 @Override removeView(View view)1834 public void removeView(View view) { 1835 if (isExtraCardView(view, indexOfChild(view))) { 1836 mTaskViewStartIndex--; 1837 } 1838 super.removeView(view); 1839 } 1840 isExtraCardView(View view, int index)1841 private boolean isExtraCardView(View view, int index) { 1842 return !(view instanceof TaskView) && !(view instanceof ClearAllButton) 1843 && index <= mTaskViewStartIndex; 1844 } 1845 } 1846