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.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.view.Surface.ROTATION_0; 21 import static android.view.View.MeasureSpec.EXACTLY; 22 import static android.view.View.MeasureSpec.makeMeasureSpec; 23 24 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU; 25 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; 26 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; 27 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; 28 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; 29 import static com.android.launcher3.LauncherState.BACKGROUND_APP; 30 import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION; 31 import static com.android.launcher3.Utilities.EDGE_NAV_BAR; 32 import static com.android.launcher3.Utilities.mapToRange; 33 import static com.android.launcher3.Utilities.squaredHypot; 34 import static com.android.launcher3.Utilities.squaredTouchSlop; 35 import static com.android.launcher3.anim.Interpolators.ACCEL; 36 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75; 37 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; 38 import static com.android.launcher3.anim.Interpolators.DEACCEL_2; 39 import static com.android.launcher3.anim.Interpolators.EMPHASIZED_DECELERATE; 40 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 41 import static com.android.launcher3.anim.Interpolators.FINAL_FRAME; 42 import static com.android.launcher3.anim.Interpolators.LINEAR; 43 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_0_75; 44 import static com.android.launcher3.anim.Interpolators.clampToProgress; 45 import static com.android.launcher3.config.FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW; 46 import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCH_FROM_STAGED_APP; 47 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_ACTIONS_SPLIT; 48 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL; 49 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP; 50 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN; 51 import static com.android.launcher3.touch.PagedOrientationHandler.CANVAS_TRANSLATE; 52 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 53 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 54 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; 55 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; 56 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; 57 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA; 58 import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED; 59 import static com.android.quickstep.views.OverviewActionsView.FLAG_IS_NOT_TABLET; 60 import static com.android.quickstep.views.OverviewActionsView.FLAG_SINGLE_TASK; 61 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU; 62 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP; 63 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION; 64 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS; 65 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS; 66 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SCREEN; 67 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE; 68 69 import android.animation.Animator; 70 import android.animation.AnimatorListenerAdapter; 71 import android.animation.AnimatorSet; 72 import android.animation.LayoutTransition; 73 import android.animation.LayoutTransition.TransitionListener; 74 import android.animation.ObjectAnimator; 75 import android.animation.PropertyValuesHolder; 76 import android.animation.ValueAnimator; 77 import android.annotation.SuppressLint; 78 import android.annotation.TargetApi; 79 import android.app.WindowConfiguration; 80 import android.content.Context; 81 import android.content.Intent; 82 import android.content.LocusId; 83 import android.content.res.Configuration; 84 import android.graphics.Bitmap; 85 import android.graphics.BlendMode; 86 import android.graphics.Canvas; 87 import android.graphics.Color; 88 import android.graphics.Matrix; 89 import android.graphics.Point; 90 import android.graphics.PointF; 91 import android.graphics.Rect; 92 import android.graphics.RectF; 93 import android.graphics.Typeface; 94 import android.graphics.drawable.Drawable; 95 import android.os.Build; 96 import android.os.Bundle; 97 import android.os.SystemClock; 98 import android.os.UserHandle; 99 import android.os.VibrationEffect; 100 import android.text.Layout; 101 import android.text.StaticLayout; 102 import android.text.TextPaint; 103 import android.util.AttributeSet; 104 import android.util.FloatProperty; 105 import android.util.Log; 106 import android.util.Pair; 107 import android.util.SparseBooleanArray; 108 import android.view.HapticFeedbackConstants; 109 import android.view.KeyEvent; 110 import android.view.LayoutInflater; 111 import android.view.MotionEvent; 112 import android.view.RemoteAnimationTarget; 113 import android.view.View; 114 import android.view.ViewDebug; 115 import android.view.ViewGroup; 116 import android.view.ViewTreeObserver.OnScrollChangedListener; 117 import android.view.accessibility.AccessibilityEvent; 118 import android.view.accessibility.AccessibilityNodeInfo; 119 import android.view.animation.Interpolator; 120 import android.widget.ListView; 121 import android.widget.OverScroller; 122 import android.widget.Toast; 123 import android.window.PictureInPictureSurfaceTransaction; 124 125 import androidx.annotation.NonNull; 126 import androidx.annotation.Nullable; 127 import androidx.annotation.UiThread; 128 import androidx.core.graphics.ColorUtils; 129 130 import com.android.launcher3.BaseActivity; 131 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener; 132 import com.android.launcher3.DeviceProfile; 133 import com.android.launcher3.Insettable; 134 import com.android.launcher3.InvariantDeviceProfile; 135 import com.android.launcher3.PagedView; 136 import com.android.launcher3.R; 137 import com.android.launcher3.Utilities; 138 import com.android.launcher3.anim.AnimatedFloat; 139 import com.android.launcher3.anim.AnimatorListeners; 140 import com.android.launcher3.anim.AnimatorPlaybackController; 141 import com.android.launcher3.anim.PendingAnimation; 142 import com.android.launcher3.anim.SpringProperty; 143 import com.android.launcher3.compat.AccessibilityManagerCompat; 144 import com.android.launcher3.config.FeatureFlags; 145 import com.android.launcher3.icons.cache.HandlerRunnable; 146 import com.android.launcher3.logging.StatsLogManager; 147 import com.android.launcher3.statehandlers.DepthController; 148 import com.android.launcher3.statemanager.BaseState; 149 import com.android.launcher3.statemanager.StatefulActivity; 150 import com.android.launcher3.testing.TestLogging; 151 import com.android.launcher3.testing.shared.TestProtocol; 152 import com.android.launcher3.touch.OverScroll; 153 import com.android.launcher3.touch.PagedOrientationHandler; 154 import com.android.launcher3.util.DynamicResource; 155 import com.android.launcher3.util.IntArray; 156 import com.android.launcher3.util.IntSet; 157 import com.android.launcher3.util.ResourceBasedOverride.Overrides; 158 import com.android.launcher3.util.RunnableList; 159 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; 160 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource; 161 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; 162 import com.android.launcher3.util.Themes; 163 import com.android.launcher3.util.TranslateEdgeEffect; 164 import com.android.launcher3.util.VibratorWrapper; 165 import com.android.launcher3.util.ViewPool; 166 import com.android.quickstep.BaseActivityInterface; 167 import com.android.quickstep.GestureState; 168 import com.android.quickstep.RecentsAnimationController; 169 import com.android.quickstep.RecentsAnimationTargets; 170 import com.android.quickstep.RecentsFilterState; 171 import com.android.quickstep.RecentsModel; 172 import com.android.quickstep.RemoteAnimationTargets; 173 import com.android.quickstep.RemoteTargetGluer; 174 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; 175 import com.android.quickstep.RotationTouchHelper; 176 import com.android.quickstep.SystemUiProxy; 177 import com.android.quickstep.TaskOverlayFactory; 178 import com.android.quickstep.TaskThumbnailCache; 179 import com.android.quickstep.TaskViewUtils; 180 import com.android.quickstep.TopTaskTracker; 181 import com.android.quickstep.ViewUtils; 182 import com.android.quickstep.util.ActiveGestureErrorDetector; 183 import com.android.quickstep.util.ActiveGestureLog; 184 import com.android.quickstep.util.AnimUtils; 185 import com.android.quickstep.util.DesktopTask; 186 import com.android.quickstep.util.GroupTask; 187 import com.android.quickstep.util.LayoutUtils; 188 import com.android.quickstep.util.RecentsOrientedState; 189 import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps; 190 import com.android.quickstep.util.SplitAnimationTimings; 191 import com.android.quickstep.util.SplitSelectStateController; 192 import com.android.quickstep.util.SurfaceTransaction; 193 import com.android.quickstep.util.SurfaceTransactionApplier; 194 import com.android.quickstep.util.TaskViewSimulator; 195 import com.android.quickstep.util.TaskVisualsChangeListener; 196 import com.android.quickstep.util.TransformParams; 197 import com.android.quickstep.util.VibrationConstants; 198 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; 199 import com.android.systemui.plugins.ResourceProvider; 200 import com.android.systemui.shared.recents.model.Task; 201 import com.android.systemui.shared.recents.model.ThumbnailData; 202 import com.android.systemui.shared.system.ActivityManagerWrapper; 203 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 204 import com.android.systemui.shared.system.PackageManagerWrapper; 205 import com.android.systemui.shared.system.TaskStackChangeListener; 206 import com.android.systemui.shared.system.TaskStackChangeListeners; 207 import com.android.wm.shell.pip.IPipAnimationListener; 208 209 import java.util.ArrayList; 210 import java.util.Arrays; 211 import java.util.HashMap; 212 import java.util.List; 213 import java.util.Objects; 214 import java.util.function.Consumer; 215 216 /** 217 * A list of recent tasks. 218 */ 219 @TargetApi(Build.VERSION_CODES.R) 220 public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>, 221 STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable, 222 TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback, 223 TaskVisualsChangeListener { 224 225 private static final String TAG = "RecentsView"; 226 private static final boolean DEBUG = false; 227 228 // TODO(b/184899234): We use this timeout to wait a fixed period after switching to the 229 // screenshot when dismissing the current live task to ensure the app can try and get stopped. 230 private static final int REMOVE_TASK_WAIT_FOR_APP_STOP_MS = 100; 231 232 public static final FloatProperty<RecentsView> CONTENT_ALPHA = 233 new FloatProperty<RecentsView>("contentAlpha") { 234 @Override 235 public void setValue(RecentsView view, float v) { 236 view.setContentAlpha(v); 237 } 238 239 @Override 240 public Float get(RecentsView view) { 241 return view.getContentAlpha(); 242 } 243 }; 244 245 public static final FloatProperty<RecentsView> FULLSCREEN_PROGRESS = 246 new FloatProperty<RecentsView>("fullscreenProgress") { 247 @Override 248 public void setValue(RecentsView recentsView, float v) { 249 recentsView.setFullscreenProgress(v); 250 } 251 252 @Override 253 public Float get(RecentsView recentsView) { 254 return recentsView.mFullscreenProgress; 255 } 256 }; 257 258 public static final FloatProperty<RecentsView> TASK_MODALNESS = 259 new FloatProperty<RecentsView>("taskModalness") { 260 @Override 261 public void setValue(RecentsView recentsView, float v) { 262 recentsView.setTaskModalness(v); 263 } 264 265 @Override 266 public Float get(RecentsView recentsView) { 267 return recentsView.mTaskModalness; 268 } 269 }; 270 271 public static final FloatProperty<RecentsView> ADJACENT_PAGE_HORIZONTAL_OFFSET = 272 new FloatProperty<RecentsView>("adjacentPageHorizontalOffset") { 273 @Override 274 public void setValue(RecentsView recentsView, float v) { 275 if (recentsView.mAdjacentPageHorizontalOffset != v) { 276 recentsView.mAdjacentPageHorizontalOffset = v; 277 recentsView.updatePageOffsets(); 278 } 279 } 280 281 @Override 282 public Float get(RecentsView recentsView) { 283 return recentsView.mAdjacentPageHorizontalOffset; 284 } 285 }; 286 287 public static final int SCROLL_VIBRATION_PRIMITIVE = 288 Utilities.ATLEAST_S ? VibrationEffect.Composition.PRIMITIVE_LOW_TICK : -1; 289 public static final float SCROLL_VIBRATION_PRIMITIVE_SCALE = 0.6f; 290 public static final VibrationEffect SCROLL_VIBRATION_FALLBACK = 291 VibrationConstants.EFFECT_TEXTURE_TICK; 292 293 /** 294 * Can be used to tint the color of the RecentsView to simulate a scrim that can views 295 * excluded from. Really should be a proper scrim. 296 * TODO(b/187528071): Remove this and replace with a real scrim. 297 */ 298 private static final FloatProperty<RecentsView> COLOR_TINT = 299 new FloatProperty<RecentsView>("colorTint") { 300 @Override 301 public void setValue(RecentsView recentsView, float v) { 302 recentsView.setColorTint(v); 303 } 304 305 @Override 306 public Float get(RecentsView recentsView) { 307 return recentsView.getColorTint(); 308 } 309 }; 310 311 /** 312 * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they 313 * are currently both used to apply secondary translation. Should their use cases change to be 314 * more specific, we'd want to create a similar FloatProperty just for a TaskView's 315 * offsetX/Y property 316 */ 317 public static final FloatProperty<RecentsView> TASK_SECONDARY_TRANSLATION = 318 new FloatProperty<RecentsView>("taskSecondaryTranslation") { 319 @Override 320 public void setValue(RecentsView recentsView, float v) { 321 recentsView.setTaskViewsResistanceTranslation(v); 322 } 323 324 @Override 325 public Float get(RecentsView recentsView) { 326 return recentsView.mTaskViewsSecondaryTranslation; 327 } 328 }; 329 330 /** 331 * Even though {@link TaskView} has distinct offsetTranslationX/Y and resistance property, they 332 * are currently both used to apply secondary translation. Should their use cases change to be 333 * more specific, we'd want to create a similar FloatProperty just for a TaskView's 334 * offsetX/Y property 335 */ 336 public static final FloatProperty<RecentsView> TASK_PRIMARY_SPLIT_TRANSLATION = 337 new FloatProperty<RecentsView>("taskPrimarySplitTranslation") { 338 @Override 339 public void setValue(RecentsView recentsView, float v) { 340 recentsView.setTaskViewsPrimarySplitTranslation(v); 341 } 342 343 @Override 344 public Float get(RecentsView recentsView) { 345 return recentsView.mTaskViewsPrimarySplitTranslation; 346 } 347 }; 348 349 public static final FloatProperty<RecentsView> TASK_SECONDARY_SPLIT_TRANSLATION = 350 new FloatProperty<RecentsView>("taskSecondarySplitTranslation") { 351 @Override 352 public void setValue(RecentsView recentsView, float v) { 353 recentsView.setTaskViewsSecondarySplitTranslation(v); 354 } 355 356 @Override 357 public Float get(RecentsView recentsView) { 358 return recentsView.mTaskViewsSecondarySplitTranslation; 359 } 360 }; 361 362 /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */ 363 public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY = 364 new FloatProperty<RecentsView>("recentsScale") { 365 @Override 366 public void setValue(RecentsView view, float scale) { 367 view.setScaleX(scale); 368 view.setScaleY(scale); 369 view.mLastComputedTaskStartPushOutDistance = null; 370 view.mLastComputedTaskEndPushOutDistance = null; 371 view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() { 372 @Override 373 public void accept(RemoteTargetHandle remoteTargetHandle) { 374 remoteTargetHandle.getTaskViewSimulator().recentsViewScale.value = 375 scale; 376 } 377 }); 378 view.setTaskViewsResistanceTranslation(view.mTaskViewsSecondaryTranslation); 379 view.updateTaskViewsSnapshotRadius(); 380 view.updatePageOffsets(); 381 } 382 383 @Override 384 public Float get(RecentsView view) { 385 return view.getScaleX(); 386 } 387 }; 388 389 /** 390 * Progress of Recents view from carousel layout to grid layout. If Recents is not shown as a 391 * grid, then the value remains 0. 392 */ 393 public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS = 394 new FloatProperty<RecentsView>("recentsGrid") { 395 @Override 396 public void setValue(RecentsView view, float gridProgress) { 397 view.setGridProgress(gridProgress); 398 } 399 400 @Override 401 public Float get(RecentsView view) { 402 return view.mGridProgress; 403 } 404 }; 405 406 /** 407 * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and 408 * being in any other state has a value of 0. 409 */ 410 public static final FloatProperty<RecentsView> TASK_THUMBNAIL_SPLASH_ALPHA = 411 new FloatProperty<RecentsView>("taskThumbnailSplashAlpha") { 412 @Override 413 public void setValue(RecentsView view, float taskThumbnailSplashAlpha) { 414 view.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha); 415 } 416 417 @Override 418 public Float get(RecentsView view) { 419 return view.mTaskThumbnailSplashAlpha; 420 } 421 }; 422 423 // OverScroll constants 424 private static final int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270; 425 426 private static final int DEFAULT_ACTIONS_VIEW_ALPHA_ANIMATION_DURATION = 300; 427 428 private static final int DISMISS_TASK_DURATION = 300; 429 private static final int ADDITION_TASK_DURATION = 200; 430 private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f; 431 private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f; 432 private static final float ANIMATION_DISMISS_PROGRESS_MIDPOINT = 0.5f; 433 private static final float END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.75f; 434 435 private static final float SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE = 0.15f; 436 437 protected final RecentsOrientedState mOrientationState; 438 protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy; 439 @Nullable 440 protected RecentsAnimationController mRecentsAnimationController; 441 @Nullable 442 protected SurfaceTransactionApplier mSyncTransactionApplier; 443 protected int mTaskWidth; 444 protected int mTaskHeight; 445 // Used to position the top of a task in the top row of the grid 446 private float mTaskGridVerticalDiff; 447 // The vertical space one grid task takes + space between top and bottom row. 448 private float mTopBottomRowHeightDiff; 449 // mTaskGridVerticalDiff and mTopBottomRowHeightDiff summed together provides the top 450 // position for bottom row of grid tasks. 451 452 @Nullable 453 protected RemoteTargetHandle[] mRemoteTargetHandles; 454 protected final Rect mLastComputedTaskSize = new Rect(); 455 protected final Rect mLastComputedGridSize = new Rect(); 456 protected final Rect mLastComputedGridTaskSize = new Rect(); 457 protected final Rect mLastComputedDesktopTaskSize = new Rect(); 458 private TaskView mSelectedTask = null; 459 // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot. 460 @Nullable 461 protected Float mLastComputedTaskStartPushOutDistance = null; 462 @Nullable 463 protected Float mLastComputedTaskEndPushOutDistance = null; 464 protected boolean mEnableDrawingLiveTile = false; 465 protected final Rect mTempRect = new Rect(); 466 protected final RectF mTempRectF = new RectF(); 467 private final PointF mTempPointF = new PointF(); 468 private final Matrix mTempMatrix = new Matrix(); 469 private final float[] mTempFloat = new float[1]; 470 private final List<OnScrollChangedListener> mScrollListeners = new ArrayList<>(); 471 472 // The threshold at which we update the SystemUI flags when animating from the task into the app 473 public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f; 474 475 protected final ACTIVITY_TYPE mActivity; 476 private final float mFastFlingVelocity; 477 private final int mScrollHapticMinGapMillis; 478 private final RecentsModel mModel; 479 private final int mSplitPlaceholderSize; 480 private final int mSplitPlaceholderInset; 481 private final ClearAllButton mClearAllButton; 482 private final Rect mClearAllButtonDeadZoneRect = new Rect(); 483 private final Rect mTaskViewDeadZoneRect = new Rect(); 484 /** 485 * Reflects if Recents is currently in the middle of a gesture 486 */ 487 private boolean mGestureActive; 488 489 // Keeps track of the previously known visible tasks for purposes of loading/unloading task data 490 private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); 491 492 private final InvariantDeviceProfile mIdp; 493 494 /** 495 * Getting views should be done via {@link #getTaskViewFromPool(int)} 496 */ 497 private final ViewPool<TaskView> mTaskViewPool; 498 private final ViewPool<GroupedTaskView> mGroupedTaskViewPool; 499 private final ViewPool<DesktopTaskView> mDesktopTaskViewPool; 500 501 private final TaskOverlayFactory mTaskOverlayFactory; 502 503 protected boolean mDisallowScrollToClearAll; 504 private boolean mOverlayEnabled; 505 protected boolean mFreezeViewVisibility; 506 private boolean mOverviewGridEnabled; 507 private boolean mOverviewFullscreenEnabled; 508 private boolean mOverviewSelectEnabled; 509 510 private boolean mShouldClampScrollOffset; 511 private int mClampedScrollOffsetBound; 512 513 private float mAdjacentPageHorizontalOffset = 0; 514 protected float mTaskViewsSecondaryTranslation = 0; 515 protected float mTaskViewsPrimarySplitTranslation = 0; 516 protected float mTaskViewsSecondarySplitTranslation = 0; 517 // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid. 518 private float mGridProgress = 0; 519 private float mTaskThumbnailSplashAlpha = 0; 520 private boolean mShowAsGridLastOnLayout = false; 521 private final IntSet mTopRowIdSet = new IntSet(); 522 private int mClearAllShortTotalWidthTranslation = 0; 523 524 // The GestureEndTarget that is still in progress. 525 @Nullable 526 protected GestureState.GestureEndTarget mCurrentGestureEndTarget; 527 528 // TODO(b/187528071): Remove these and replace with a real scrim. 529 private float mColorTint; 530 private final int mTintingColor; 531 @Nullable 532 private ObjectAnimator mTintingAnimator; 533 534 private int mOverScrollShift = 0; 535 private long mScrollLastHapticTimestamp; 536 537 /** 538 * TODO: Call reloadIdNeeded in onTaskStackChanged. 539 */ 540 private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { 541 @Override 542 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { 543 if (!mHandleTaskStackChanges) { 544 return; 545 } 546 // Check this is for the right user 547 if (!checkCurrentOrManagedUserId(userId, getContext())) { 548 return; 549 } 550 551 // Remove the task immediately from the task list 552 TaskView taskView = getTaskViewByTaskId(taskId); 553 if (taskView != null) { 554 removeView(taskView); 555 } 556 } 557 558 @Override 559 public void onActivityUnpinned() { 560 if (!mHandleTaskStackChanges) { 561 return; 562 } 563 564 reloadIfNeeded(); 565 enableLayoutTransitions(); 566 } 567 568 @Override 569 public void onTaskRemoved(int taskId) { 570 if (!mHandleTaskStackChanges) { 571 return; 572 } 573 574 TaskView taskView = getTaskViewByTaskId(taskId); 575 if (taskView == null) { 576 return; 577 } 578 Task.TaskKey taskKey = taskView.getTask().key; 579 UI_HELPER_EXECUTOR.execute(new HandlerRunnable<>( 580 UI_HELPER_EXECUTOR.getHandler(), 581 () -> PackageManagerWrapper.getInstance() 582 .getActivityInfo(taskKey.getComponent(), taskKey.userId) == null, 583 MAIN_EXECUTOR, 584 apkRemoved -> { 585 if (apkRemoved) { 586 dismissTask(taskId); 587 } else { 588 mModel.isTaskRemoved(taskKey.id, taskRemoved -> { 589 if (taskRemoved) { 590 dismissTask(taskId); 591 } 592 }, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter())); 593 } 594 })); 595 } 596 }; 597 598 private final PinnedStackAnimationListener mIPipAnimationListener = 599 new PinnedStackAnimationListener(); 600 private int mPipCornerRadius; 601 private int mPipShadowRadius; 602 603 // Used to keep track of the last requested task list id, so that we do not request to load the 604 // tasks again if we have already requested it and the task list has not changed 605 private int mTaskListChangeId = -1; 606 607 // Only valid until the launcher state changes to NORMAL 608 /** 609 * ID for the current running TaskView view, unique amongst TaskView instances. ID's are set 610 * through {@link #getTaskViewFromPool(boolean)} and incremented by {@link #mTaskViewIdCount} 611 */ 612 protected int mRunningTaskViewId = -1; 613 private int mTaskViewIdCount; 614 private final int[] INVALID_TASK_IDS = new int[]{-1, -1}; 615 protected boolean mRunningTaskTileHidden; 616 @Nullable 617 private Task[] mTmpRunningTasks; 618 protected int mFocusedTaskViewId = -1; 619 620 private boolean mTaskIconScaledDown = false; 621 private boolean mRunningTaskShowScreenshot = false; 622 623 private boolean mOverviewStateEnabled; 624 private boolean mHandleTaskStackChanges; 625 private boolean mSwipeDownShouldLaunchApp; 626 private boolean mTouchDownToStartHome; 627 private final float mSquaredTouchSlop; 628 private int mDownX; 629 private int mDownY; 630 631 @Nullable 632 private PendingAnimation mPendingAnimation; 633 @Nullable 634 private LayoutTransition mLayoutTransition; 635 636 @ViewDebug.ExportedProperty(category = "launcher") 637 protected float mContentAlpha = 1; 638 @ViewDebug.ExportedProperty(category = "launcher") 639 protected float mFullscreenProgress = 0; 640 /** 641 * How modal is the current task to be displayed, 1 means the task is fully modal and no other 642 * tasks are show. 0 means the task is displays in context in the list with other tasks. 643 */ 644 @ViewDebug.ExportedProperty(category = "launcher") 645 protected float mTaskModalness = 0; 646 647 // Keeps track of task id whose visual state should not be reset 648 private int mIgnoreResetTaskId = -1; 649 protected boolean mLoadPlanEverApplied; 650 651 // Variables for empty state 652 private final Drawable mEmptyIcon; 653 private final CharSequence mEmptyMessage; 654 private final TextPaint mEmptyMessagePaint; 655 private final Point mLastMeasureSize = new Point(); 656 private final int mEmptyMessagePadding; 657 private boolean mShowEmptyMessage; 658 @Nullable 659 private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener; 660 @Nullable 661 private Layout mEmptyTextLayout; 662 663 /** 664 * Placeholder view indicating where the first split screen selected app will be placed 665 */ 666 private SplitSelectStateController mSplitSelectStateController; 667 /** 668 * The first task that split screen selection was initiated with. When split select state is 669 * initialized, we create a 670 * {@link #createTaskDismissAnimation(TaskView, boolean, boolean, long, boolean)} for this 671 * TaskView but don't actually remove the task since the user might back out. As such, we also 672 * ensure this View doesn't go back into the {@link #mTaskViewPool}, 673 * see {@link #onViewRemoved(View)} 674 */ 675 @Nullable 676 private TaskView mSplitHiddenTaskView; 677 @Nullable 678 private TaskView mSecondSplitHiddenView; 679 @Nullable 680 private SplitBounds mSplitBoundsConfig; 681 private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(), 682 R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT); 683 684 private SplitInstructionsView mSplitInstructionsView; 685 686 @Nullable 687 private SplitSelectSource mSplitSelectSource; 688 689 /** 690 * Keeps track of the index of the TaskView that split screen was initialized with so we know 691 * where to insert it back into list of taskViews in case user backs out of entering split 692 * screen. 693 * NOTE: This index is the index while {@link #mSplitHiddenTaskView} was a child of recentsView, 694 * this doesn't get adjusted to reflect the new child count after the taskView is dismissed/ 695 * removed from recentsView 696 */ 697 private int mSplitHiddenTaskViewIndex = -1; 698 @Nullable 699 private FloatingTaskView mFirstFloatingTaskView; 700 @Nullable 701 private FloatingTaskView mSecondFloatingTaskView; 702 703 /** 704 * The task to be removed and immediately re-added. Should not be added to task pool. 705 */ 706 @Nullable 707 private TaskView mMovingTaskView; 708 709 private OverviewActionsView mActionsView; 710 private ObjectAnimator mActionsViewAlphaAnimator; 711 private float mActionsViewAlphaAnimatorFinalValue; 712 713 /** 714 * Keeps track of the desktop task. Optional and only present when the feature flag is enabled. 715 */ 716 @Nullable 717 private DesktopTaskView mDesktopTaskView; 718 719 private MultiWindowModeChangedListener mMultiWindowModeChangedListener = 720 new MultiWindowModeChangedListener() { 721 @Override 722 public void onMultiWindowModeChanged(boolean inMultiWindowMode) { 723 mOrientationState.setMultiWindowMode(inMultiWindowMode); 724 setLayoutRotation(mOrientationState.getTouchRotation(), 725 mOrientationState.getDisplayRotation()); 726 updateChildTaskOrientations(); 727 if (!inMultiWindowMode && mOverviewStateEnabled) { 728 // TODO: Re-enable layout transitions for addition of the unpinned task 729 reloadIfNeeded(); 730 } 731 } 732 }; 733 734 @Nullable 735 private RunnableList mSideTaskLaunchCallback; 736 @Nullable 737 private TaskLaunchListener mTaskLaunchListener; 738 739 // keeps track of the state of the filter for tasks in recents view 740 private final RecentsFilterState mFilterState = new RecentsFilterState(); 741 RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, BaseActivityInterface sizeStrategy)742 public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, 743 BaseActivityInterface sizeStrategy) { 744 super(context, attrs, defStyleAttr); 745 setEnableFreeScroll(true); 746 mSizeStrategy = sizeStrategy; 747 mActivity = BaseActivity.fromContext(context); 748 mOrientationState = new RecentsOrientedState( 749 context, mSizeStrategy, this::animateRecentsRotationInPlace); 750 final int rotation = mActivity.getDisplay().getRotation(); 751 mOrientationState.setRecentsRotation(rotation); 752 753 mScrollHapticMinGapMillis = getResources() 754 .getInteger(R.integer.recentsScrollHapticMinGapMillis); 755 mFastFlingVelocity = getResources() 756 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); 757 mModel = RecentsModel.INSTANCE.get(context); 758 mIdp = InvariantDeviceProfile.INSTANCE.get(context); 759 760 mClearAllButton = (ClearAllButton) LayoutInflater.from(context) 761 .inflate(R.layout.overview_clear_all_button, this, false); 762 mClearAllButton.setOnClickListener(this::dismissAllTasks); 763 mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */, 764 10 /* initial size */); 765 mGroupedTaskViewPool = new ViewPool<>(context, this, 766 R.layout.task_grouped, 20 /* max size */, 10 /* initial size */); 767 mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop, 768 5 /* max size */, 1 /* initial size */); 769 770 mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources()); 771 setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); 772 mSplitPlaceholderSize = getResources().getDimensionPixelSize( 773 R.dimen.split_placeholder_size); 774 mSplitPlaceholderInset = getResources().getDimensionPixelSize( 775 R.dimen.split_placeholder_inset); 776 mSquaredTouchSlop = squaredTouchSlop(context); 777 mClampedScrollOffsetBound = getResources().getDimensionPixelSize( 778 R.dimen.transient_taskbar_clamped_offset_bound); 779 780 mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); 781 mEmptyIcon.setCallback(this); 782 mEmptyMessage = context.getText(R.string.recents_empty_message); 783 mEmptyMessagePaint = new TextPaint(); 784 mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); 785 mEmptyMessagePaint.setTextSize(getResources() 786 .getDimension(R.dimen.recents_empty_message_text_size)); 787 mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context), 788 Typeface.NORMAL)); 789 mEmptyMessagePaint.setAntiAlias(true); 790 mEmptyMessagePadding = getResources() 791 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); 792 setWillNotDraw(false); 793 updateEmptyMessage(); 794 mOrientationHandler = mOrientationState.getOrientationHandler(); 795 796 mTaskOverlayFactory = Overrides.getObject( 797 TaskOverlayFactory.class, 798 context.getApplicationContext(), 799 R.string.task_overlay_factory_class); 800 801 // Initialize quickstep specific cache params here, as this is constructed only once 802 mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5); 803 804 mTintingColor = getForegroundScrimDimColor(context); 805 806 // if multi-instance feature is enabled 807 if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) { 808 // invalidate the current list of tasks if filter changes 809 mFilterState.setOnFilterUpdatedListener(this::invalidateTaskList); 810 } 811 // make sure filter is turned off by default 812 mFilterState.setFilterBy(null); 813 } 814 815 /** Get the state of the filter */ getFilterState()816 public RecentsFilterState getFilterState() { 817 return mFilterState; 818 } 819 820 /** 821 * Toggles the filter and reloads the recents view if needed. 822 * 823 * @param packageName package name to filter by if the filter is being turned on; 824 * should be null if filter is being turned off 825 */ setAndApplyFilter(@ullable String packageName)826 public void setAndApplyFilter(@Nullable String packageName) { 827 mFilterState.setFilterBy(packageName); 828 updateClearAllFunction(); 829 reloadIfNeeded(); 830 } 831 832 /** 833 * Updates the "Clear All" button and its function depending on the recents view state. 834 * 835 * TODO: add a different button for going back to overview. Present solution is for demo only. 836 */ updateClearAllFunction()837 public void updateClearAllFunction() { 838 if (mFilterState.isFiltered()) { 839 mClearAllButton.setText(R.string.recents_back); 840 mClearAllButton.setOnClickListener((view) -> { 841 this.setAndApplyFilter(null); 842 }); 843 } else { 844 mClearAllButton.setText(R.string.recents_clear_all); 845 mClearAllButton.setOnClickListener(this::dismissAllTasks); 846 } 847 } 848 849 /** 850 * Invalidates the list of tasks so that an update occurs to the list of tasks if requested. 851 */ invalidateTaskList()852 private void invalidateTaskList() { 853 mTaskListChangeId = -1; 854 } 855 getScroller()856 public OverScroller getScroller() { 857 return mScroller; 858 } 859 isRtl()860 public boolean isRtl() { 861 return mIsRtl; 862 } 863 864 @Override initEdgeEffect()865 protected void initEdgeEffect() { 866 mEdgeGlowLeft = new TranslateEdgeEffect(getContext()); 867 mEdgeGlowRight = new TranslateEdgeEffect(getContext()); 868 } 869 870 @Override drawEdgeEffect(Canvas canvas)871 protected void drawEdgeEffect(Canvas canvas) { 872 // Do not draw edge effect 873 } 874 875 @Override dispatchDraw(Canvas canvas)876 protected void dispatchDraw(Canvas canvas) { 877 // Draw overscroll 878 if (mAllowOverScroll && (!mEdgeGlowRight.isFinished() || !mEdgeGlowLeft.isFinished())) { 879 final int restoreCount = canvas.save(); 880 881 int primarySize = mOrientationHandler.getPrimaryValue(getWidth(), getHeight()); 882 int scroll = OverScroll.dampedScroll(getUndampedOverScrollShift(), primarySize); 883 mOrientationHandler.setPrimary(canvas, CANVAS_TRANSLATE, scroll); 884 885 if (mOverScrollShift != scroll) { 886 mOverScrollShift = scroll; 887 dispatchScrollChanged(); 888 } 889 890 super.dispatchDraw(canvas); 891 canvas.restoreToCount(restoreCount); 892 } else { 893 if (mOverScrollShift != 0) { 894 mOverScrollShift = 0; 895 dispatchScrollChanged(); 896 } 897 super.dispatchDraw(canvas); 898 } 899 if (mEnableDrawingLiveTile && mRemoteTargetHandles != null) { 900 redrawLiveTile(); 901 } 902 } 903 getUndampedOverScrollShift()904 private float getUndampedOverScrollShift() { 905 final int width = getWidth(); 906 final int height = getHeight(); 907 int primarySize = mOrientationHandler.getPrimaryValue(width, height); 908 int secondarySize = mOrientationHandler.getSecondaryValue(width, height); 909 910 float effectiveShift = 0; 911 if (!mEdgeGlowLeft.isFinished()) { 912 mEdgeGlowLeft.setSize(secondarySize, primarySize); 913 if (((TranslateEdgeEffect) mEdgeGlowLeft).getTranslationShift(mTempFloat)) { 914 effectiveShift = mTempFloat[0]; 915 postInvalidateOnAnimation(); 916 } 917 } 918 if (!mEdgeGlowRight.isFinished()) { 919 mEdgeGlowRight.setSize(secondarySize, primarySize); 920 if (((TranslateEdgeEffect) mEdgeGlowRight).getTranslationShift(mTempFloat)) { 921 effectiveShift -= mTempFloat[0]; 922 postInvalidateOnAnimation(); 923 } 924 } 925 926 return effectiveShift * primarySize; 927 } 928 929 /** 930 * Returns the view shift due to overscroll 931 */ getOverScrollShift()932 public int getOverScrollShift() { 933 return mOverScrollShift; 934 } 935 936 @Override 937 @Nullable onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData)938 public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) { 939 if (mHandleTaskStackChanges) { 940 TaskView taskView = getTaskViewByTaskId(taskId); 941 if (taskView != null) { 942 for (TaskIdAttributeContainer container : 943 taskView.getTaskIdAttributeContainers()) { 944 if (container == null || taskId != container.getTask().key.id) { 945 continue; 946 } 947 container.getThumbnailView().setThumbnail(container.getTask(), thumbnailData); 948 } 949 } 950 } 951 return null; 952 } 953 954 @Override onTaskIconChanged(String pkg, UserHandle user)955 public void onTaskIconChanged(String pkg, UserHandle user) { 956 for (int i = 0; i < getTaskViewCount(); i++) { 957 TaskView tv = requireTaskViewAt(i); 958 Task task = tv.getTask(); 959 if (task != null && task.key != null && pkg.equals(task.key.getPackageName()) 960 && task.key.userId == user.getIdentifier()) { 961 task.icon = null; 962 if (tv.getIconView().getDrawable() != null) { 963 tv.onTaskListVisibilityChanged(true /* visible */); 964 } 965 } 966 } 967 } 968 969 @Override onTaskIconChanged(int taskId)970 public void onTaskIconChanged(int taskId) { 971 TaskView taskView = getTaskViewByTaskId(taskId); 972 if (taskView != null) { 973 taskView.refreshTaskThumbnailSplash(); 974 } 975 } 976 977 /** 978 * Update the thumbnail of the task. 979 * @param refreshNow Refresh immediately if it's true. 980 */ 981 @Nullable updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow)982 public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData, boolean refreshNow) { 983 TaskView taskView = getTaskViewByTaskId(taskId); 984 if (taskView != null) { 985 taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData, refreshNow); 986 } 987 return taskView; 988 } 989 990 @Override onWindowVisibilityChanged(int visibility)991 protected void onWindowVisibilityChanged(int visibility) { 992 super.onWindowVisibilityChanged(visibility); 993 updateTaskStackListenerState(); 994 } 995 init(OverviewActionsView actionsView, SplitSelectStateController splitController)996 public void init(OverviewActionsView actionsView, SplitSelectStateController splitController) { 997 mActionsView = actionsView; 998 mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0); 999 mSplitSelectStateController = splitController; 1000 } 1001 getSplitSelectController()1002 public SplitSelectStateController getSplitSelectController() { 1003 return mSplitSelectStateController; 1004 } 1005 isSplitSelectionActive()1006 public boolean isSplitSelectionActive() { 1007 return mSplitSelectStateController.isSplitSelectActive(); 1008 } 1009 1010 /** 1011 * See overridden implementations 1012 * @return {@code true} if child TaskViews can be launched when user taps on them 1013 */ canLaunchFullscreenTask()1014 protected boolean canLaunchFullscreenTask() { 1015 return true; 1016 } 1017 1018 @Override onAttachedToWindow()1019 protected void onAttachedToWindow() { 1020 super.onAttachedToWindow(); 1021 updateTaskStackListenerState(); 1022 mModel.getThumbnailCache().getHighResLoadingState().addCallback(this); 1023 mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); 1024 TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); 1025 mSyncTransactionApplier = new SurfaceTransactionApplier(this); 1026 runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams() 1027 .setSyncTransactionApplier(mSyncTransactionApplier)); 1028 RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this); 1029 mIPipAnimationListener.setActivityAndRecentsView(mActivity, this); 1030 SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener( 1031 mIPipAnimationListener); 1032 mOrientationState.initListeners(); 1033 mTaskOverlayFactory.initListeners(); 1034 } 1035 1036 @Override onDetachedFromWindow()1037 protected void onDetachedFromWindow() { 1038 super.onDetachedFromWindow(); 1039 updateTaskStackListenerState(); 1040 mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this); 1041 mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); 1042 TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); 1043 mSyncTransactionApplier = null; 1044 runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams() 1045 .setSyncTransactionApplier(null)); 1046 executeSideTaskLaunchCallback(); 1047 RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this); 1048 SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null); 1049 mIPipAnimationListener.setActivityAndRecentsView(null, null); 1050 mOrientationState.destroyListeners(); 1051 mTaskOverlayFactory.removeListeners(); 1052 } 1053 1054 @Override onViewRemoved(View child)1055 public void onViewRemoved(View child) { 1056 super.onViewRemoved(child); 1057 1058 // Clear the task data for the removed child if it was visible unless: 1059 // - It's the initial taskview for entering split screen, we only pretend to dismiss the 1060 // task 1061 // - It's the focused task to be moved to the front, we immediately re-add the task 1062 if (child instanceof TaskView && child != mSplitHiddenTaskView 1063 && child != mMovingTaskView) { 1064 TaskView taskView = (TaskView) child; 1065 for (int i : taskView.getTaskIds()) { 1066 mHasVisibleTaskData.delete(i); 1067 } 1068 if (child instanceof GroupedTaskView) { 1069 mGroupedTaskViewPool.recycle((GroupedTaskView) taskView); 1070 } else if (child instanceof DesktopTaskView) { 1071 mDesktopTaskViewPool.recycle((DesktopTaskView) taskView); 1072 } else { 1073 mTaskViewPool.recycle(taskView); 1074 } 1075 taskView.setTaskViewId(-1); 1076 mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0); 1077 } 1078 } 1079 1080 @Override onViewAdded(View child)1081 public void onViewAdded(View child) { 1082 super.onViewAdded(child); 1083 child.setAlpha(mContentAlpha); 1084 // RecentsView is set to RTL in the constructor when system is using LTR. Here we set the 1085 // child direction back to match system settings. 1086 child.setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_LTR : View.LAYOUT_DIRECTION_RTL); 1087 mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, false); 1088 updateEmptyMessage(); 1089 } 1090 1091 @Override draw(Canvas canvas)1092 public void draw(Canvas canvas) { 1093 maybeDrawEmptyMessage(canvas); 1094 super.draw(canvas); 1095 } 1096 1097 @Override requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1098 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 1099 if (isModal()) { 1100 // Do not scroll when clicking on a modal grid task, as it will already be centered 1101 // on screen. 1102 return false; 1103 } 1104 return super.requestChildRectangleOnScreen(child, rectangle, immediate); 1105 } 1106 addSideTaskLaunchCallback(RunnableList callback)1107 public void addSideTaskLaunchCallback(RunnableList callback) { 1108 if (mSideTaskLaunchCallback == null) { 1109 mSideTaskLaunchCallback = new RunnableList(); 1110 } 1111 mSideTaskLaunchCallback.add(callback::executeAllAndDestroy); 1112 } 1113 1114 /** 1115 * This is a one-time callback when touching in live tile mode. It's reset to null right 1116 * after it's called. 1117 */ setTaskLaunchListener(TaskLaunchListener taskLaunchListener)1118 public void setTaskLaunchListener(TaskLaunchListener taskLaunchListener) { 1119 mTaskLaunchListener = taskLaunchListener; 1120 } 1121 onTaskLaunchedInLiveTileMode()1122 public void onTaskLaunchedInLiveTileMode() { 1123 if (mTaskLaunchListener != null) { 1124 mTaskLaunchListener.onTaskLaunched(); 1125 mTaskLaunchListener = null; 1126 } 1127 } 1128 executeSideTaskLaunchCallback()1129 private void executeSideTaskLaunchCallback() { 1130 if (mSideTaskLaunchCallback != null) { 1131 mSideTaskLaunchCallback.executeAllAndDestroy(); 1132 mSideTaskLaunchCallback = null; 1133 } 1134 } 1135 1136 /** 1137 * TODO(b/195675206) Check both taskIDs from runningTaskViewId 1138 * and launch if either of them is {@param taskId} 1139 */ launchSideTaskInLiveTileModeForRestartedApp(int taskId)1140 public void launchSideTaskInLiveTileModeForRestartedApp(int taskId) { 1141 int runningTaskViewId = getTaskViewIdFromTaskId(taskId); 1142 if (mRunningTaskViewId == -1 || 1143 mRunningTaskViewId != runningTaskViewId || 1144 mRemoteTargetHandles == null) { 1145 return; 1146 } 1147 1148 TransformParams params = mRemoteTargetHandles[0].getTransformParams(); 1149 RemoteAnimationTargets targets = params.getTargetSet(); 1150 if (targets != null && targets.findTask(taskId) != null) { 1151 launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers, 1152 targets.nonApps); 1153 } 1154 } 1155 launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps)1156 public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTarget[] apps, 1157 RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps) { 1158 AnimatorSet anim = new AnimatorSet(); 1159 TaskView taskView = getTaskViewByTaskId(taskId); 1160 if (taskView == null || !isTaskViewVisible(taskView)) { 1161 // TODO: Refine this animation. 1162 SurfaceTransactionApplier surfaceApplier = 1163 new SurfaceTransactionApplier(mActivity.getDragLayer()); 1164 ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); 1165 appAnimator.setDuration(RECENTS_LAUNCH_DURATION); 1166 appAnimator.setInterpolator(ACCEL_DEACCEL); 1167 appAnimator.addUpdateListener(valueAnimator -> { 1168 float percent = valueAnimator.getAnimatedFraction(); 1169 SurfaceTransaction transaction = new SurfaceTransaction(); 1170 Matrix matrix = new Matrix(); 1171 matrix.postScale(percent, percent); 1172 matrix.postTranslate(mActivity.getDeviceProfile().widthPx * (1 - percent) / 2, 1173 mActivity.getDeviceProfile().heightPx * (1 - percent) / 2); 1174 transaction.forSurface(apps[apps.length - 1].leash) 1175 .setAlpha(percent) 1176 .setMatrix(matrix); 1177 surfaceApplier.scheduleApply(transaction); 1178 }); 1179 anim.play(appAnimator); 1180 anim.addListener(new AnimatorListenerAdapter() { 1181 @Override 1182 public void onAnimationEnd(Animator animation) { 1183 finishRecentsAnimation(false /* toRecents */, null); 1184 } 1185 }); 1186 } else { 1187 TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps, 1188 true /* launcherClosing */, mActivity.getStateManager(), this, 1189 getDepthController()); 1190 } 1191 anim.start(); 1192 } 1193 isTaskViewVisible(TaskView tv)1194 public boolean isTaskViewVisible(TaskView tv) { 1195 if (showAsGrid()) { 1196 int screenStart = mOrientationHandler.getPrimaryScroll(this); 1197 int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this); 1198 return isTaskViewWithinBounds(tv, screenStart, screenEnd); 1199 } else { 1200 // For now, just check if it's the active task or an adjacent task 1201 return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; 1202 } 1203 } 1204 isTaskViewFullyVisible(TaskView tv)1205 public boolean isTaskViewFullyVisible(TaskView tv) { 1206 if (showAsGrid()) { 1207 int screenStart = mOrientationHandler.getPrimaryScroll(this); 1208 int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this); 1209 return isTaskViewFullyWithinBounds(tv, screenStart, screenEnd); 1210 } else { 1211 // For now, just check if it's the active task 1212 return indexOfChild(tv) == getNextPage(); 1213 } 1214 } 1215 1216 @Nullable getLastGridTaskView()1217 private TaskView getLastGridTaskView() { 1218 return getLastGridTaskView(getTopRowIdArray(), getBottomRowIdArray()); 1219 } 1220 1221 @Nullable getLastGridTaskView(IntArray topRowIdArray, IntArray bottomRowIdArray)1222 private TaskView getLastGridTaskView(IntArray topRowIdArray, IntArray bottomRowIdArray) { 1223 if (topRowIdArray.isEmpty() && bottomRowIdArray.isEmpty()) { 1224 return null; 1225 } 1226 int lastTaskViewId = topRowIdArray.size() >= bottomRowIdArray.size() ? topRowIdArray.get( 1227 topRowIdArray.size() - 1) : bottomRowIdArray.get(bottomRowIdArray.size() - 1); 1228 return getTaskViewFromTaskViewId(lastTaskViewId); 1229 } 1230 getSnapToLastTaskScrollDiff()1231 private int getSnapToLastTaskScrollDiff() { 1232 // Snap to a position where ClearAll is just invisible. 1233 int screenStart = mOrientationHandler.getPrimaryScroll(this); 1234 int clearAllScroll = getScrollForPage(indexOfChild(mClearAllButton)); 1235 int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton); 1236 int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth); 1237 return screenStart - lastTaskScroll; 1238 } 1239 getLastTaskScroll(int clearAllScroll, int clearAllWidth)1240 private int getLastTaskScroll(int clearAllScroll, int clearAllWidth) { 1241 int distance = clearAllWidth + getClearAllExtraPageSpacing(); 1242 return clearAllScroll + (mIsRtl ? distance : -distance); 1243 } 1244 isTaskViewWithinBounds(TaskView tv, int start, int end)1245 private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) { 1246 int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment( 1247 showAsGrid()); 1248 int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment( 1249 showAsFullscreen())); 1250 int taskEnd = taskStart + taskSize; 1251 return (taskStart >= start && taskStart <= end) || (taskEnd >= start 1252 && taskEnd <= end); 1253 } 1254 isTaskViewFullyWithinBounds(TaskView tv, int start, int end)1255 private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) { 1256 int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment( 1257 showAsGrid()); 1258 int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment( 1259 showAsFullscreen())); 1260 int taskEnd = taskStart + taskSize; 1261 return taskStart >= start && taskEnd <= end; 1262 } 1263 1264 /** 1265 * Returns true if the task is in expected scroll position. 1266 * 1267 * @param taskIndex the index of the task 1268 */ isTaskInExpectedScrollPosition(int taskIndex)1269 public boolean isTaskInExpectedScrollPosition(int taskIndex) { 1270 return getScrollForPage(taskIndex) == getPagedOrientationHandler().getPrimaryScroll(this); 1271 } 1272 isFocusedTaskInExpectedScrollPosition()1273 private boolean isFocusedTaskInExpectedScrollPosition() { 1274 TaskView focusedTask = getFocusedTaskView(); 1275 return focusedTask != null && isTaskInExpectedScrollPosition(indexOfChild(focusedTask)); 1276 } 1277 1278 /** 1279 * Returns a {@link TaskView} that has taskId matching {@code taskId} or null if no match. 1280 */ 1281 @Nullable getTaskViewByTaskId(int taskId)1282 public TaskView getTaskViewByTaskId(int taskId) { 1283 if (taskId == INVALID_TASK_ID) { 1284 return null; 1285 } 1286 1287 for (int i = 0; i < getTaskViewCount(); i++) { 1288 TaskView taskView = requireTaskViewAt(i); 1289 if (taskView.containsTaskId(taskId)) { 1290 return taskView; 1291 } 1292 } 1293 return null; 1294 } 1295 setOverviewStateEnabled(boolean enabled)1296 public void setOverviewStateEnabled(boolean enabled) { 1297 mOverviewStateEnabled = enabled; 1298 updateTaskStackListenerState(); 1299 mOrientationState.setRotationWatcherEnabled(enabled); 1300 if (!enabled) { 1301 // Reset the running task when leaving overview since it can still have a reference to 1302 // its thumbnail 1303 mTmpRunningTasks = null; 1304 mSplitBoundsConfig = null; 1305 } 1306 updateLocusId(); 1307 } 1308 1309 /** 1310 * Whether the Clear All button is hidden or fully visible. Used to determine if center 1311 * displayed page is a task or the Clear All button. 1312 * 1313 * @return True = Clear All button not fully visible, center page is a task. False = Clear All 1314 * button fully visible, center page is Clear All button. 1315 */ isClearAllHidden()1316 public boolean isClearAllHidden() { 1317 return mClearAllButton.getAlpha() != 1f; 1318 } 1319 1320 @Override onPageBeginTransition()1321 protected void onPageBeginTransition() { 1322 super.onPageBeginTransition(); 1323 if (!mActivity.getDeviceProfile().isTablet) { 1324 mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, true); 1325 } 1326 if (mOverviewStateEnabled) { // only when in overview 1327 InteractionJankMonitorWrapper.begin(/* view= */ this, 1328 InteractionJankMonitorWrapper.CUJ_RECENTS_SCROLLING); 1329 } 1330 } 1331 1332 @Override onPageEndTransition()1333 protected void onPageEndTransition() { 1334 super.onPageEndTransition(); 1335 ActiveGestureLog.INSTANCE.addLog( 1336 "onPageEndTransition: current page index updated", getNextPage()); 1337 if (isClearAllHidden() && !mActivity.getDeviceProfile().isTablet) { 1338 mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false); 1339 } 1340 if (getNextPage() > 0) { 1341 setSwipeDownShouldLaunchApp(true); 1342 } 1343 InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_RECENTS_SCROLLING); 1344 } 1345 1346 @Override isSignificantMove(float absoluteDelta, int pageOrientedSize)1347 protected boolean isSignificantMove(float absoluteDelta, int pageOrientedSize) { 1348 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 1349 if (!deviceProfile.isTablet) { 1350 return super.isSignificantMove(absoluteDelta, pageOrientedSize); 1351 } 1352 1353 return absoluteDelta 1354 > deviceProfile.availableWidthPx * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE; 1355 } 1356 1357 @Override onTouchEvent(MotionEvent ev)1358 public boolean onTouchEvent(MotionEvent ev) { 1359 super.onTouchEvent(ev); 1360 1361 if (showAsGrid()) { 1362 int taskCount = getTaskViewCount(); 1363 for (int i = 0; i < taskCount; i++) { 1364 TaskView taskView = requireTaskViewAt(i); 1365 if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) { 1366 // Keep consuming events to pass to delegate 1367 return true; 1368 } 1369 } 1370 } else { 1371 TaskView taskView = getCurrentPageTaskView(); 1372 if (taskView != null && taskView.offerTouchToChildren(ev)) { 1373 // Keep consuming events to pass to delegate 1374 return true; 1375 } 1376 } 1377 1378 final int x = (int) ev.getX(); 1379 final int y = (int) ev.getY(); 1380 switch (ev.getAction()) { 1381 case MotionEvent.ACTION_UP: 1382 if (mTouchDownToStartHome) { 1383 startHome(); 1384 } 1385 mTouchDownToStartHome = false; 1386 break; 1387 case MotionEvent.ACTION_CANCEL: 1388 mTouchDownToStartHome = false; 1389 break; 1390 case MotionEvent.ACTION_MOVE: 1391 // Passing the touch slop will not allow dismiss to home 1392 if (mTouchDownToStartHome && 1393 (isHandlingTouch() || 1394 squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop)) { 1395 mTouchDownToStartHome = false; 1396 } 1397 break; 1398 case MotionEvent.ACTION_DOWN: 1399 // Touch down anywhere but the deadzone around the visible clear all button and 1400 // between the task views will start home on touch up 1401 if (!isHandlingTouch() && !isModal()) { 1402 if (mShowEmptyMessage) { 1403 mTouchDownToStartHome = true; 1404 } else { 1405 updateDeadZoneRects(); 1406 final boolean clearAllButtonDeadZoneConsumed = 1407 mClearAllButton.getAlpha() == 1 1408 && mClearAllButtonDeadZoneRect.contains(x, y); 1409 final boolean cameFromNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0; 1410 if (!clearAllButtonDeadZoneConsumed && !cameFromNavBar 1411 && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { 1412 mTouchDownToStartHome = true; 1413 } 1414 } 1415 } 1416 mDownX = x; 1417 mDownY = y; 1418 break; 1419 } 1420 1421 return isHandlingTouch(); 1422 } 1423 1424 @Override onNotSnappingToPageInFreeScroll()1425 protected void onNotSnappingToPageInFreeScroll() { 1426 int finalPos = mScroller.getFinalX(); 1427 if (finalPos > mMinScroll && finalPos < mMaxScroll) { 1428 int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1); 1429 int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0); 1430 1431 // If scrolling ends in the half of the added space that is closer to 1432 // the end, settle to the end. Otherwise snap to the nearest page. 1433 // If flinging past one of the ends, don't change the velocity as it 1434 // will get stopped at the end anyway. 1435 int pageSnapped = finalPos < (firstPageScroll + mMinScroll) / 2 1436 ? mMinScroll 1437 : finalPos > (lastPageScroll + mMaxScroll) / 2 1438 ? mMaxScroll 1439 : getScrollForPage(mNextPage); 1440 1441 if (showAsGrid()) { 1442 if (isSplitSelectionActive()) { 1443 return; 1444 } 1445 TaskView taskView = getTaskViewAt(mNextPage); 1446 // Snap to fully visible focused task and clear all button. 1447 boolean shouldSnapToFocusedTask = taskView != null && taskView.isFocusedTask() 1448 && isTaskViewFullyVisible(taskView); 1449 boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton); 1450 if (!shouldSnapToFocusedTask && !shouldSnapToClearAll) { 1451 return; 1452 } 1453 } 1454 1455 mScroller.setFinalX(pageSnapped); 1456 // Ensure the scroll/snap doesn't happen too fast; 1457 int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION 1458 - mScroller.getDuration(); 1459 if (extraScrollDuration > 0) { 1460 mScroller.extendDuration(extraScrollDuration); 1461 } 1462 } 1463 } 1464 1465 @Override onEdgeAbsorbingScroll()1466 protected void onEdgeAbsorbingScroll() { 1467 vibrateForScroll(); 1468 } 1469 1470 @Override onScrollOverPageChanged()1471 protected void onScrollOverPageChanged() { 1472 vibrateForScroll(); 1473 } 1474 vibrateForScroll()1475 private void vibrateForScroll() { 1476 long now = SystemClock.uptimeMillis(); 1477 if (now - mScrollLastHapticTimestamp > mScrollHapticMinGapMillis) { 1478 mScrollLastHapticTimestamp = now; 1479 VibratorWrapper.INSTANCE.get(mContext).vibrate(SCROLL_VIBRATION_PRIMITIVE, 1480 SCROLL_VIBRATION_PRIMITIVE_SCALE, SCROLL_VIBRATION_FALLBACK); 1481 } 1482 } 1483 1484 @Override determineScrollingStart(MotionEvent ev, float touchSlopScale)1485 protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { 1486 // Enables swiping to the left or right only if the task overlay is not modal. 1487 if (!isModal()) { 1488 super.determineScrollingStart(ev, touchSlopScale); 1489 } 1490 } 1491 1492 /** 1493 * Moves the running task to the front of the carousel in tablets, to minimize animation 1494 * required to move the running task in grid. 1495 */ moveRunningTaskToFront()1496 public void moveRunningTaskToFront() { 1497 if (!mActivity.getDeviceProfile().isTablet) { 1498 return; 1499 } 1500 1501 TaskView runningTaskView = getRunningTaskView(); 1502 if (runningTaskView == null) { 1503 return; 1504 } 1505 1506 if (indexOfChild(runningTaskView) != mCurrentPage) { 1507 return; 1508 } 1509 1510 if (mCurrentPage == 0) { 1511 return; 1512 } 1513 1514 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 1515 int currentPageScroll = getScrollForPage(mCurrentPage); 1516 mCurrentPageScrollDiff = primaryScroll - currentPageScroll; 1517 1518 mMovingTaskView = runningTaskView; 1519 removeView(runningTaskView); 1520 mMovingTaskView = null; 1521 runningTaskView.resetPersistentViewTransforms(); 1522 int frontTaskIndex = 0; 1523 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED && mDesktopTaskView != null 1524 && !runningTaskView.isDesktopTask()) { 1525 // If desktop mode is enabled, desktop task view is pinned at first position if present. 1526 // Move running task to position 1. 1527 frontTaskIndex = 1; 1528 } 1529 addView(runningTaskView, frontTaskIndex); 1530 setCurrentPage(frontTaskIndex); 1531 1532 updateTaskSize(); 1533 } 1534 1535 @Override onScrollerAnimationAborted()1536 protected void onScrollerAnimationAborted() { 1537 ActiveGestureLog.INSTANCE.addLog("scroller animation aborted", 1538 ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED); 1539 } 1540 1541 @Override isPageScrollsInitialized()1542 protected boolean isPageScrollsInitialized() { 1543 return super.isPageScrollsInitialized() && mLoadPlanEverApplied; 1544 } 1545 applyLoadPlan(ArrayList<GroupTask> taskGroups)1546 protected void applyLoadPlan(ArrayList<GroupTask> taskGroups) { 1547 if (mPendingAnimation != null) { 1548 mPendingAnimation.addEndListener(success -> applyLoadPlan(taskGroups)); 1549 return; 1550 } 1551 1552 mLoadPlanEverApplied = true; 1553 if (taskGroups == null || taskGroups.isEmpty()) { 1554 removeTasksViewsAndClearAllButton(); 1555 onTaskStackUpdated(); 1556 // With all tasks removed, touch handling in PagedView is disabled and we need to reset 1557 // touch state or otherwise values will be obsolete. 1558 resetTouchState(); 1559 if (isPageScrollsInitialized()) { 1560 onPageScrollsInitialized(); 1561 } 1562 return; 1563 } 1564 1565 int currentTaskId = INVALID_TASK_ID; 1566 TaskView currentTaskView = getTaskViewAt(mCurrentPage); 1567 if (currentTaskView != null && currentTaskView.getTask() != null) { 1568 currentTaskId = currentTaskView.getTask().key.id; 1569 } 1570 1571 // Unload existing visible task data 1572 unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); 1573 1574 TaskView ignoreResetTaskView = 1575 mIgnoreResetTaskId == INVALID_TASK_ID 1576 ? null : getTaskViewByTaskId(mIgnoreResetTaskId); 1577 1578 // Save running task ID if it exists before rebinding all taskViews, otherwise the task from 1579 // the runningTaskView currently bound could get assigned to another TaskView 1580 int runningTaskId = getTaskIdsForTaskViewId(mRunningTaskViewId)[0]; 1581 int focusedTaskId = getTaskIdsForTaskViewId(mFocusedTaskViewId)[0]; 1582 1583 // Removing views sets the currentPage to 0, so we save this and restore it after 1584 // the new set of views are added 1585 int previousCurrentPage = mCurrentPage; 1586 removeAllViews(); 1587 1588 // If we are entering Overview as a result of initiating a split from somewhere else 1589 // (e.g. split from Home), we need to make sure the staged app is not drawn as a thumbnail. 1590 int stagedTaskIdToBeRemovedFromGrid; 1591 if (mSplitSelectSource != null) { 1592 stagedTaskIdToBeRemovedFromGrid = mSplitSelectSource.alreadyRunningTaskId; 1593 updateCurrentTaskActionsVisibility(); 1594 } else { 1595 stagedTaskIdToBeRemovedFromGrid = INVALID_TASK_ID; 1596 } 1597 // update the map of instance counts 1598 mFilterState.updateInstanceCountMap(taskGroups); 1599 1600 // Clear out desktop view if it is set 1601 mDesktopTaskView = null; 1602 DesktopTask desktopTask = null; 1603 1604 // Add views as children based on whether it's grouped or single task. Looping through 1605 // taskGroups backwards populates the thumbnail grid from least recent to most recent. 1606 for (int i = taskGroups.size() - 1; i >= 0; i--) { 1607 GroupTask groupTask = taskGroups.get(i); 1608 boolean isRemovalNeeded = stagedTaskIdToBeRemovedFromGrid != INVALID_TASK_ID 1609 && groupTask.containsTask(stagedTaskIdToBeRemovedFromGrid); 1610 1611 if (groupTask instanceof DesktopTask) { 1612 desktopTask = (DesktopTask) groupTask; 1613 // Desktop task will be added separately in the end 1614 continue; 1615 } 1616 1617 TaskView taskView; 1618 if (isRemovalNeeded && groupTask.hasMultipleTasks()) { 1619 // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE 1620 // to be a temporary container for the remaining task. 1621 taskView = getTaskViewFromPool(TaskView.Type.SINGLE); 1622 } else { 1623 taskView = getTaskViewFromPool(groupTask.taskViewType); 1624 } 1625 1626 addView(taskView); 1627 1628 if (isRemovalNeeded && groupTask.hasMultipleTasks()) { 1629 if (groupTask.task1.key.id == stagedTaskIdToBeRemovedFromGrid) { 1630 taskView.bind(groupTask.task2, mOrientationState); 1631 } else { 1632 taskView.bind(groupTask.task1, mOrientationState); 1633 } 1634 } else if (isRemovalNeeded) { 1635 // If the task we need to remove is not part of a pair, bind it to the TaskView 1636 // first (to prevent problems), then remove the whole thing. 1637 taskView.bind(groupTask.task1, mOrientationState); 1638 removeView(taskView); 1639 } else if (taskView instanceof GroupedTaskView) { 1640 boolean firstTaskIsLeftTopTask = 1641 groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id; 1642 Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2; 1643 Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1; 1644 1645 ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState, 1646 groupTask.mSplitBounds); 1647 } else { 1648 taskView.bind(groupTask.task1, mOrientationState); 1649 } 1650 1651 // enables instance filtering if the feature flag for it is on 1652 if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) { 1653 taskView.setUpShowAllInstancesListener(); 1654 } 1655 } 1656 1657 if (!taskGroups.isEmpty()) { 1658 addView(mClearAllButton); 1659 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 1660 // Check if we have apps on the desktop 1661 if (desktopTask != null && !desktopTask.tasks.isEmpty()) { 1662 // If we are actively choosing apps for split, skip the desktop tile 1663 if (!getSplitSelectController().isSplitSelectActive()) { 1664 mDesktopTaskView = (DesktopTaskView) getTaskViewFromPool( 1665 TaskView.Type.DESKTOP); 1666 // Always add a desktop task to the first position 1667 addView(mDesktopTaskView, 0); 1668 mDesktopTaskView.bind(desktopTask.tasks, mOrientationState); 1669 } 1670 } 1671 } 1672 } 1673 1674 // Keep same previous focused task 1675 TaskView newFocusedTaskView = getTaskViewByTaskId(focusedTaskId); 1676 // If the list changed, maybe the focused task doesn't exist anymore 1677 if (newFocusedTaskView == null && getTaskViewCount() > 0) { 1678 newFocusedTaskView = getTaskViewAt(0); 1679 // Check if the first task is the desktop. 1680 // If first task is desktop, try to find another task to set as the focused task 1681 if (newFocusedTaskView != null && newFocusedTaskView.isDesktopTask() 1682 && getTaskViewCount() > 1) { 1683 newFocusedTaskView = getTaskViewAt(1); 1684 } 1685 } 1686 mFocusedTaskViewId = newFocusedTaskView != null && !ENABLE_GRID_ONLY_OVERVIEW.get() 1687 ? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID; 1688 updateTaskSize(); 1689 updateChildTaskOrientations(); 1690 1691 TaskView newRunningTaskView = null; 1692 if (runningTaskId != INVALID_TASK_ID) { 1693 // Update mRunningTaskViewId to be the new TaskView that was assigned by binding 1694 // the full list of tasks to taskViews 1695 newRunningTaskView = getTaskViewByTaskId(runningTaskId); 1696 if (newRunningTaskView != null) { 1697 mRunningTaskViewId = newRunningTaskView.getTaskViewId(); 1698 } else { 1699 mRunningTaskViewId = INVALID_TASK_ID; 1700 } 1701 } 1702 1703 int targetPage = -1; 1704 if (mNextPage != INVALID_PAGE) { 1705 // Restore mCurrentPage but don't call setCurrentPage() as that clobbers the scroll. 1706 mCurrentPage = previousCurrentPage; 1707 if (currentTaskId != INVALID_TASK_ID) { 1708 currentTaskView = getTaskViewByTaskId(currentTaskId); 1709 if (currentTaskView != null) { 1710 targetPage = indexOfChild(currentTaskView); 1711 } 1712 } 1713 } else { 1714 // Set the current page to the running task, but not if settling on new task. 1715 if (runningTaskId != INVALID_TASK_ID) { 1716 targetPage = indexOfChild(newRunningTaskView); 1717 } else if (getTaskViewCount() > 0) { 1718 TaskView taskView = requireTaskViewAt(0); 1719 // If first task id desktop, try to find another task to set the target page 1720 if (taskView.isDesktopTask() && getTaskViewCount() > 1) { 1721 taskView = requireTaskViewAt(1); 1722 } 1723 targetPage = indexOfChild(taskView); 1724 } 1725 } 1726 if (targetPage != -1 && mCurrentPage != targetPage) { 1727 int finalTargetPage = targetPage; 1728 runOnPageScrollsInitialized(() -> { 1729 // TODO(b/246283207): Remove logging once root cause of flake detected. 1730 if (Utilities.isRunningInTestHarness()) { 1731 Log.d("b/246283207", "RecentsView#applyLoadPlan() -> " 1732 + "previousCurrentPage: " + previousCurrentPage 1733 + ", targetPage: " + finalTargetPage 1734 + ", getScrollForPage(targetPage): " 1735 + getScrollForPage(finalTargetPage)); 1736 } 1737 setCurrentPage(finalTargetPage); 1738 }); 1739 } 1740 1741 if (mIgnoreResetTaskId != INVALID_TASK_ID && 1742 getTaskViewByTaskId(mIgnoreResetTaskId) != ignoreResetTaskView) { 1743 // If the taskView mapping is changing, do not preserve the visuals. Since we are 1744 // mostly preserving the first task, and new taskViews are added to the end, it should 1745 // generally map to the same task. 1746 mIgnoreResetTaskId = INVALID_TASK_ID; 1747 } 1748 resetTaskVisuals(); 1749 onTaskStackUpdated(); 1750 updateEnabledOverlays(); 1751 if (isPageScrollsInitialized()) { 1752 onPageScrollsInitialized(); 1753 } 1754 } 1755 isModal()1756 private boolean isModal() { 1757 return mTaskModalness > 0; 1758 } 1759 isLoadingTasks()1760 public boolean isLoadingTasks() { 1761 return mModel.isLoadingTasksInBackground(); 1762 } 1763 removeTasksViewsAndClearAllButton()1764 private void removeTasksViewsAndClearAllButton() { 1765 for (int i = getTaskViewCount() - 1; i >= 0; i--) { 1766 removeView(requireTaskViewAt(i)); 1767 } 1768 if (indexOfChild(mClearAllButton) != -1) { 1769 removeView(mClearAllButton); 1770 } 1771 } 1772 getTaskViewCount()1773 public int getTaskViewCount() { 1774 int taskViewCount = getChildCount(); 1775 if (indexOfChild(mClearAllButton) != -1) { 1776 taskViewCount--; 1777 } 1778 return taskViewCount; 1779 } 1780 getGroupedTaskViewCount()1781 public int getGroupedTaskViewCount() { 1782 int groupViewCount = 0; 1783 for (int i = 0; i < getChildCount(); i++) { 1784 if (getChildAt(i) instanceof GroupedTaskView) { 1785 groupViewCount++; 1786 } 1787 } 1788 return groupViewCount; 1789 } 1790 1791 /** 1792 * Returns the number of tasks in the top row of the overview grid. 1793 */ getTopRowTaskCountForTablet()1794 public int getTopRowTaskCountForTablet() { 1795 return mTopRowIdSet.size(); 1796 } 1797 1798 /** 1799 * Returns the number of tasks in the bottom row of the overview grid. 1800 */ getBottomRowTaskCountForTablet()1801 public int getBottomRowTaskCountForTablet() { 1802 return getTaskViewCount() - mTopRowIdSet.size() - (ENABLE_GRID_ONLY_OVERVIEW.get() ? 0 : 1); 1803 } 1804 onTaskStackUpdated()1805 protected void onTaskStackUpdated() { 1806 // Lazily update the empty message only when the task stack is reapplied 1807 updateEmptyMessage(); 1808 } 1809 resetTaskVisuals()1810 public void resetTaskVisuals() { 1811 for (int i = getTaskViewCount() - 1; i >= 0; i--) { 1812 TaskView taskView = requireTaskViewAt(i); 1813 if (mIgnoreResetTaskId != taskView.getTaskIds()[0]) { 1814 taskView.resetViewTransforms(); 1815 taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); 1816 taskView.setStableAlpha(mContentAlpha); 1817 taskView.setFullscreenProgress(mFullscreenProgress); 1818 taskView.setModalness(mTaskModalness); 1819 taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha); 1820 } 1821 } 1822 // resetTaskVisuals is called at the end of dismiss animation which could update 1823 // primary and secondary translation of the live tile cut out. We will need to do so 1824 // here accordingly. 1825 runActionOnRemoteHandles(remoteTargetHandle -> { 1826 TaskViewSimulator simulator = remoteTargetHandle.getTaskViewSimulator(); 1827 simulator.taskPrimaryTranslation.value = 0; 1828 simulator.taskSecondaryTranslation.value = 0; 1829 simulator.fullScreenProgress.value = 0; 1830 simulator.recentsViewScale.value = 1; 1831 }); 1832 // Similar to setRunningTaskHidden below, reapply the state before runningTaskView is 1833 // null. 1834 if (!mRunningTaskShowScreenshot) { 1835 setRunningTaskViewShowScreenshot(mRunningTaskShowScreenshot); 1836 } 1837 if (mRunningTaskTileHidden) { 1838 setRunningTaskHidden(mRunningTaskTileHidden); 1839 } 1840 1841 updateCurveProperties(); 1842 // Update the set of visible task's data 1843 loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); 1844 setTaskModalness(0); 1845 setColorTint(0); 1846 } 1847 setFullscreenProgress(float fullscreenProgress)1848 public void setFullscreenProgress(float fullscreenProgress) { 1849 mFullscreenProgress = fullscreenProgress; 1850 int taskCount = getTaskViewCount(); 1851 for (int i = 0; i < taskCount; i++) { 1852 requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress); 1853 } 1854 mClearAllButton.setFullscreenProgress(fullscreenProgress); 1855 1856 // Fade out the actions view quickly (0.1 range) 1857 mActionsView.getFullscreenAlpha().setValue( 1858 mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR)); 1859 } 1860 updateTaskStackListenerState()1861 private void updateTaskStackListenerState() { 1862 boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() 1863 && getWindowVisibility() == VISIBLE; 1864 if (handleTaskStackChanges != mHandleTaskStackChanges) { 1865 mHandleTaskStackChanges = handleTaskStackChanges; 1866 if (handleTaskStackChanges) { 1867 reloadIfNeeded(); 1868 } 1869 } 1870 } 1871 1872 @Override setInsets(Rect insets)1873 public void setInsets(Rect insets) { 1874 mInsets.set(insets); 1875 1876 // Update DeviceProfile dependant state. 1877 DeviceProfile dp = mActivity.getDeviceProfile(); 1878 setOverviewGridEnabled( 1879 mActivity.getStateManager().getState().displayOverviewTasksAsGrid(dp)); 1880 if (ENABLE_GRID_ONLY_OVERVIEW.get()) { 1881 mActionsView.updateHiddenFlags(HIDDEN_ACTIONS_IN_MENU, dp.isTablet); 1882 } 1883 setPageSpacing(dp.overviewPageSpacing); 1884 1885 // Propagate DeviceProfile change event. 1886 runActionOnRemoteHandles( 1887 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator().setDp(dp)); 1888 mOrientationState.setDeviceProfile(dp); 1889 1890 // Update RecentsView and TaskView's DeviceProfile dependent layout. 1891 updateOrientationHandler(); 1892 mActionsView.updateDimension(dp, mLastComputedTaskSize); 1893 } 1894 updateOrientationHandler()1895 private void updateOrientationHandler() { 1896 updateOrientationHandler(true); 1897 } 1898 updateOrientationHandler(boolean forceRecreateDragLayerControllers)1899 private void updateOrientationHandler(boolean forceRecreateDragLayerControllers) { 1900 // Handle orientation changes. 1901 PagedOrientationHandler oldOrientationHandler = mOrientationHandler; 1902 mOrientationHandler = mOrientationState.getOrientationHandler(); 1903 1904 mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources()); 1905 setLayoutDirection(mIsRtl 1906 ? View.LAYOUT_DIRECTION_RTL 1907 : View.LAYOUT_DIRECTION_LTR); 1908 mClearAllButton.setLayoutDirection(mIsRtl 1909 ? View.LAYOUT_DIRECTION_LTR 1910 : View.LAYOUT_DIRECTION_RTL); 1911 mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated()); 1912 1913 if (forceRecreateDragLayerControllers 1914 || !mOrientationHandler.equals(oldOrientationHandler)) { 1915 // Changed orientations, update controllers so they intercept accordingly. 1916 mActivity.getDragLayer().recreateControllers(); 1917 onOrientationChanged(); 1918 resetTaskVisuals(); 1919 } 1920 1921 boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0 1922 || mOrientationState.getRecentsActivityRotation() != ROTATION_0; 1923 mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, 1924 !mOrientationState.isRecentsActivityRotationAllowed() && isInLandscape); 1925 1926 // Update TaskView's DeviceProfile dependent layout. 1927 updateChildTaskOrientations(); 1928 1929 // Recalculate DeviceProfile dependent layout. 1930 updateSizeAndPadding(); 1931 1932 requestLayout(); 1933 // Reapply the current page to update page scrolls. 1934 setCurrentPage(mCurrentPage); 1935 } 1936 onOrientationChanged()1937 private void onOrientationChanged() { 1938 // If overview is in modal state when rotate, reset it to overview state without running 1939 // animation. 1940 setModalStateEnabled(/* taskId= */ INVALID_TASK_ID, /* animate= */ false); 1941 if (isSplitSelectionActive()) { 1942 onRotateInSplitSelectionState(); 1943 } 1944 } 1945 1946 // Update task size and padding that are dependent on DeviceProfile and insets. updateSizeAndPadding()1947 private void updateSizeAndPadding() { 1948 DeviceProfile dp = mActivity.getDeviceProfile(); 1949 getTaskSize(mTempRect); 1950 mTaskWidth = mTempRect.width(); 1951 mTaskHeight = mTempRect.height(); 1952 1953 mTempRect.top -= dp.overviewTaskThumbnailTopMarginPx; 1954 setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, 1955 dp.widthPx - mInsets.right - mTempRect.right, 1956 dp.heightPx - mInsets.bottom - mTempRect.bottom); 1957 1958 mSizeStrategy.calculateGridSize(mActivity.getDeviceProfile(), 1959 mLastComputedGridSize); 1960 mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(), 1961 mLastComputedGridTaskSize, mOrientationHandler); 1962 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 1963 mSizeStrategy.calculateDesktopTaskSize(mActivity, mActivity.getDeviceProfile(), 1964 mLastComputedDesktopTaskSize); 1965 } 1966 1967 mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top; 1968 mTopBottomRowHeightDiff = 1969 mLastComputedGridTaskSize.height() + dp.overviewTaskThumbnailTopMarginPx 1970 + dp.overviewRowSpacing; 1971 1972 // Force TaskView to update size from thumbnail 1973 updateTaskSize(); 1974 } 1975 1976 /** 1977 * Updates TaskView scaling and translation required to support variable width. 1978 */ updateTaskSize()1979 private void updateTaskSize() { 1980 updateTaskSize(false); 1981 } 1982 1983 /** 1984 * Updates TaskView scaling and translation required to support variable width. 1985 * 1986 * @param isTaskDismissal indicates if update was called due to task dismissal 1987 */ updateTaskSize(boolean isTaskDismissal)1988 private void updateTaskSize(boolean isTaskDismissal) { 1989 final int taskCount = getTaskViewCount(); 1990 if (taskCount == 0) { 1991 return; 1992 } 1993 1994 float accumulatedTranslationX = 0; 1995 for (int i = 0; i < taskCount; i++) { 1996 TaskView taskView = requireTaskViewAt(i); 1997 taskView.updateTaskSize(); 1998 taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX); 1999 taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f); 2000 // Compensate space caused by TaskView scaling. 2001 float widthDiff = 2002 taskView.getLayoutParams().width * (1 - taskView.getNonGridScale()); 2003 accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff; 2004 } 2005 2006 mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX); 2007 2008 updateGridProperties(isTaskDismissal); 2009 } 2010 getTaskSize(Rect outRect)2011 public void getTaskSize(Rect outRect) { 2012 mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect, 2013 mOrientationHandler); 2014 mLastComputedTaskSize.set(outRect); 2015 } 2016 2017 /** 2018 * Sets the last TaskView selected. 2019 */ setSelectedTask(int lastSelectedTaskId)2020 public void setSelectedTask(int lastSelectedTaskId) { 2021 mSelectedTask = getTaskViewByTaskId(lastSelectedTaskId); 2022 } 2023 2024 /** 2025 * Returns the bounds of the task selected to enter modal state. 2026 */ getSelectedTaskBounds()2027 public Rect getSelectedTaskBounds() { 2028 if (mSelectedTask == null) { 2029 return mLastComputedTaskSize; 2030 } 2031 return getTaskBounds(mSelectedTask); 2032 } 2033 getTaskBounds(TaskView taskView)2034 private Rect getTaskBounds(TaskView taskView) { 2035 int selectedPage = indexOfChild(taskView); 2036 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 2037 int selectedPageScroll = getScrollForPage(selectedPage); 2038 boolean isTopRow = taskView != null && mTopRowIdSet.contains(taskView.getTaskViewId()); 2039 Rect outRect = new Rect(mLastComputedTaskSize); 2040 outRect.offset( 2041 -(primaryScroll - (selectedPageScroll + getOffsetFromScrollPosition(selectedPage))), 2042 (int) (showAsGrid() && ENABLE_GRID_ONLY_OVERVIEW.get() && !isTopRow 2043 ? mTopBottomRowHeightDiff : 0)); 2044 return outRect; 2045 } 2046 2047 /** Gets the last computed task size */ getLastComputedTaskSize()2048 public Rect getLastComputedTaskSize() { 2049 return mLastComputedTaskSize; 2050 } 2051 getLastComputedGridTaskSize()2052 public Rect getLastComputedGridTaskSize() { 2053 return mLastComputedGridTaskSize; 2054 } 2055 2056 /** Gets the last computed desktop task size */ getLastComputedDesktopTaskSize()2057 public Rect getLastComputedDesktopTaskSize() { 2058 return mLastComputedDesktopTaskSize; 2059 } 2060 2061 /** Gets the task size for modal state. */ getModalTaskSize(Rect outRect)2062 public void getModalTaskSize(Rect outRect) { 2063 mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect, 2064 mOrientationHandler); 2065 } 2066 2067 @Override computeScrollHelper()2068 protected boolean computeScrollHelper() { 2069 boolean scrolling = super.computeScrollHelper(); 2070 boolean isFlingingFast = false; 2071 updateCurveProperties(); 2072 if (scrolling || isHandlingTouch()) { 2073 if (scrolling) { 2074 // Check if we are flinging quickly to disable high res thumbnail loading 2075 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; 2076 } 2077 2078 // After scrolling, update the visible task's data 2079 loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); 2080 } 2081 2082 // Update ActionsView's visibility when scroll changes. 2083 updateActionsViewFocusedScroll(); 2084 2085 // Update the high res thumbnail loader state 2086 mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast); 2087 return scrolling; 2088 } 2089 updateActionsViewFocusedScroll()2090 private void updateActionsViewFocusedScroll() { 2091 if (showAsGrid()) { 2092 float actionsViewAlphaValue = isFocusedTaskInExpectedScrollPosition() ? 1 : 0; 2093 // If animation is already in progress towards the same end value, do not restart. 2094 if (mActionsViewAlphaAnimator == null || !mActionsViewAlphaAnimator.isStarted() 2095 || (mActionsViewAlphaAnimator.isStarted() 2096 && mActionsViewAlphaAnimatorFinalValue != actionsViewAlphaValue)) { 2097 animateActionsViewAlpha(actionsViewAlphaValue, 2098 DEFAULT_ACTIONS_VIEW_ALPHA_ANIMATION_DURATION); 2099 } 2100 } 2101 } 2102 animateActionsViewAlpha(float alphaValue, long duration)2103 private void animateActionsViewAlpha(float alphaValue, long duration) { 2104 mActionsViewAlphaAnimator = ObjectAnimator.ofFloat( 2105 mActionsView.getVisibilityAlpha(), MULTI_PROPERTY_VALUE, alphaValue); 2106 mActionsViewAlphaAnimatorFinalValue = alphaValue; 2107 mActionsViewAlphaAnimator.setDuration(duration); 2108 // Set autocancel to prevent race-conditiony setting of alpha from other animations 2109 mActionsViewAlphaAnimator.setAutoCancel(true); 2110 mActionsViewAlphaAnimator.start(); 2111 } 2112 2113 /** 2114 * Scales and adjusts translation of adjacent pages as if on a curved carousel. 2115 */ updateCurveProperties()2116 public void updateCurveProperties() { 2117 if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { 2118 return; 2119 } 2120 int scroll = mOrientationHandler.getPrimaryScroll(this); 2121 mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled); 2122 2123 // Clear all button alpha was set by the previous line. 2124 mActionsView.getIndexScrollAlpha().setValue(1 - mClearAllButton.getScrollAlpha()); 2125 } 2126 2127 @Override getDestinationPage(int scaledScroll)2128 protected int getDestinationPage(int scaledScroll) { 2129 if (!mActivity.getDeviceProfile().isTablet) { 2130 return super.getDestinationPage(scaledScroll); 2131 } 2132 if (!isPageScrollsInitialized()) { 2133 Log.e(TAG, 2134 "Cannot get destination page: RecentsView not properly initialized", 2135 new IllegalStateException()); 2136 return INVALID_PAGE; 2137 } 2138 2139 // When in tablet with variable task width, return the page which scroll is closest to 2140 // screenStart instead of page nearest to center of screen. 2141 int minDistanceFromScreenStart = Integer.MAX_VALUE; 2142 int minDistanceFromScreenStartIndex = INVALID_PAGE; 2143 for (int i = 0; i < getChildCount(); ++i) { 2144 int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll); 2145 if (distanceFromScreenStart < minDistanceFromScreenStart) { 2146 minDistanceFromScreenStart = distanceFromScreenStart; 2147 minDistanceFromScreenStartIndex = i; 2148 } 2149 } 2150 return minDistanceFromScreenStartIndex; 2151 } 2152 2153 /** 2154 * Iterates through all the tasks, and loads the associated task data for newly visible tasks, 2155 * and unloads the associated task data for tasks that are no longer visible. 2156 */ loadVisibleTaskData(@askView.TaskDataChanges int dataChanges)2157 public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) { 2158 boolean hasLeftOverview = !mOverviewStateEnabled && mScroller.isFinished(); 2159 if (hasLeftOverview || mTaskListChangeId == -1) { 2160 // Skip loading visible task data if we've already left the overview state, or if the 2161 // task list hasn't been loaded yet (the task views will not reflect the task list) 2162 return; 2163 } 2164 2165 int lower = 0; 2166 int upper = 0; 2167 int visibleStart = 0; 2168 int visibleEnd = 0; 2169 if (showAsGrid()) { 2170 int screenStart = mOrientationHandler.getPrimaryScroll(this); 2171 int pageOrientedSize = mOrientationHandler.getMeasuredSize(this); 2172 int halfScreenSize = pageOrientedSize / 2; 2173 // Use +/- 50% screen width as visible area. 2174 visibleStart = screenStart - halfScreenSize; 2175 visibleEnd = screenStart + pageOrientedSize + halfScreenSize; 2176 } else { 2177 int centerPageIndex = getPageNearestToCenterOfScreen(); 2178 int numChildren = getChildCount(); 2179 lower = Math.max(0, centerPageIndex - 2); 2180 upper = Math.min(centerPageIndex + 2, numChildren - 1); 2181 } 2182 2183 // Update the task data for the in/visible children 2184 for (int i = 0; i < getTaskViewCount(); i++) { 2185 TaskView taskView = requireTaskViewAt(i); 2186 Task task = taskView.getTask(); 2187 if (task == null) { 2188 continue; 2189 } 2190 int index = indexOfChild(taskView); 2191 boolean visible; 2192 if (showAsGrid()) { 2193 visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd); 2194 } else { 2195 visible = lower <= index && index <= upper; 2196 } 2197 if (visible) { 2198 boolean skipLoadingTask = false; 2199 if (mTmpRunningTasks != null) { 2200 for (Task t : mTmpRunningTasks) { 2201 if (task == t) { 2202 // Skip loading if this is the task that we are animating into 2203 skipLoadingTask = true; 2204 break; 2205 } 2206 } 2207 } 2208 if (skipLoadingTask) { 2209 continue; 2210 } 2211 if (!mHasVisibleTaskData.get(task.key.id)) { 2212 // Ignore thumbnail update if it's current running task during the gesture 2213 // We snapshot at end of gesture, it will update then 2214 int changes = dataChanges; 2215 if (taskView == getRunningTaskView() && mGestureActive) { 2216 changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL; 2217 } 2218 taskView.onTaskListVisibilityChanged(true /* visible */, changes); 2219 } 2220 mHasVisibleTaskData.put(task.key.id, visible); 2221 } else { 2222 if (mHasVisibleTaskData.get(task.key.id)) { 2223 taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges); 2224 } 2225 mHasVisibleTaskData.delete(task.key.id); 2226 } 2227 } 2228 } 2229 2230 /** 2231 * Unloads any associated data from the currently visible tasks 2232 */ unloadVisibleTaskData(@askView.TaskDataChanges int dataChanges)2233 private void unloadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) { 2234 for (int i = 0; i < mHasVisibleTaskData.size(); i++) { 2235 if (mHasVisibleTaskData.valueAt(i)) { 2236 TaskView taskView = getTaskViewByTaskId(mHasVisibleTaskData.keyAt(i)); 2237 if (taskView != null) { 2238 taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges); 2239 } 2240 } 2241 } 2242 mHasVisibleTaskData.clear(); 2243 } 2244 2245 @Override onHighResLoadingStateChanged(boolean enabled)2246 public void onHighResLoadingStateChanged(boolean enabled) { 2247 // Whenever the high res loading state changes, poke each of the visible tasks to see if 2248 // they want to updated their thumbnail state 2249 for (int i = 0; i < mHasVisibleTaskData.size(); i++) { 2250 if (mHasVisibleTaskData.valueAt(i)) { 2251 TaskView taskView = getTaskViewByTaskId(mHasVisibleTaskData.keyAt(i)); 2252 if (taskView != null) { 2253 // Poke the view again, which will trigger it to load high res if the state 2254 // is enabled 2255 taskView.onTaskListVisibilityChanged(true /* visible */); 2256 } 2257 } 2258 } 2259 } 2260 startHome()2261 public abstract void startHome(); 2262 reset()2263 public void reset() { 2264 setCurrentTask(-1); 2265 mCurrentPageScrollDiff = 0; 2266 mIgnoreResetTaskId = -1; 2267 mTaskListChangeId = -1; 2268 mFocusedTaskViewId = -1; 2269 2270 if (mRecentsAnimationController != null) { 2271 if (mEnableDrawingLiveTile) { 2272 // We are still drawing the live tile, finish it now to clean up. 2273 finishRecentsAnimation(true /* toRecents */, null); 2274 } else { 2275 mRecentsAnimationController = null; 2276 } 2277 } 2278 setEnableDrawingLiveTile(false); 2279 runActionOnRemoteHandles(remoteTargetHandle -> { 2280 remoteTargetHandle.getTransformParams().setTargetSet(null); 2281 remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false); 2282 }); 2283 resetFromSplitSelectionState(); 2284 mSplitSelectStateController.resetState(); 2285 2286 // These are relatively expensive and don't need to be done this frame (RecentsView isn't 2287 // visible anyway), so defer by a frame to get off the critical path, e.g. app to home. 2288 post(() -> { 2289 unloadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); 2290 setCurrentPage(0); 2291 LayoutUtils.setViewEnabled(mActionsView, true); 2292 if (mOrientationState.setGestureActive(false)) { 2293 updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); 2294 } 2295 }); 2296 } 2297 getRunningTaskViewId()2298 public int getRunningTaskViewId() { 2299 return mRunningTaskViewId; 2300 } 2301 getTaskIdsForRunningTaskView()2302 protected int[] getTaskIdsForRunningTaskView() { 2303 return getTaskIdsForTaskViewId(mRunningTaskViewId); 2304 } 2305 getTaskIdsForTaskViewId(int taskViewId)2306 private int[] getTaskIdsForTaskViewId(int taskViewId) { 2307 // For now 2 distinct task IDs is max for split screen 2308 TaskView runningTaskView = getTaskViewFromTaskViewId(taskViewId); 2309 if (runningTaskView == null) { 2310 return INVALID_TASK_IDS; 2311 } 2312 2313 return runningTaskView.getTaskIds(); 2314 } 2315 getRunningTaskView()2316 public @Nullable TaskView getRunningTaskView() { 2317 return getTaskViewFromTaskViewId(mRunningTaskViewId); 2318 } 2319 getFocusedTaskView()2320 public @Nullable TaskView getFocusedTaskView() { 2321 return getTaskViewFromTaskViewId(mFocusedTaskViewId); 2322 } 2323 2324 @Nullable getTaskViewFromTaskViewId(int taskViewId)2325 private TaskView getTaskViewFromTaskViewId(int taskViewId) { 2326 if (taskViewId == -1) { 2327 return null; 2328 } 2329 2330 for (int i = 0; i < getTaskViewCount(); i++) { 2331 TaskView taskView = requireTaskViewAt(i); 2332 if (taskView.getTaskViewId() == taskViewId) { 2333 return taskView; 2334 } 2335 } 2336 return null; 2337 } 2338 getRunningTaskIndex()2339 public int getRunningTaskIndex() { 2340 TaskView taskView = getRunningTaskView(); 2341 return taskView == null ? -1 : indexOfChild(taskView); 2342 } 2343 getHomeTaskView()2344 protected @Nullable TaskView getHomeTaskView() { 2345 return null; 2346 } 2347 2348 /** 2349 * Handle the edge case where Recents could increment task count very high over long 2350 * period of device usage. Probably will never happen, but meh. 2351 */ getTaskViewFromPool(@askView.Type int type)2352 private TaskView getTaskViewFromPool(@TaskView.Type int type) { 2353 TaskView taskView; 2354 switch (type) { 2355 case TaskView.Type.GROUPED: 2356 taskView = mGroupedTaskViewPool.getView(); 2357 break; 2358 case TaskView.Type.DESKTOP: 2359 taskView = mDesktopTaskViewPool.getView(); 2360 break; 2361 case TaskView.Type.SINGLE: 2362 default: 2363 taskView = mTaskViewPool.getView(); 2364 } 2365 taskView.setTaskViewId(mTaskViewIdCount); 2366 if (mTaskViewIdCount == Integer.MAX_VALUE) { 2367 mTaskViewIdCount = 0; 2368 } else { 2369 mTaskViewIdCount++; 2370 } 2371 2372 return taskView; 2373 } 2374 2375 /** 2376 * Get the index of the task view whose id matches {@param taskId}. 2377 * @return -1 if there is no task view for the task id, else the index of the task view. 2378 */ getTaskIndexForId(int taskId)2379 public int getTaskIndexForId(int taskId) { 2380 TaskView tv = getTaskViewByTaskId(taskId); 2381 return tv == null ? -1 : indexOfChild(tv); 2382 } 2383 2384 /** 2385 * Reloads the view if anything in recents changed. 2386 */ reloadIfNeeded()2387 public void reloadIfNeeded() { 2388 if (!mModel.isTaskListValid(mTaskListChangeId)) { 2389 mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState 2390 .getFilter(mFilterState.getPackageNameToFilter())); 2391 } 2392 } 2393 2394 /** 2395 * Called when a gesture from an app is starting. 2396 */ onGestureAnimationStart( Task[] runningTasks, RotationTouchHelper rotationTouchHelper)2397 public void onGestureAnimationStart( 2398 Task[] runningTasks, RotationTouchHelper rotationTouchHelper) { 2399 mGestureActive = true; 2400 // This needs to be called before the other states are set since it can create the task view 2401 if (mOrientationState.setGestureActive(true)) { 2402 setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(), 2403 rotationTouchHelper.getDisplayRotation()); 2404 // Force update to ensure the initial task size is computed even if the orientation has 2405 // not changed. 2406 updateSizeAndPadding(); 2407 } 2408 2409 showCurrentTask(runningTasks); 2410 setEnableFreeScroll(false); 2411 setEnableDrawingLiveTile(false); 2412 setRunningTaskHidden(true); 2413 setTaskIconScaledDown(true); 2414 } 2415 2416 /** 2417 * Called only when a swipe-up gesture from an app has completed. Only called after 2418 * {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}. 2419 */ onSwipeUpAnimationSuccess()2420 public void onSwipeUpAnimationSuccess() { 2421 animateUpTaskIconScale(); 2422 setSwipeDownShouldLaunchApp(true); 2423 } 2424 animateRecentsRotationInPlace(int newRotation)2425 private void animateRecentsRotationInPlace(int newRotation) { 2426 if (mOrientationState.isRecentsActivityRotationAllowed()) { 2427 // Let system take care of the rotation 2428 return; 2429 } 2430 AnimatorSet pa = setRecentsChangedOrientation(true); 2431 pa.addListener(AnimatorListeners.forSuccessCallback(() -> { 2432 setLayoutRotation(newRotation, mOrientationState.getDisplayRotation()); 2433 mActivity.getDragLayer().recreateControllers(); 2434 setRecentsChangedOrientation(false).start(); 2435 })); 2436 pa.start(); 2437 } 2438 setRecentsChangedOrientation(boolean fadeInChildren)2439 public AnimatorSet setRecentsChangedOrientation(boolean fadeInChildren) { 2440 getRunningTaskIndex(); 2441 int runningIndex = getCurrentPage(); 2442 AnimatorSet as = new AnimatorSet(); 2443 for (int i = 0; i < getTaskViewCount(); i++) { 2444 View taskView = requireTaskViewAt(i); 2445 if (runningIndex == i && taskView.getAlpha() != 0) { 2446 continue; 2447 } 2448 as.play(ObjectAnimator.ofFloat(taskView, View.ALPHA, fadeInChildren ? 0 : 1)); 2449 } 2450 return as; 2451 } 2452 updateChildTaskOrientations()2453 private void updateChildTaskOrientations() { 2454 for (int i = 0; i < getTaskViewCount(); i++) { 2455 requireTaskViewAt(i).setOrientationState(mOrientationState); 2456 } 2457 boolean shouldRotateMenuForFakeRotation = 2458 !mOrientationState.isRecentsActivityRotationAllowed(); 2459 if (!shouldRotateMenuForFakeRotation) { 2460 return; 2461 } 2462 TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU); 2463 if (tv != null) { 2464 // Rotation is supported on phone (details at b/254198019#comment4) 2465 tv.onRotationChanged(); 2466 } 2467 } 2468 2469 /** 2470 * Called when a gesture from an app has finished, and an end target has been determined. 2471 */ onPrepareGestureEndAnimation( @ullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget, TaskViewSimulator[] taskViewSimulators)2472 public void onPrepareGestureEndAnimation( 2473 @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget, 2474 TaskViewSimulator[] taskViewSimulators) { 2475 mCurrentGestureEndTarget = endTarget; 2476 boolean isOverviewEndTarget = endTarget == GestureState.GestureEndTarget.RECENTS; 2477 if (isOverviewEndTarget) { 2478 updateGridProperties(); 2479 } 2480 2481 BaseState<?> endState = mSizeStrategy.stateFromGestureEndTarget(endTarget); 2482 if (endState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) { 2483 TaskView runningTaskView = getRunningTaskView(); 2484 float runningTaskPrimaryGridTranslation = 0; 2485 if (runningTaskView != null) { 2486 // Apply the grid translation to running task unless it's being snapped to 2487 // and removes the current translation applied to the running task. 2488 runningTaskPrimaryGridTranslation = mOrientationHandler.getPrimaryValue( 2489 runningTaskView.getGridTranslationX(), 2490 runningTaskView.getGridTranslationY()) 2491 - runningTaskView.getPrimaryNonGridTranslationProperty().get( 2492 runningTaskView); 2493 } 2494 for (TaskViewSimulator tvs : taskViewSimulators) { 2495 if (animatorSet == null) { 2496 setGridProgress(1); 2497 tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation; 2498 } else { 2499 animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1)); 2500 animatorSet.play(tvs.taskPrimaryTranslation.animateToValue( 2501 runningTaskPrimaryGridTranslation)); 2502 } 2503 } 2504 } 2505 int splashAlpha = endState.showTaskThumbnailSplash() ? 1 : 0; 2506 if (animatorSet == null) { 2507 setTaskThumbnailSplashAlpha(splashAlpha); 2508 } else { 2509 animatorSet.play( 2510 ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, splashAlpha)); 2511 } 2512 } 2513 2514 /** 2515 * Called when a gesture from an app has finished, and the animation to the target has ended. 2516 */ onGestureAnimationEnd()2517 public void onGestureAnimationEnd() { 2518 mGestureActive = false; 2519 if (mOrientationState.setGestureActive(false)) { 2520 updateOrientationHandler(/* forceRecreateDragLayerControllers = */ false); 2521 } 2522 2523 setEnableFreeScroll(true); 2524 setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS); 2525 setRunningTaskHidden(false); 2526 animateUpTaskIconScale(); 2527 animateActionsViewIn(); 2528 2529 mCurrentGestureEndTarget = null; 2530 } 2531 2532 /** 2533 * Returns true if we should add a stub taskView for the running task id 2534 */ shouldAddStubTaskView(Task[] runningTasks)2535 protected boolean shouldAddStubTaskView(Task[] runningTasks) { 2536 if (runningTasks.length > 1) { 2537 TaskView primaryTaskView = getTaskViewByTaskId(runningTasks[0].key.id); 2538 TaskView secondaryTaskView = getTaskViewByTaskId(runningTasks[1].key.id); 2539 int leftTopTaskViewId = 2540 (primaryTaskView == null) ? -1 : primaryTaskView.getTaskViewId(); 2541 int rightBottomTaskViewId = 2542 (secondaryTaskView == null) ? -1 : secondaryTaskView.getTaskViewId(); 2543 // Add a new stub view if both taskIds don't match any taskViews 2544 return leftTopTaskViewId != rightBottomTaskViewId || leftTopTaskViewId == -1; 2545 } 2546 Task runningTaskInfo = runningTasks[0]; 2547 return runningTaskInfo != null && getTaskViewByTaskId(runningTaskInfo.key.id) == null; 2548 } 2549 2550 /** 2551 * Creates a task view (if necessary) to represent the task with the {@param runningTaskId}. 2552 * 2553 * All subsequent calls to reload will keep the task as the first item until {@link #reset()} 2554 * is called. Also scrolls the view to this task. 2555 */ showCurrentTask(Task[] runningTasks)2556 private void showCurrentTask(Task[] runningTasks) { 2557 if (runningTasks.length == 0) { 2558 return; 2559 } 2560 int runningTaskViewId = -1; 2561 boolean needGroupTaskView = runningTasks.length > 1; 2562 boolean needDesktopTask = hasDesktopTask(runningTasks); 2563 if (shouldAddStubTaskView(runningTasks)) { 2564 boolean wasEmpty = getChildCount() == 0; 2565 // Add an empty view for now until the task plan is loaded and applied 2566 final TaskView taskView; 2567 if (needDesktopTask) { 2568 taskView = getTaskViewFromPool(TaskView.Type.DESKTOP); 2569 mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length); 2570 addView(taskView, 0); 2571 ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks), 2572 mOrientationState); 2573 } else if (needGroupTaskView) { 2574 taskView = getTaskViewFromPool(TaskView.Type.GROUPED); 2575 mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]}; 2576 addView(taskView, 0); 2577 // When we create a placeholder task view mSplitBoundsConfig will be null, but with 2578 // the actual app running we won't need to show the thumbnail until all the tasks 2579 // load later anyways 2580 ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1], 2581 mOrientationState, mSplitBoundsConfig); 2582 } else { 2583 taskView = getTaskViewFromPool(TaskView.Type.SINGLE); 2584 addView(taskView, 0); 2585 // The temporary running task is only used for the duration between the start of the 2586 // gesture and the task list is loaded and applied 2587 mTmpRunningTasks = new Task[]{runningTasks[0]}; 2588 taskView.bind(mTmpRunningTasks[0], mOrientationState); 2589 } 2590 runningTaskViewId = taskView.getTaskViewId(); 2591 if (wasEmpty) { 2592 addView(mClearAllButton); 2593 } 2594 2595 // Measure and layout immediately so that the scroll values is updated instantly 2596 // as the user might be quick-switching 2597 measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY), 2598 makeMeasureSpec(getMeasuredHeight(), EXACTLY)); 2599 layout(getLeft(), getTop(), getRight(), getBottom()); 2600 } else if (getTaskViewByTaskId(runningTasks[0].key.id) != null) { 2601 runningTaskViewId = getTaskViewByTaskId(runningTasks[0].key.id).getTaskViewId(); 2602 } 2603 2604 boolean runningTaskTileHidden = mRunningTaskTileHidden; 2605 setCurrentTask(runningTaskViewId); 2606 mFocusedTaskViewId = ENABLE_GRID_ONLY_OVERVIEW.get() ? INVALID_TASK_ID : runningTaskViewId; 2607 runOnPageScrollsInitialized(() -> setCurrentPage(getRunningTaskIndex())); 2608 setRunningTaskViewShowScreenshot(false); 2609 setRunningTaskHidden(runningTaskTileHidden); 2610 // Update task size after setting current task. 2611 updateTaskSize(); 2612 updateChildTaskOrientations(); 2613 2614 // Reload the task list 2615 reloadIfNeeded(); 2616 } 2617 hasDesktopTask(Task[] runningTasks)2618 private boolean hasDesktopTask(Task[] runningTasks) { 2619 if (!DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 2620 return false; 2621 } 2622 for (Task task : runningTasks) { 2623 if (task.key.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) { 2624 return true; 2625 } 2626 } 2627 return false; 2628 } 2629 2630 /** 2631 * Sets the running task id, cleaning up the old running task if necessary. 2632 */ setCurrentTask(int runningTaskViewId)2633 public void setCurrentTask(int runningTaskViewId) { 2634 if (mRunningTaskViewId == runningTaskViewId) { 2635 return; 2636 } 2637 2638 if (mRunningTaskViewId != -1) { 2639 // Reset the state on the old running task view 2640 setTaskIconScaledDown(false); 2641 setRunningTaskViewShowScreenshot(true); 2642 setRunningTaskHidden(false); 2643 } 2644 mRunningTaskViewId = runningTaskViewId; 2645 } 2646 getTaskViewIdFromTaskId(int taskId)2647 private int getTaskViewIdFromTaskId(int taskId) { 2648 TaskView taskView = getTaskViewByTaskId(taskId); 2649 return taskView != null ? taskView.getTaskViewId() : -1; 2650 } 2651 2652 /** 2653 * Hides the tile associated with {@link #mRunningTaskViewId} 2654 */ setRunningTaskHidden(boolean isHidden)2655 public void setRunningTaskHidden(boolean isHidden) { 2656 mRunningTaskTileHidden = isHidden; 2657 TaskView runningTask = getRunningTaskView(); 2658 if (runningTask != null) { 2659 runningTask.setStableAlpha(isHidden ? 0 : mContentAlpha); 2660 if (!isHidden) { 2661 AccessibilityManagerCompat.sendCustomAccessibilityEvent(runningTask, 2662 AccessibilityEvent.TYPE_VIEW_FOCUSED, null); 2663 } 2664 } 2665 } 2666 setRunningTaskViewShowScreenshot(boolean showScreenshot)2667 private void setRunningTaskViewShowScreenshot(boolean showScreenshot) { 2668 mRunningTaskShowScreenshot = showScreenshot; 2669 TaskView runningTaskView = getRunningTaskView(); 2670 if (runningTaskView != null) { 2671 runningTaskView.setShowScreenshot(mRunningTaskShowScreenshot); 2672 } 2673 } 2674 setTaskIconScaledDown(boolean isScaledDown)2675 public void setTaskIconScaledDown(boolean isScaledDown) { 2676 if (mTaskIconScaledDown != isScaledDown) { 2677 mTaskIconScaledDown = isScaledDown; 2678 int taskCount = getTaskViewCount(); 2679 for (int i = 0; i < taskCount; i++) { 2680 requireTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1); 2681 } 2682 } 2683 } 2684 animateActionsViewIn()2685 private void animateActionsViewIn() { 2686 if (!showAsGrid() || isFocusedTaskInExpectedScrollPosition()) { 2687 animateActionsViewAlpha(1, TaskView.SCALE_ICON_DURATION); 2688 } 2689 } 2690 animateUpTaskIconScale()2691 public void animateUpTaskIconScale() { 2692 mTaskIconScaledDown = false; 2693 int taskCount = getTaskViewCount(); 2694 for (int i = 0; i < taskCount; i++) { 2695 TaskView taskView = requireTaskViewAt(i); 2696 taskView.setIconScaleAnimStartProgress(0f); 2697 taskView.animateIconScaleAndDimIntoView(); 2698 } 2699 } 2700 2701 /** 2702 * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid 2703 * layout. 2704 * This method is used when no task dismissal has occurred. 2705 */ updateGridProperties()2706 private void updateGridProperties() { 2707 updateGridProperties(false, Integer.MAX_VALUE); 2708 } 2709 2710 /** 2711 * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid 2712 * layout. 2713 * 2714 * This method is used when task dismissal has occurred, but rebalance is not needed. 2715 * 2716 * @param isTaskDismissal indicates if update was called due to task dismissal 2717 */ updateGridProperties(boolean isTaskDismissal)2718 private void updateGridProperties(boolean isTaskDismissal) { 2719 updateGridProperties(isTaskDismissal, Integer.MAX_VALUE); 2720 } 2721 2722 /** 2723 * Updates TaskView and ClearAllButton scaling and translation required to turn into grid 2724 * layout. 2725 * 2726 * This method only calculates the potential position and depends on {@link #setGridProgress} to 2727 * apply the actual scaling and translation. 2728 * 2729 * @param isTaskDismissal indicates if update was called due to task dismissal 2730 * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE 2731 * to skip rebalance 2732 */ updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter)2733 private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter) { 2734 int taskCount = getTaskViewCount(); 2735 if (taskCount == 0) { 2736 return; 2737 } 2738 2739 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 2740 int taskTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; 2741 2742 int topRowWidth = 0; 2743 int bottomRowWidth = 0; 2744 float topAccumulatedTranslationX = 0; 2745 float bottomAccumulatedTranslationX = 0; 2746 2747 // Contains whether the child index is in top or bottom of grid (for non-focused task) 2748 // Different from mTopRowIdSet, which contains the taskViewId of what task is in top row 2749 IntSet topSet = new IntSet(); 2750 IntSet bottomSet = new IntSet(); 2751 2752 // Horizontal grid translation for each task 2753 float[] gridTranslations = new float[taskCount]; 2754 2755 int focusedTaskIndex = Integer.MAX_VALUE; 2756 int focusedTaskShift = 0; 2757 int focusedTaskWidthAndSpacing = 0; 2758 int snappedTaskRowWidth = 0; 2759 int snappedPage = getNextPage(); 2760 TaskView snappedTaskView = getTaskViewAt(snappedPage); 2761 TaskView homeTaskView = getHomeTaskView(); 2762 TaskView nextFocusedTaskView = null; 2763 2764 int desktopTaskIndex = Integer.MAX_VALUE; 2765 2766 if (!isTaskDismissal) { 2767 mTopRowIdSet.clear(); 2768 } 2769 for (int i = 0; i < taskCount; i++) { 2770 TaskView taskView = requireTaskViewAt(i); 2771 int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing; 2772 // Evenly distribute tasks between rows unless rearranging due to task dismissal, in 2773 // which case keep tasks in their respective rows. For the running task, don't join 2774 // the grid. 2775 if (taskView.isFocusedTask()) { 2776 topRowWidth += taskWidthAndSpacing; 2777 bottomRowWidth += taskWidthAndSpacing; 2778 2779 focusedTaskIndex = i; 2780 focusedTaskWidthAndSpacing = taskWidthAndSpacing; 2781 gridTranslations[i] += focusedTaskShift; 2782 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; 2783 2784 // Center view vertically in case it's from different orientation. 2785 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin 2786 - taskView.getLayoutParams().height) / 2f); 2787 2788 if (taskView == snappedTaskView) { 2789 // If focused task is snapped, the row width is just task width and spacing. 2790 snappedTaskRowWidth = taskWidthAndSpacing; 2791 } 2792 } else if (taskView.isDesktopTask()) { 2793 // Desktop task was not focused. Pin it to the right of focused 2794 desktopTaskIndex = i; 2795 if (taskView.getVisibility() == View.GONE) { 2796 // Desktop task view is hidden, skip it from grid calculations 2797 continue; 2798 } 2799 if (!ENABLE_GRID_ONLY_OVERVIEW.get()) { 2800 // Only apply x-translation when using legacy overview grid 2801 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; 2802 } 2803 2804 // Center view vertically in case it's from different orientation. 2805 taskView.setGridTranslationY((mLastComputedDesktopTaskSize.height() + taskTopMargin 2806 - taskView.getLayoutParams().height) / 2f); 2807 } else { 2808 if (i > focusedTaskIndex) { 2809 // For tasks after the focused task, shift by focused task's width and spacing. 2810 gridTranslations[i] += 2811 mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; 2812 } else { 2813 // For task before the focused task, accumulate the width and spacing to 2814 // calculate the distance focused task need to shift. 2815 focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing; 2816 } 2817 int taskViewId = taskView.getTaskViewId(); 2818 2819 // Rebalance the grid starting after a certain index 2820 boolean isTopRow; 2821 if (isTaskDismissal) { 2822 if (i > startRebalanceAfter) { 2823 mTopRowIdSet.remove(taskViewId); 2824 isTopRow = topRowWidth <= bottomRowWidth; 2825 } else { 2826 isTopRow = mTopRowIdSet.contains(taskViewId); 2827 } 2828 } else { 2829 isTopRow = topRowWidth <= bottomRowWidth; 2830 } 2831 2832 if (isTopRow) { 2833 if (homeTaskView != null && nextFocusedTaskView == null) { 2834 // TaskView will be focused when swipe up, don't count towards row width. 2835 nextFocusedTaskView = taskView; 2836 } else { 2837 topRowWidth += taskWidthAndSpacing; 2838 } 2839 topSet.add(i); 2840 mTopRowIdSet.add(taskViewId); 2841 2842 taskView.setGridTranslationY(mTaskGridVerticalDiff); 2843 2844 // Move horizontally into empty space. 2845 float widthOffset = 0; 2846 for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) { 2847 if (j == focusedTaskIndex || j == desktopTaskIndex) { 2848 continue; 2849 } 2850 widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; 2851 } 2852 2853 float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; 2854 gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX; 2855 topAccumulatedTranslationX += currentTaskTranslationX; 2856 } else { 2857 bottomRowWidth += taskWidthAndSpacing; 2858 bottomSet.add(i); 2859 2860 // Move into bottom row. 2861 taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff); 2862 2863 // Move horizontally into empty space. 2864 float widthOffset = 0; 2865 for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) { 2866 if (j == focusedTaskIndex || j == desktopTaskIndex) { 2867 continue; 2868 } 2869 widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing; 2870 } 2871 2872 float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset; 2873 gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX; 2874 bottomAccumulatedTranslationX += currentTaskTranslationX; 2875 } 2876 if (taskView == snappedTaskView) { 2877 snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth; 2878 } 2879 } 2880 } 2881 2882 // We need to maintain snapped task's page scroll invariant between quick switch and 2883 // overview, so we sure snapped task's grid translation is 0, and add a non-fullscreen 2884 // translationX that is the same as snapped task's full scroll adjustment. 2885 float snappedTaskNonGridScrollAdjustment = 0; 2886 float snappedTaskGridTranslationX = 0; 2887 if (snappedTaskView != null) { 2888 snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment( 2889 /*gridEnabled=*/false); 2890 snappedTaskGridTranslationX = gridTranslations[snappedPage]; 2891 } 2892 2893 // Use the accumulated translation of the row containing the last task. 2894 float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1) 2895 ? topAccumulatedTranslationX : bottomAccumulatedTranslationX; 2896 2897 // If the last task is on the shorter row, ClearAllButton will embed into the shorter row 2898 // which is not what we want. Compensate the width difference of the 2 rows in that case. 2899 float shorterRowCompensation = 0; 2900 if (topRowWidth <= bottomRowWidth) { 2901 if (topSet.contains(taskCount - 1)) { 2902 shorterRowCompensation = bottomRowWidth - topRowWidth; 2903 } 2904 } else { 2905 if (bottomSet.contains(taskCount - 1)) { 2906 shorterRowCompensation = topRowWidth - bottomRowWidth; 2907 } 2908 } 2909 float clearAllShorterRowCompensation = 2910 mIsRtl ? -shorterRowCompensation : shorterRowCompensation; 2911 2912 // If the total width is shorter than one grid's width, move ClearAllButton further away 2913 // accordingly. Update longRowWidth if ClearAllButton has been moved. 2914 float clearAllShortTotalWidthTranslation = 0; 2915 int longRowWidth = Math.max(topRowWidth, bottomRowWidth); 2916 if (longRowWidth < mLastComputedGridSize.width()) { 2917 mClearAllShortTotalWidthTranslation = 2918 (mIsRtl 2919 ? mLastComputedTaskSize.right 2920 : deviceProfile.widthPx - mLastComputedTaskSize.left) 2921 - longRowWidth - deviceProfile.overviewGridSideMargin; 2922 clearAllShortTotalWidthTranslation = mIsRtl 2923 ? -mClearAllShortTotalWidthTranslation : mClearAllShortTotalWidthTranslation; 2924 if (snappedTaskRowWidth == longRowWidth) { 2925 // Updated snappedTaskRowWidth as well if it's same as longRowWidth. 2926 snappedTaskRowWidth += mClearAllShortTotalWidthTranslation; 2927 } 2928 longRowWidth += mClearAllShortTotalWidthTranslation; 2929 } else { 2930 mClearAllShortTotalWidthTranslation = 0; 2931 } 2932 2933 float clearAllTotalTranslationX = 2934 clearAllAccumulatedTranslation + clearAllShorterRowCompensation 2935 + clearAllShortTotalWidthTranslation + snappedTaskNonGridScrollAdjustment; 2936 if (focusedTaskIndex < taskCount) { 2937 // Shift by focused task's width and spacing if a task is focused. 2938 clearAllTotalTranslationX += 2939 mIsRtl ? focusedTaskWidthAndSpacing : -focusedTaskWidthAndSpacing; 2940 } 2941 2942 // Make sure there are enough space between snapped page and ClearAllButton, for the case 2943 // of swiping up after quick switch. 2944 if (snappedTaskView != null) { 2945 int distanceFromClearAll = longRowWidth - snappedTaskRowWidth; 2946 // ClearAllButton should be off screen when snapped task is in its snapped position. 2947 int minimumDistance = 2948 (mIsRtl 2949 ? mLastComputedTaskSize.left 2950 : deviceProfile.widthPx - mLastComputedTaskSize.right) 2951 - deviceProfile.overviewGridSideMargin - mPageSpacing 2952 + (mTaskWidth - snappedTaskView.getLayoutParams().width) 2953 - mClearAllShortTotalWidthTranslation; 2954 if (distanceFromClearAll < minimumDistance) { 2955 int distanceDifference = minimumDistance - distanceFromClearAll; 2956 snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference; 2957 } 2958 } 2959 2960 for (int i = 0; i < taskCount; i++) { 2961 TaskView taskView = requireTaskViewAt(i); 2962 taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX 2963 + snappedTaskNonGridScrollAdjustment); 2964 } 2965 2966 mClearAllButton.setGridTranslationPrimary( 2967 clearAllTotalTranslationX - snappedTaskGridTranslationX); 2968 mClearAllButton.setGridScrollOffset( 2969 mIsRtl ? mLastComputedTaskSize.left - mLastComputedGridSize.left 2970 : mLastComputedTaskSize.right - mLastComputedGridSize.right); 2971 2972 setGridProgress(mGridProgress); 2973 } 2974 isSameGridRow(TaskView taskView1, TaskView taskView2)2975 private boolean isSameGridRow(TaskView taskView1, TaskView taskView2) { 2976 if (taskView1 == null || taskView2 == null) { 2977 return false; 2978 } 2979 int taskViewId1 = taskView1.getTaskViewId(); 2980 int taskViewId2 = taskView2.getTaskViewId(); 2981 if (taskViewId1 == mFocusedTaskViewId || taskViewId2 == mFocusedTaskViewId) { 2982 return false; 2983 } 2984 return (mTopRowIdSet.contains(taskViewId1) && mTopRowIdSet.contains(taskViewId2)) || ( 2985 !mTopRowIdSet.contains(taskViewId1) && !mTopRowIdSet.contains(taskViewId2)); 2986 } 2987 2988 /** 2989 * Moves TaskView and ClearAllButton between carousel and 2 row grid. 2990 * 2991 * @param gridProgress 0 = carousel; 1 = 2 row grid. 2992 */ setGridProgress(float gridProgress)2993 private void setGridProgress(float gridProgress) { 2994 mGridProgress = gridProgress; 2995 2996 int taskCount = getTaskViewCount(); 2997 for (int i = 0; i < taskCount; i++) { 2998 requireTaskViewAt(i).setGridProgress(gridProgress); 2999 } 3000 mClearAllButton.setGridProgress(gridProgress); 3001 } 3002 setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha)3003 private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) { 3004 int taskCount = getTaskViewCount(); 3005 if (taskCount == 0) { 3006 return; 3007 } 3008 3009 mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha; 3010 for (int i = 0; i < taskCount; i++) { 3011 requireTaskViewAt(i).setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha); 3012 } 3013 } 3014 enableLayoutTransitions()3015 private void enableLayoutTransitions() { 3016 if (mLayoutTransition == null) { 3017 mLayoutTransition = new LayoutTransition(); 3018 mLayoutTransition.enableTransitionType(LayoutTransition.APPEARING); 3019 mLayoutTransition.setDuration(ADDITION_TASK_DURATION); 3020 mLayoutTransition.setStartDelay(LayoutTransition.APPEARING, 0); 3021 3022 mLayoutTransition.addTransitionListener(new TransitionListener() { 3023 @Override 3024 public void startTransition(LayoutTransition transition, ViewGroup viewGroup, 3025 View view, int i) { 3026 } 3027 3028 @Override 3029 public void endTransition(LayoutTransition transition, ViewGroup viewGroup, 3030 View view, int i) { 3031 // When the unpinned task is added, snap to first page and disable transitions 3032 if (view instanceof TaskView) { 3033 snapToPage(0); 3034 setLayoutTransition(null); 3035 } 3036 3037 } 3038 }); 3039 } 3040 setLayoutTransition(mLayoutTransition); 3041 } 3042 setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp)3043 public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { 3044 mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; 3045 } 3046 shouldSwipeDownLaunchApp()3047 public boolean shouldSwipeDownLaunchApp() { 3048 return mSwipeDownShouldLaunchApp; 3049 } 3050 setIgnoreResetTask(int taskId)3051 public void setIgnoreResetTask(int taskId) { 3052 mIgnoreResetTaskId = taskId; 3053 } 3054 clearIgnoreResetTask(int taskId)3055 public void clearIgnoreResetTask(int taskId) { 3056 if (mIgnoreResetTaskId == taskId) { 3057 mIgnoreResetTaskId = -1; 3058 } 3059 } 3060 addDismissedTaskAnimations(TaskView taskView, long duration, PendingAnimation anim)3061 private void addDismissedTaskAnimations(TaskView taskView, long duration, 3062 PendingAnimation anim) { 3063 // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's 3064 // alpha is set to 0 so that it can be recycled in the view pool properly 3065 anim.setFloat(taskView, VIEW_ALPHA, 0, 3066 clampToProgress(isOnGridBottomRow(taskView) ? ACCEL : FINAL_FRAME, 0, 0.5f)); 3067 FloatProperty<TaskView> secondaryViewTranslate = 3068 taskView.getSecondaryDismissTranslationProperty(); 3069 int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView); 3070 int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor(); 3071 3072 ResourceProvider rp = DynamicResource.provider(mActivity); 3073 SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START) 3074 .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio)) 3075 .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness)); 3076 3077 anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate, 3078 verticalFactor * secondaryTaskDimension * 2).setDuration(duration), LINEAR, sp); 3079 3080 if (mEnableDrawingLiveTile && taskView.isRunningTask()) { 3081 anim.addOnFrameCallback(() -> { 3082 runActionOnRemoteHandles( 3083 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() 3084 .taskSecondaryTranslation.value = mOrientationHandler 3085 .getSecondaryValue(taskView.getTranslationX(), 3086 taskView.getTranslationY() 3087 )); 3088 redrawLiveTile(); 3089 }); 3090 } 3091 } 3092 3093 /** 3094 * Places an {@link FloatingTaskView} on top of the thumbnail for {@link #mSplitHiddenTaskView} 3095 * and then animates it into the split position that was desired 3096 */ createInitialSplitSelectAnimation(PendingAnimation anim)3097 private void createInitialSplitSelectAnimation(PendingAnimation anim) { 3098 mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize, 3099 mSplitPlaceholderInset, mActivity.getDeviceProfile(), 3100 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect); 3101 SplitAnimationTimings timings = 3102 AnimUtils.getDeviceOverviewToSplitTimings(mActivity.getDeviceProfile().isTablet); 3103 3104 RectF startingTaskRect = new RectF(); 3105 safeRemoveDragLayerView(mFirstFloatingTaskView); 3106 SplitAnimInitProps splitAnimInitProps = 3107 mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews( 3108 () -> mSplitHiddenTaskView, () -> mSplitSelectSource); 3109 if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) { 3110 // Create the split select animation from Overview 3111 mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE, 3112 mSplitSelectStateController.getInitialTaskId()); 3113 anim.setViewAlpha(splitAnimInitProps.getIconView(), 0, clampToProgress(LINEAR, 3114 timings.getIconFadeStartOffset(), 3115 timings.getIconFadeEndOffset())); 3116 } 3117 3118 mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, 3119 splitAnimInitProps.getOriginalView(), 3120 splitAnimInitProps.getOriginalBitmap(), 3121 splitAnimInitProps.getIconDrawable(), startingTaskRect); 3122 mFirstFloatingTaskView.setAlpha(1); 3123 mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect, 3124 splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask()); 3125 3126 // Allow user to click staged app to launch into fullscreen 3127 if (ENABLE_LAUNCH_FROM_STAGED_APP.get()) { 3128 mFirstFloatingTaskView.setOnClickListener(this::animateToFullscreen); 3129 } 3130 3131 // SplitInstructionsView: animate in 3132 safeRemoveDragLayerView(mSplitInstructionsView); 3133 mSplitInstructionsView = SplitInstructionsView.getSplitInstructionsView(mActivity); 3134 mSplitInstructionsView.setAlpha(0); 3135 anim.setViewAlpha(mSplitInstructionsView, 1, clampToProgress(LINEAR, 3136 timings.getInstructionsContainerFadeInStartOffset(), 3137 timings.getInstructionsContainerFadeInEndOffset())); 3138 anim.setViewAlpha(mSplitInstructionsView.getTextView(), 1, clampToProgress(LINEAR, 3139 timings.getInstructionsTextFadeInStartOffset(), 3140 timings.getInstructionsTextFadeInEndOffset())); 3141 anim.addFloat(mSplitInstructionsView, mSplitInstructionsView.UNFOLD, 0.1f, 1, 3142 clampToProgress(EMPHASIZED_DECELERATE, 3143 timings.getInstructionsUnfoldStartOffset(), 3144 timings.getInstructionsUnfoldEndOffset())); 3145 3146 InteractionJankMonitorWrapper.begin(this, 3147 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected"); 3148 anim.addListener(new AnimatorListenerAdapter() { 3149 @Override 3150 public void onAnimationStart(Animator animation) { 3151 if (mSplitHiddenTaskView == getRunningTaskView()) { 3152 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, 3153 null /* onFinishComplete */); 3154 } else { 3155 switchToScreenshot( 3156 () -> finishRecentsAnimation(true /* toRecents */, 3157 false /* shouldPip */, null /* onFinishComplete */)); 3158 } 3159 } 3160 }); 3161 anim.addEndListener(success -> { 3162 if (success) { 3163 InteractionJankMonitorWrapper.end( 3164 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); 3165 } else { 3166 // If transition to split select was interrupted, clean up to prevent glitches 3167 resetFromSplitSelectionState(); 3168 InteractionJankMonitorWrapper.cancel( 3169 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); 3170 } 3171 3172 updateCurrentTaskActionsVisibility(); 3173 }); 3174 } 3175 animateToFullscreen(View view)3176 private void animateToFullscreen(View view) { 3177 FloatingTaskView stagedTaskView = (FloatingTaskView) view; 3178 3179 boolean isTablet = mActivity.getDeviceProfile().isTablet; 3180 int duration = isTablet 3181 ? SplitAnimationTimings.TABLET_CONFIRM_DURATION 3182 : SplitAnimationTimings.PHONE_CONFIRM_DURATION; 3183 3184 PendingAnimation pendingAnimation = new PendingAnimation(duration); 3185 3186 Rect firstTaskStartingBounds = new Rect(); 3187 Rect firstTaskEndingBounds = new Rect(); 3188 3189 stagedTaskView.getBoundsOnScreen(firstTaskStartingBounds); 3190 mActivity.getDragLayer().getBoundsOnScreen(firstTaskEndingBounds); 3191 3192 stagedTaskView.addConfirmAnimation( 3193 pendingAnimation, 3194 new RectF(firstTaskStartingBounds), 3195 firstTaskEndingBounds, 3196 false /* fadeWithThumbnail */, 3197 true /* isStagedTask */); 3198 3199 pendingAnimation.addEndListener(animationSuccess -> 3200 mSplitSelectStateController.launchSplitTasks(launchSuccess -> 3201 resetFromSplitSelectionState())); 3202 3203 pendingAnimation.buildAnim().start(); 3204 } 3205 3206 /** 3207 * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}. 3208 * @param dismissedTaskView the {@link TaskView} to be dismissed 3209 * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated 3210 * @param shouldRemoveTask whether the associated {@link Task} should be removed from 3211 * ActivityManager after dismissal 3212 * @param duration duration of the animation 3213 * @param dismissingForSplitSelection task dismiss animation is used for entering split 3214 * selection state from app icon 3215 */ createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView, boolean animateTaskView, boolean shouldRemoveTask, long duration, boolean dismissingForSplitSelection)3216 public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView, 3217 boolean animateTaskView, boolean shouldRemoveTask, long duration, 3218 boolean dismissingForSplitSelection) { 3219 if (mPendingAnimation != null) { 3220 mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd(); 3221 } 3222 3223 int count = getPageCount(); 3224 if (count == 0) { 3225 return; 3226 } 3227 3228 boolean showAsGrid = showAsGrid(); 3229 int taskCount = getTaskViewCount(); 3230 int dismissedIndex = indexOfChild(dismissedTaskView); 3231 int dismissedTaskViewId = dismissedTaskView.getTaskViewId(); 3232 3233 // Grid specific properties. 3234 boolean isFocusedTaskDismissed = false; 3235 boolean isStagingFocusedTask = false; 3236 TaskView nextFocusedTaskView = null; 3237 boolean nextFocusedTaskFromTop = false; 3238 float dismissedTaskWidth = 0; 3239 float nextFocusedTaskWidth = 0; 3240 3241 // Non-grid specific properties. 3242 int[] oldScroll = new int[count]; 3243 int[] newScroll = new int[count]; 3244 int scrollDiffPerPage = 0; 3245 boolean needsCurveUpdates = false; 3246 3247 if (showAsGrid) { 3248 dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing; 3249 isFocusedTaskDismissed = dismissedTaskViewId == mFocusedTaskViewId; 3250 if (isFocusedTaskDismissed) { 3251 if (isSplitSelectionActive()) { 3252 isStagingFocusedTask = true; 3253 } else { 3254 nextFocusedTaskFromTop = 3255 mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f; 3256 // Pick the next focused task from the preferred row. 3257 for (int i = 0; i < taskCount; i++) { 3258 TaskView taskView = requireTaskViewAt(i); 3259 if (taskView == dismissedTaskView) { 3260 continue; 3261 } 3262 boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskViewId()); 3263 if ((nextFocusedTaskFromTop && isTopRow 3264 || (!nextFocusedTaskFromTop && !isTopRow))) { 3265 nextFocusedTaskView = taskView; 3266 break; 3267 } 3268 } 3269 if (nextFocusedTaskView != null) { 3270 nextFocusedTaskWidth = 3271 nextFocusedTaskView.getLayoutParams().width + mPageSpacing; 3272 } 3273 } 3274 } 3275 } else { 3276 getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); 3277 getPageScrolls(newScroll, false, 3278 v -> v.getVisibility() != GONE && v != dismissedTaskView); 3279 if (count > 1) { 3280 scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]); 3281 } 3282 } 3283 3284 float dismissTranslationInterpolationEnd = 1; 3285 boolean closeGapBetweenClearAll = false; 3286 boolean isClearAllHidden = isClearAllHidden(); 3287 boolean snapToLastTask = false; 3288 boolean isLandscapeSplit = 3289 mActivity.getDeviceProfile().isLandscape && isSplitSelectionActive(); 3290 TaskView lastGridTaskView = showAsGrid ? getLastGridTaskView() : null; 3291 int currentPageScroll = getScrollForPage(mCurrentPage); 3292 int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView)); 3293 boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll; 3294 if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) { 3295 // After dismissal, animate translation of the remaining tasks to fill any gap left 3296 // between the end of the grid and the clear all button. Only animate if the clear 3297 // all button is visible or would become visible after dismissal. 3298 float longGridRowWidthDiff = 0; 3299 3300 int topGridRowSize = mTopRowIdSet.size(); 3301 int bottomGridRowSize = taskCount - mTopRowIdSet.size() 3302 - (ENABLE_GRID_ONLY_OVERVIEW.get() ? 0 : 1); 3303 boolean topRowLonger = topGridRowSize > bottomGridRowSize; 3304 boolean bottomRowLonger = bottomGridRowSize > topGridRowSize; 3305 boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId); 3306 boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed; 3307 if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) { 3308 topGridRowSize--; 3309 } 3310 if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) { 3311 bottomGridRowSize--; 3312 } 3313 int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize) 3314 * (mLastComputedGridTaskSize.width() + mPageSpacing); 3315 if (!ENABLE_GRID_ONLY_OVERVIEW.get() && !isStagingFocusedTask) { 3316 longRowWidth += mLastComputedTaskSize.width() + mPageSpacing; 3317 } 3318 3319 float gapWidth = 0; 3320 if ((topRowLonger && dismissedTaskFromTop) 3321 || (bottomRowLonger && dismissedTaskFromBottom)) { 3322 gapWidth = dismissedTaskWidth; 3323 } else if (nextFocusedTaskView != null 3324 && ((topRowLonger && nextFocusedTaskFromTop) 3325 || (bottomRowLonger && !nextFocusedTaskFromTop))) { 3326 gapWidth = nextFocusedTaskWidth; 3327 } 3328 if (gapWidth > 0) { 3329 if (mClearAllShortTotalWidthTranslation == 0) { 3330 // Compensate the removed gap if we don't already have shortTotalCompensation, 3331 // and adjust accordingly to the new shortTotalCompensation after dismiss. 3332 int newClearAllShortTotalWidthTranslation = 0; 3333 if (longRowWidth < mLastComputedGridSize.width()) { 3334 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 3335 newClearAllShortTotalWidthTranslation = 3336 (mIsRtl 3337 ? mLastComputedTaskSize.right 3338 : deviceProfile.widthPx - mLastComputedTaskSize.left) 3339 - longRowWidth - deviceProfile.overviewGridSideMargin; 3340 } 3341 float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation; 3342 longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation; 3343 } 3344 if (isClearAllHidden) { 3345 // If ClearAllButton isn't fully shown, snap to the last task. 3346 snapToLastTask = true; 3347 } 3348 } 3349 if (isLandscapeSplit && !isStagingFocusedTask) { 3350 // LastTask's scroll is the minimum scroll in split select, if current scroll is 3351 // beyond that, we'll need to snap to last task instead. 3352 TaskView lastTask = getLastGridTaskView(); 3353 if (lastTask != null) { 3354 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 3355 int lastTaskScroll = getScrollForPage(indexOfChild(lastTask)); 3356 if ((mIsRtl && primaryScroll < lastTaskScroll) 3357 || (!mIsRtl && primaryScroll > lastTaskScroll)) { 3358 snapToLastTask = true; 3359 } 3360 } 3361 } 3362 if (snapToLastTask) { 3363 longGridRowWidthDiff += getSnapToLastTaskScrollDiff(); 3364 } else if (isLandscapeSplit && currentPageSnapsToEndOfGrid) { 3365 // Use last task as reference point for scroll diff and snapping calculation as it's 3366 // the only invariant point in landscape split screen. 3367 snapToLastTask = true; 3368 } 3369 3370 // If we need to animate the grid to compensate the clear all gap, we split the second 3371 // half of the dismiss pending animation (in which the non-dismissed tasks slide into 3372 // place) in half again, making the first quarter the existing non-dismissal sliding 3373 // and the second quarter this new animation of gap filling. This is due to the fact 3374 // that PendingAnimation is a single animation, not a sequence of animations, so we 3375 // fake it using interpolation. 3376 if (longGridRowWidthDiff != 0) { 3377 closeGapBetweenClearAll = true; 3378 // Stagger the offsets of each additional task for a delayed animation. We use 3379 // half here as this animation is half of half of an animation (1/4th). 3380 float halfAdditionalDismissTranslationOffset = 3381 (0.5f * ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET); 3382 dismissTranslationInterpolationEnd = Utilities.boundToRange( 3383 END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET 3384 + (taskCount - 1) * halfAdditionalDismissTranslationOffset, 3385 END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1); 3386 for (int i = 0; i < taskCount; i++) { 3387 TaskView taskView = requireTaskViewAt(i); 3388 anim.setFloat(taskView, TaskView.GRID_END_TRANSLATION_X, longGridRowWidthDiff, 3389 clampToProgress(LINEAR, dismissTranslationInterpolationEnd, 1)); 3390 dismissTranslationInterpolationEnd = Utilities.boundToRange( 3391 dismissTranslationInterpolationEnd 3392 - halfAdditionalDismissTranslationOffset, 3393 END_DISMISS_TRANSLATION_INTERPOLATION_OFFSET, 1); 3394 if (mEnableDrawingLiveTile && taskView.isRunningTask()) { 3395 anim.addOnFrameCallback(() -> { 3396 runActionOnRemoteHandles( 3397 remoteTargetHandle -> 3398 remoteTargetHandle.getTaskViewSimulator() 3399 .taskPrimaryTranslation.value = 3400 TaskView.GRID_END_TRANSLATION_X.get(taskView)); 3401 redrawLiveTile(); 3402 }); 3403 } 3404 } 3405 3406 // Change alpha of clear all if translating grid to hide it 3407 if (isClearAllHidden) { 3408 anim.setFloat(mClearAllButton, DISMISS_ALPHA, 0, LINEAR); 3409 anim.addListener(new AnimatorListenerAdapter() { 3410 @Override 3411 public void onAnimationEnd(Animator animation) { 3412 super.onAnimationEnd(animation); 3413 mClearAllButton.setDismissAlpha(1); 3414 } 3415 }); 3416 } 3417 } 3418 } 3419 3420 SplitAnimationTimings splitTimings = 3421 AnimUtils.getDeviceOverviewToSplitTimings(mActivity.getDeviceProfile().isTablet); 3422 3423 int distanceFromDismissedTask = 0; 3424 for (int i = 0; i < count; i++) { 3425 View child = getChildAt(i); 3426 if (child == dismissedTaskView) { 3427 if (animateTaskView) { 3428 if (dismissingForSplitSelection) { 3429 createInitialSplitSelectAnimation(anim); 3430 } else { 3431 addDismissedTaskAnimations(dismissedTaskView, duration, anim); 3432 } 3433 } 3434 } else if (!showAsGrid) { 3435 // Compute scroll offsets from task dismissal for animation. 3436 // If we just take newScroll - oldScroll, everything to the right of dragged task 3437 // translates to the left. We need to offset this in some cases: 3438 // - In RTL, add page offset to all pages, since we want pages to move to the right 3439 // Additionally, add a page offset if: 3440 // - Current page is rightmost page (leftmost for RTL) 3441 // - Dragging an adjacent page on the left side (right side for RTL) 3442 int offset = mIsRtl ? scrollDiffPerPage : 0; 3443 if (mCurrentPage == dismissedIndex) { 3444 int lastPage = taskCount - 1; 3445 if (mCurrentPage == lastPage) { 3446 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; 3447 } 3448 } else { 3449 // Dismissing an adjacent page. 3450 int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) 3451 if (dismissedIndex == negativeAdjacent) { 3452 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; 3453 } 3454 } 3455 3456 int scrollDiff = newScroll[i] - oldScroll[i] + offset; 3457 if (scrollDiff != 0) { 3458 FloatProperty translationProperty = child instanceof TaskView 3459 ? ((TaskView) child).getPrimaryDismissTranslationProperty() 3460 : mOrientationHandler.getPrimaryViewTranslate(); 3461 3462 float additionalDismissDuration = 3463 ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs( 3464 i - dismissedIndex); 3465 3466 // We are in non-grid layout. 3467 // If dismissing for split select, use split timings. 3468 // If not, use dismiss timings. 3469 float animationStartProgress = isSplitSelectionActive() 3470 ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f) 3471 : Utilities.boundToRange( 3472 INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET 3473 + additionalDismissDuration, 0f, 1f); 3474 3475 float animationEndProgress = isSplitSelectionActive() 3476 ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset() 3477 + splitTimings.getGridSlideDurationOffset(), 0f, 1f) 3478 : 1f; 3479 3480 // Slide tiles in horizontally to fill dismissed area 3481 anim.setFloat(child, translationProperty, scrollDiff, 3482 clampToProgress( 3483 splitTimings.getGridSlidePrimaryInterpolator(), 3484 animationStartProgress, 3485 animationEndProgress 3486 ) 3487 ); 3488 3489 if (mEnableDrawingLiveTile && child instanceof TaskView 3490 && ((TaskView) child).isRunningTask()) { 3491 anim.addOnFrameCallback(() -> { 3492 runActionOnRemoteHandles( 3493 remoteTargetHandle -> 3494 remoteTargetHandle.getTaskViewSimulator() 3495 .taskPrimaryTranslation.value = 3496 mOrientationHandler.getPrimaryValue( 3497 child.getTranslationX(), 3498 child.getTranslationY() 3499 )); 3500 redrawLiveTile(); 3501 }); 3502 } 3503 needsCurveUpdates = true; 3504 } 3505 } else if (child instanceof TaskView) { 3506 TaskView taskView = (TaskView) child; 3507 if (isFocusedTaskDismissed) { 3508 if (nextFocusedTaskView != null && 3509 !isSameGridRow(taskView, nextFocusedTaskView)) { 3510 continue; 3511 } 3512 } else { 3513 if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) { 3514 continue; 3515 } 3516 } 3517 // Animate task with index >= dismissed index and in the same row as the 3518 // dismissed index or next focused index. Offset successive task dismissal 3519 // durations for a staggered effect. 3520 distanceFromDismissedTask++; 3521 int staggerColumn = isStagingFocusedTask 3522 ? (int) Math.ceil(distanceFromDismissedTask / 2f) 3523 : distanceFromDismissedTask; 3524 // Set timings based on if user is initiating splitscreen on the focused task, 3525 // or splitting/dismissing some other task. 3526 float animationStartProgress = isStagingFocusedTask 3527 ? Utilities.boundToRange( 3528 splitTimings.getGridSlideStartOffset() 3529 + (splitTimings.getGridSlideStaggerOffset() 3530 * staggerColumn), 3531 0f, 3532 dismissTranslationInterpolationEnd) 3533 : Utilities.boundToRange( 3534 INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET 3535 + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET 3536 * staggerColumn, 0f, dismissTranslationInterpolationEnd); 3537 float animationEndProgress = isStagingFocusedTask 3538 ? Utilities.boundToRange( 3539 splitTimings.getGridSlideStartOffset() 3540 + (splitTimings.getGridSlideStaggerOffset() * staggerColumn) 3541 + splitTimings.getGridSlideDurationOffset(), 3542 0f, 3543 dismissTranslationInterpolationEnd) 3544 : dismissTranslationInterpolationEnd; 3545 Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR; 3546 3547 if (taskView == nextFocusedTaskView) { 3548 // Enlarge the task to be focused next, and translate into focus position. 3549 float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width(); 3550 anim.setFloat(taskView, TaskView.SNAPSHOT_SCALE, scale, 3551 clampToProgress(LINEAR, animationStartProgress, 3552 dismissTranslationInterpolationEnd)); 3553 anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), 3554 mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth, 3555 clampToProgress(LINEAR, animationStartProgress, 3556 dismissTranslationInterpolationEnd)); 3557 float secondaryTranslation = -mTaskGridVerticalDiff; 3558 if (!nextFocusedTaskFromTop) { 3559 secondaryTranslation -= mTopBottomRowHeightDiff; 3560 } 3561 anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(), 3562 secondaryTranslation, clampToProgress(LINEAR, animationStartProgress, 3563 dismissTranslationInterpolationEnd)); 3564 anim.setFloat(taskView, TaskView.FOCUS_TRANSITION, 0f, 3565 clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT)); 3566 } else { 3567 float primaryTranslation = 3568 nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth; 3569 if (isStagingFocusedTask) { 3570 // Moves less if focused task is not in scroll position. 3571 int focusedTaskScroll = getScrollForPage(dismissedIndex); 3572 int primaryScroll = mOrientationHandler.getPrimaryScroll(this); 3573 int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll; 3574 primaryTranslation += 3575 mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff; 3576 } 3577 3578 anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(), 3579 mIsRtl ? primaryTranslation : -primaryTranslation, 3580 clampToProgress(dismissInterpolator, animationStartProgress, 3581 animationEndProgress)); 3582 } 3583 } 3584 } 3585 3586 if (needsCurveUpdates) { 3587 anim.addOnFrameCallback(this::updateCurveProperties); 3588 } 3589 3590 // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant 3591 // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we 3592 // want the dragged task to stay above all other views. 3593 if (animateTaskView) { 3594 dismissedTaskView.setTranslationZ(0.1f); 3595 } 3596 3597 mPendingAnimation = anim; 3598 final TaskView finalNextFocusedTaskView = nextFocusedTaskView; 3599 final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll; 3600 final boolean finalSnapToLastTask = snapToLastTask; 3601 final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed; 3602 mPendingAnimation.addEndListener(new Consumer<Boolean>() { 3603 @Override 3604 public void accept(Boolean success) { 3605 if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) { 3606 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, 3607 () -> onEnd(success)); 3608 } else { 3609 onEnd(success); 3610 } 3611 } 3612 3613 @SuppressWarnings("WrongCall") 3614 private void onEnd(boolean success) { 3615 // Reset task translations as they may have updated via animations in 3616 // createTaskDismissAnimation 3617 resetTaskVisuals(); 3618 3619 if (success) { 3620 if (shouldRemoveTask) { 3621 if (dismissedTaskView.getTask() != null) { 3622 if (dismissedTaskView.isRunningTask()) { 3623 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, 3624 () -> removeTaskInternal(dismissedTaskViewId)); 3625 } else { 3626 removeTaskInternal(dismissedTaskViewId); 3627 } 3628 announceForAccessibility( 3629 getResources().getString(R.string.task_view_closed)); 3630 mActivity.getStatsLogManager().logger() 3631 .withItemInfo(dismissedTaskView.getItemInfo()) 3632 .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); 3633 } 3634 } 3635 3636 int pageToSnapTo = mCurrentPage; 3637 mCurrentPageScrollDiff = 0; 3638 int taskViewIdToSnapTo = -1; 3639 if (showAsGrid) { 3640 if (finalCloseGapBetweenClearAll) { 3641 if (finalSnapToLastTask) { 3642 // Last task will be determined after removing dismissed task. 3643 pageToSnapTo = -1; 3644 } else if (taskCount > 2) { 3645 pageToSnapTo = indexOfChild(mClearAllButton); 3646 } else if (isClearAllHidden) { 3647 // Snap to focused task if clear all is hidden. 3648 pageToSnapTo = 0; 3649 } 3650 } else { 3651 // Get the id of the task view we will snap to based on the current 3652 // page's relative position as the order of indices change over time due 3653 // to dismissals. 3654 TaskView snappedTaskView = getTaskViewAt(mCurrentPage); 3655 boolean calculateScrollDiff = true; 3656 if (snappedTaskView != null && !finalSnapToLastTask) { 3657 if (snappedTaskView.getTaskViewId() == mFocusedTaskViewId) { 3658 if (finalNextFocusedTaskView != null) { 3659 taskViewIdToSnapTo = 3660 finalNextFocusedTaskView.getTaskViewId(); 3661 } else if (dismissedTaskViewId != mFocusedTaskViewId) { 3662 taskViewIdToSnapTo = mFocusedTaskViewId; 3663 } else { 3664 // Won't focus next task in split select, so snap to the 3665 // first task. 3666 pageToSnapTo = 0; 3667 calculateScrollDiff = false; 3668 } 3669 } else { 3670 int snappedTaskViewId = snappedTaskView.getTaskViewId(); 3671 boolean isSnappedTaskInTopRow = mTopRowIdSet.contains( 3672 snappedTaskViewId); 3673 IntArray taskViewIdArray = 3674 isSnappedTaskInTopRow ? getTopRowIdArray() 3675 : getBottomRowIdArray(); 3676 int snappedIndex = taskViewIdArray.indexOf(snappedTaskViewId); 3677 taskViewIdArray.removeValue(dismissedTaskViewId); 3678 if (finalNextFocusedTaskView != null) { 3679 taskViewIdArray.removeValue( 3680 finalNextFocusedTaskView.getTaskViewId()); 3681 } 3682 if (snappedIndex < taskViewIdArray.size()) { 3683 taskViewIdToSnapTo = taskViewIdArray.get(snappedIndex); 3684 } else if (snappedIndex == taskViewIdArray.size()) { 3685 // If the snapped task is the last item from the 3686 // dismissed row, 3687 // snap to the same column in the other grid row 3688 IntArray inverseRowTaskViewIdArray = 3689 isSnappedTaskInTopRow ? getBottomRowIdArray() 3690 : getTopRowIdArray(); 3691 if (snappedIndex < inverseRowTaskViewIdArray.size()) { 3692 taskViewIdToSnapTo = inverseRowTaskViewIdArray.get( 3693 snappedIndex); 3694 } 3695 } 3696 } 3697 } 3698 3699 if (calculateScrollDiff) { 3700 int primaryScroll = mOrientationHandler.getPrimaryScroll( 3701 RecentsView.this); 3702 int currentPageScroll = getScrollForPage(mCurrentPage); 3703 mCurrentPageScrollDiff = primaryScroll - currentPageScroll; 3704 } 3705 } 3706 } else if (dismissedIndex < pageToSnapTo || pageToSnapTo == taskCount - 1) { 3707 pageToSnapTo--; 3708 } 3709 boolean isHomeTaskDismissed = dismissedTaskView == getHomeTaskView(); 3710 removeViewInLayout(dismissedTaskView); 3711 mTopRowIdSet.remove(dismissedTaskViewId); 3712 3713 if (taskCount == 1) { 3714 removeViewInLayout(mClearAllButton); 3715 if (isHomeTaskDismissed) { 3716 updateEmptyMessage(); 3717 } else if (!mSplitSelectStateController.isSplitSelectActive()) { 3718 startHome(); 3719 } 3720 } else { 3721 // Update focus task and its size. 3722 if (finalIsFocusedTaskDismissed && finalNextFocusedTaskView != null) { 3723 mFocusedTaskViewId = ENABLE_GRID_ONLY_OVERVIEW.get() 3724 ? INVALID_TASK_ID 3725 : finalNextFocusedTaskView.getTaskViewId(); 3726 mTopRowIdSet.remove(mFocusedTaskViewId); 3727 finalNextFocusedTaskView.animateIconScaleAndDimIntoView(); 3728 } 3729 updateTaskSize(/*isTaskDismissal=*/ true); 3730 updateChildTaskOrientations(); 3731 // Update scroll and snap to page. 3732 updateScrollSynchronously(); 3733 3734 if (showAsGrid) { 3735 // Rebalance tasks in the grid 3736 int highestVisibleTaskIndex = getHighestVisibleTaskIndex(); 3737 if (highestVisibleTaskIndex < Integer.MAX_VALUE) { 3738 TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex); 3739 3740 boolean shouldRebalance; 3741 int screenStart = mOrientationHandler.getPrimaryScroll( 3742 RecentsView.this); 3743 int taskStart = mOrientationHandler.getChildStart(taskView) 3744 + (int) taskView.getOffsetAdjustment(/*gridEnabled=*/ true); 3745 3746 // Rebalance only if there is a maximum gap between the task and the 3747 // screen's edge; this ensures that rebalanced tasks are outside the 3748 // visible screen. 3749 if (mIsRtl) { 3750 shouldRebalance = taskStart <= screenStart + mPageSpacing; 3751 } else { 3752 int screenEnd = 3753 screenStart + mOrientationHandler.getMeasuredSize( 3754 RecentsView.this); 3755 int taskSize = (int) (mOrientationHandler.getMeasuredSize( 3756 taskView) * taskView 3757 .getSizeAdjustment(/*fullscreenEnabled=*/false)); 3758 int taskEnd = taskStart + taskSize; 3759 3760 shouldRebalance = taskEnd >= screenEnd - mPageSpacing; 3761 } 3762 3763 if (shouldRebalance) { 3764 updateGridProperties(/*isTaskDismissal=*/ true, 3765 highestVisibleTaskIndex); 3766 updateScrollSynchronously(); 3767 } 3768 } 3769 3770 IntArray topRowIdArray = getTopRowIdArray(); 3771 IntArray bottomRowIdArray = getBottomRowIdArray(); 3772 if (finalSnapToLastTask) { 3773 // If snapping to last task, find the last task after dismissal. 3774 pageToSnapTo = indexOfChild( 3775 getLastGridTaskView(topRowIdArray, bottomRowIdArray)); 3776 } else if (taskViewIdToSnapTo != -1) { 3777 // If snapping to another page due to indices rearranging, find 3778 // the new index after dismissal & rearrange using the task view id. 3779 pageToSnapTo = indexOfChild( 3780 getTaskViewFromTaskViewId(taskViewIdToSnapTo)); 3781 if (!currentPageSnapsToEndOfGrid) { 3782 // If it wasn't snapped to one of the last pages, but is now 3783 // snapped to last pages, we'll need to compensate for the 3784 // offset from the page's scroll to its visual position. 3785 mCurrentPageScrollDiff += getOffsetFromScrollPosition( 3786 pageToSnapTo, topRowIdArray, bottomRowIdArray); 3787 } 3788 } 3789 } 3790 pageBeginTransition(); 3791 setCurrentPage(pageToSnapTo); 3792 // Update various scroll-dependent UI. 3793 dispatchScrollChanged(); 3794 updateActionsViewFocusedScroll(); 3795 if (isClearAllHidden() && !mActivity.getDeviceProfile().isTablet) { 3796 mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, 3797 false); 3798 } 3799 } 3800 } 3801 updateCurrentTaskActionsVisibility(); 3802 onDismissAnimationEnds(); 3803 mPendingAnimation = null; 3804 } 3805 }); 3806 } 3807 3808 /** 3809 * Hides all overview actions if current page is for split apps, shows otherwise 3810 * If actions are showing, we only show split option if 3811 * * Device is large screen 3812 * * There are at least 2 tasks to invoke split 3813 */ updateCurrentTaskActionsVisibility()3814 private void updateCurrentTaskActionsVisibility() { 3815 boolean isCurrentSplit = getCurrentPageTaskView() instanceof GroupedTaskView; 3816 mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit); 3817 mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive()); 3818 mActionsView.updateSplitButtonHiddenFlags(FLAG_IS_NOT_TABLET, 3819 !mActivity.getDeviceProfile().isTablet); 3820 mActionsView.updateSplitButtonDisabledFlags(FLAG_SINGLE_TASK, /*enable=*/ false); 3821 if (DESKTOP_MODE_SUPPORTED) { 3822 boolean isCurrentDesktop = getCurrentPageTaskView() instanceof DesktopTaskView; 3823 mActionsView.updateHiddenFlags(HIDDEN_DESKTOP, isCurrentDesktop); 3824 } 3825 } 3826 3827 /** 3828 * Returns all the tasks in the top row, without the focused task 3829 */ getTopRowIdArray()3830 private IntArray getTopRowIdArray() { 3831 if (mTopRowIdSet.isEmpty()) { 3832 return new IntArray(0); 3833 } 3834 IntArray topArray = new IntArray(mTopRowIdSet.size()); 3835 int taskViewCount = getTaskViewCount(); 3836 for (int i = 0; i < taskViewCount; i++) { 3837 int taskViewId = requireTaskViewAt(i).getTaskViewId(); 3838 if (mTopRowIdSet.contains(taskViewId)) { 3839 topArray.add(taskViewId); 3840 } 3841 } 3842 return topArray; 3843 } 3844 3845 /** 3846 * Returns all the tasks in the bottom row, without the focused task 3847 */ getBottomRowIdArray()3848 private IntArray getBottomRowIdArray() { 3849 int bottomRowIdArraySize = getBottomRowTaskCountForTablet(); 3850 if (bottomRowIdArraySize <= 0) { 3851 return new IntArray(0); 3852 } 3853 IntArray bottomArray = new IntArray(bottomRowIdArraySize); 3854 int taskViewCount = getTaskViewCount(); 3855 for (int i = 0; i < taskViewCount; i++) { 3856 int taskViewId = requireTaskViewAt(i).getTaskViewId(); 3857 if (!mTopRowIdSet.contains(taskViewId) && taskViewId != mFocusedTaskViewId) { 3858 bottomArray.add(taskViewId); 3859 } 3860 } 3861 return bottomArray; 3862 } 3863 3864 /** 3865 * Iterate the grid by columns instead of by TaskView index, starting after the focused task and 3866 * up to the last balanced column. 3867 * 3868 * @return the highest visible TaskView index between both rows 3869 */ getHighestVisibleTaskIndex()3870 private int getHighestVisibleTaskIndex() { 3871 if (mTopRowIdSet.isEmpty()) return Integer.MAX_VALUE; // return earlier 3872 3873 int lastVisibleIndex = Integer.MAX_VALUE; 3874 IntArray topRowIdArray = getTopRowIdArray(); 3875 IntArray bottomRowIdArray = getBottomRowIdArray(); 3876 int balancedColumns = Math.min(bottomRowIdArray.size(), topRowIdArray.size()); 3877 3878 for (int i = 0; i < balancedColumns; i++) { 3879 TaskView topTask = getTaskViewFromTaskViewId(topRowIdArray.get(i)); 3880 3881 if (isTaskViewVisible(topTask)) { 3882 TaskView bottomTask = getTaskViewFromTaskViewId(bottomRowIdArray.get(i)); 3883 lastVisibleIndex = Math.max(indexOfChild(topTask), indexOfChild(bottomTask)); 3884 } else if (lastVisibleIndex < Integer.MAX_VALUE) { 3885 break; 3886 } 3887 } 3888 3889 return lastVisibleIndex; 3890 } 3891 removeTaskInternal(int dismissedTaskViewId)3892 private void removeTaskInternal(int dismissedTaskViewId) { 3893 int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId); 3894 int primaryTaskId = taskIds[0]; 3895 int secondaryTaskId = taskIds[1]; 3896 UI_HELPER_EXECUTOR.getHandler().postDelayed( 3897 () -> { 3898 ActivityManagerWrapper.getInstance().removeTask(primaryTaskId); 3899 if (secondaryTaskId != -1) { 3900 ActivityManagerWrapper.getInstance().removeTask(secondaryTaskId); 3901 } 3902 }, 3903 REMOVE_TASK_WAIT_FOR_APP_STOP_MS); 3904 } 3905 onDismissAnimationEnds()3906 protected void onDismissAnimationEnds() { 3907 AccessibilityManagerCompat.sendDismissAnimationEndsEventToTest(getContext()); 3908 } 3909 createAllTasksDismissAnimation(long duration)3910 public PendingAnimation createAllTasksDismissAnimation(long duration) { 3911 if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) { 3912 throw new IllegalStateException("Another pending animation is still running"); 3913 } 3914 PendingAnimation anim = new PendingAnimation(duration); 3915 3916 int count = getTaskViewCount(); 3917 for (int i = 0; i < count; i++) { 3918 addDismissedTaskAnimations(requireTaskViewAt(i), duration, anim); 3919 } 3920 3921 mPendingAnimation = anim; 3922 mPendingAnimation.addEndListener(isSuccess -> { 3923 if (isSuccess) { 3924 // Remove all the task views now 3925 finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> { 3926 UI_HELPER_EXECUTOR.getHandler().postDelayed( 3927 ActivityManagerWrapper.getInstance()::removeAllRecentTasks, 3928 REMOVE_TASK_WAIT_FOR_APP_STOP_MS); 3929 removeTasksViewsAndClearAllButton(); 3930 startHome(); 3931 }); 3932 } 3933 mPendingAnimation = null; 3934 }); 3935 return anim; 3936 } 3937 snapToPageRelative(int pageCount, int delta, boolean cycle)3938 private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) { 3939 if (pageCount == 0) { 3940 return false; 3941 } 3942 final int newPageUnbound = getNextPage() + delta; 3943 if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) { 3944 return false; 3945 } 3946 snapToPage((newPageUnbound + pageCount) % pageCount); 3947 getChildAt(getNextPage()).requestFocus(); 3948 return true; 3949 } 3950 runDismissAnimation(PendingAnimation pendingAnim)3951 private void runDismissAnimation(PendingAnimation pendingAnim) { 3952 AnimatorPlaybackController controller = pendingAnim.createPlaybackController(); 3953 controller.dispatchOnStart(); 3954 controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); 3955 controller.start(); 3956 } 3957 3958 @UiThread dismissTask(int taskId)3959 private void dismissTask(int taskId) { 3960 TaskView taskView = getTaskViewByTaskId(taskId); 3961 if (taskView == null) { 3962 return; 3963 } 3964 dismissTask(taskView, true /* animate */, false /* removeTask */); 3965 } 3966 dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask)3967 public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { 3968 PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION); 3969 createTaskDismissAnimation(pa, taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION, 3970 false /* dismissingForSplitSelection*/); 3971 runDismissAnimation(pa); 3972 } 3973 3974 @SuppressWarnings("unused") dismissAllTasks(View view)3975 private void dismissAllTasks(View view) { 3976 runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); 3977 mActivity.getStatsLogManager().logger().log(LAUNCHER_TASK_CLEAR_ALL); 3978 } 3979 dismissCurrentTask()3980 private void dismissCurrentTask() { 3981 TaskView taskView = getNextPageTaskView(); 3982 if (taskView != null) { 3983 dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/); 3984 } 3985 } 3986 3987 @Override dispatchKeyEvent(KeyEvent event)3988 public boolean dispatchKeyEvent(KeyEvent event) { 3989 if (event.getAction() == KeyEvent.ACTION_DOWN) { 3990 switch (event.getKeyCode()) { 3991 case KeyEvent.KEYCODE_TAB: 3992 return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1, 3993 event.isAltPressed() /* cycle */); 3994 case KeyEvent.KEYCODE_DPAD_RIGHT: 3995 return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */); 3996 case KeyEvent.KEYCODE_DPAD_LEFT: 3997 return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */); 3998 case KeyEvent.KEYCODE_DEL: 3999 case KeyEvent.KEYCODE_FORWARD_DEL: 4000 dismissCurrentTask(); 4001 return true; 4002 case KeyEvent.KEYCODE_NUMPAD_DOT: 4003 if (event.isAltPressed()) { 4004 // Numpad DEL pressed while holding Alt. 4005 dismissCurrentTask(); 4006 return true; 4007 } 4008 } 4009 } 4010 return super.dispatchKeyEvent(event); 4011 } 4012 4013 @Override onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect)4014 protected void onFocusChanged(boolean gainFocus, int direction, 4015 @Nullable Rect previouslyFocusedRect) { 4016 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 4017 if (gainFocus && getChildCount() > 0) { 4018 switch (direction) { 4019 case FOCUS_FORWARD: 4020 setCurrentPage(0); 4021 break; 4022 case FOCUS_BACKWARD: 4023 case FOCUS_RIGHT: 4024 case FOCUS_LEFT: 4025 setCurrentPage(getChildCount() - 1); 4026 break; 4027 } 4028 } 4029 } 4030 getContentAlpha()4031 public float getContentAlpha() { 4032 return mContentAlpha; 4033 } 4034 setContentAlpha(float alpha)4035 public void setContentAlpha(float alpha) { 4036 if (alpha == mContentAlpha) { 4037 return; 4038 } 4039 alpha = Utilities.boundToRange(alpha, 0, 1); 4040 mContentAlpha = alpha; 4041 int runningTaskId = getTaskIdsForRunningTaskView()[0]; 4042 for (int i = getTaskViewCount() - 1; i >= 0; i--) { 4043 TaskView child = requireTaskViewAt(i); 4044 int[] childTaskIds = child.getTaskIds(); 4045 if (!mRunningTaskTileHidden || 4046 (childTaskIds[0] != runningTaskId && childTaskIds[1] != runningTaskId)) { 4047 child.setStableAlpha(alpha); 4048 } 4049 } 4050 mClearAllButton.setContentAlpha(mContentAlpha); 4051 int alphaInt = Math.round(alpha * 255); 4052 mEmptyMessagePaint.setAlpha(alphaInt); 4053 mEmptyIcon.setAlpha(alphaInt); 4054 mActionsView.getContentAlpha().setValue(mContentAlpha); 4055 4056 if (alpha > 0) { 4057 setVisibility(VISIBLE); 4058 } else if (!mFreezeViewVisibility) { 4059 setVisibility(INVISIBLE); 4060 } 4061 } 4062 4063 /** 4064 * Freezes the view visibility change. When frozen, the view will not change its visibility 4065 * to gone due to alpha changes. 4066 */ setFreezeViewVisibility(boolean freezeViewVisibility)4067 public void setFreezeViewVisibility(boolean freezeViewVisibility) { 4068 if (mFreezeViewVisibility != freezeViewVisibility) { 4069 mFreezeViewVisibility = freezeViewVisibility; 4070 if (!mFreezeViewVisibility) { 4071 setVisibility(mContentAlpha > 0 ? VISIBLE : INVISIBLE); 4072 } 4073 } 4074 } 4075 4076 @Override setVisibility(int visibility)4077 public void setVisibility(int visibility) { 4078 super.setVisibility(visibility); 4079 if (mActionsView != null) { 4080 mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE); 4081 if (visibility != VISIBLE) { 4082 mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false); 4083 } 4084 } 4085 } 4086 4087 @Override onConfigurationChanged(Configuration newConfig)4088 protected void onConfigurationChanged(Configuration newConfig) { 4089 super.onConfigurationChanged(newConfig); 4090 updateRecentsRotation(); 4091 onOrientationChanged(); 4092 } 4093 4094 /** 4095 * Updates {@link RecentsOrientedState}'s cached RecentsView rotation. 4096 */ updateRecentsRotation()4097 public void updateRecentsRotation() { 4098 final int rotation = mActivity.getDisplay().getRotation(); 4099 mOrientationState.setRecentsRotation(rotation); 4100 } 4101 setLayoutRotation(int touchRotation, int displayRotation)4102 public void setLayoutRotation(int touchRotation, int displayRotation) { 4103 if (mOrientationState.update(touchRotation, displayRotation)) { 4104 updateOrientationHandler(); 4105 } 4106 } 4107 getPagedViewOrientedState()4108 public RecentsOrientedState getPagedViewOrientedState() { 4109 return mOrientationState; 4110 } 4111 getPagedOrientationHandler()4112 public PagedOrientationHandler getPagedOrientationHandler() { 4113 return mOrientationHandler; 4114 } 4115 4116 @Nullable getNextTaskView()4117 public TaskView getNextTaskView() { 4118 return getTaskViewAt(getRunningTaskIndex() + 1); 4119 } 4120 4121 @Nullable getCurrentPageTaskView()4122 public TaskView getCurrentPageTaskView() { 4123 return getTaskViewAt(getCurrentPage()); 4124 } 4125 4126 @Nullable getNextPageTaskView()4127 public TaskView getNextPageTaskView() { 4128 return getTaskViewAt(getNextPage()); 4129 } 4130 4131 @Nullable getTaskViewNearestToCenterOfScreen()4132 public TaskView getTaskViewNearestToCenterOfScreen() { 4133 return getTaskViewAt(getPageNearestToCenterOfScreen()); 4134 } 4135 4136 /** 4137 * Returns null instead of indexOutOfBoundsError when index is not in range 4138 */ 4139 @Nullable getTaskViewAt(int index)4140 public TaskView getTaskViewAt(int index) { 4141 View child = getChildAt(index); 4142 return child instanceof TaskView ? (TaskView) child : null; 4143 } 4144 4145 /** 4146 * A version of {@link #getTaskViewAt} when the caller is sure about the input index. 4147 */ 4148 @NonNull requireTaskViewAt(int index)4149 private TaskView requireTaskViewAt(int index) { 4150 return Objects.requireNonNull(getTaskViewAt(index)); 4151 } 4152 setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener)4153 public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) { 4154 mOnEmptyMessageUpdatedListener = listener; 4155 } 4156 updateEmptyMessage()4157 public void updateEmptyMessage() { 4158 boolean isEmpty = getTaskViewCount() == 0; 4159 boolean hasSizeChanged = mLastMeasureSize.x != getWidth() 4160 || mLastMeasureSize.y != getHeight(); 4161 if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { 4162 return; 4163 } 4164 setContentDescription(isEmpty ? mEmptyMessage : ""); 4165 mShowEmptyMessage = isEmpty; 4166 updateEmptyStateUi(hasSizeChanged); 4167 invalidate(); 4168 4169 if (mOnEmptyMessageUpdatedListener != null) { 4170 mOnEmptyMessageUpdatedListener.onEmptyMessageUpdated(mShowEmptyMessage); 4171 } 4172 } 4173 4174 @Override onLayout(boolean changed, int left, int top, int right, int bottom)4175 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 4176 // If we're going to a state without overview panel, avoid unnecessary onLayout that 4177 // cause TaskViews to re-arrange during animation to that state. 4178 if (!mOverviewStateEnabled && !mFirstLayout) { 4179 return; 4180 } 4181 4182 mShowAsGridLastOnLayout = showAsGrid(); 4183 4184 super.onLayout(changed, left, top, right, bottom); 4185 4186 updateEmptyStateUi(changed); 4187 4188 // Update the pivots such that when the task is scaled, it fills the full page 4189 getTaskSize(mTempRect); 4190 updatePivots(); 4191 setTaskModalness(mTaskModalness); 4192 mLastComputedTaskStartPushOutDistance = null; 4193 mLastComputedTaskEndPushOutDistance = null; 4194 updatePageOffsets(); 4195 runActionOnRemoteHandles( 4196 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() 4197 .setScroll(getScrollOffset())); 4198 setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO 4199 : IMPORTANT_FOR_ACCESSIBILITY_AUTO); 4200 } 4201 updatePivots()4202 private void updatePivots() { 4203 if (mOverviewSelectEnabled) { 4204 getModalTaskSize(mTempRect); 4205 Rect selectedTaskPosition = getSelectedTaskBounds(); 4206 4207 Utilities.getPivotsForScalingRectToRect(mTempRect, selectedTaskPosition, 4208 mTempPointF); 4209 setPivotX(mTempPointF.x); 4210 setPivotY(mTempPointF.y); 4211 } else { 4212 getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect, 4213 mActivity.getDeviceProfile(), mTempPointF); 4214 setPivotX(mTempPointF.x); 4215 setPivotY(mTempPointF.y); 4216 } 4217 } 4218 updatePageOffsets()4219 private void updatePageOffsets() { 4220 float offset = mAdjacentPageHorizontalOffset; 4221 float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness); 4222 int count = getChildCount(); 4223 boolean showAsGrid = showAsGrid(); 4224 4225 TaskView runningTask = mRunningTaskViewId == -1 || !mRunningTaskTileHidden 4226 ? null : getRunningTaskView(); 4227 int midpoint = runningTask == null ? -1 : indexOfChild(runningTask); 4228 int modalMidpoint = getCurrentPage(); 4229 boolean isModalGridWithoutFocusedTask = 4230 showAsGrid && ENABLE_GRID_ONLY_OVERVIEW.get() && mTaskModalness > 0; 4231 if (isModalGridWithoutFocusedTask) { 4232 modalMidpoint = indexOfChild(mSelectedTask); 4233 } 4234 4235 float midpointOffsetSize = 0; 4236 float leftOffsetSize = midpoint - 1 >= 0 4237 ? getHorizontalOffsetSize(midpoint - 1, midpoint, offset) 4238 : 0; 4239 float rightOffsetSize = midpoint + 1 < count 4240 ? getHorizontalOffsetSize(midpoint + 1, midpoint, offset) 4241 : 0; 4242 4243 float modalMidpointOffsetSize = 0; 4244 float modalLeftOffsetSize = 0; 4245 float modalRightOffsetSize = 0; 4246 float gridOffsetSize = 0; 4247 4248 if (showAsGrid) { 4249 // In grid, we only focus the task on the side. The reference index used for offset 4250 // calculation is the task directly next to the focus task in the grid. 4251 int referenceIndex = modalMidpoint == 0 ? 1 : 0; 4252 gridOffsetSize = referenceIndex < count 4253 ? getHorizontalOffsetSize(referenceIndex, modalMidpoint, modalOffset) 4254 : 0; 4255 } else { 4256 modalLeftOffsetSize = modalMidpoint - 1 >= 0 4257 ? getHorizontalOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset) 4258 : 0; 4259 modalRightOffsetSize = modalMidpoint + 1 < count 4260 ? getHorizontalOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset) 4261 : 0; 4262 } 4263 4264 for (int i = 0; i < count; i++) { 4265 float translation = i == midpoint 4266 ? midpointOffsetSize 4267 : i < midpoint 4268 ? leftOffsetSize 4269 : rightOffsetSize; 4270 if (isModalGridWithoutFocusedTask) { 4271 gridOffsetSize = getHorizontalOffsetSize(i, modalMidpoint, modalOffset); 4272 gridOffsetSize = Math.abs(gridOffsetSize) * (i <= modalMidpoint ? 1 : -1); 4273 } 4274 float modalTranslation = i == modalMidpoint 4275 ? modalMidpointOffsetSize 4276 : showAsGrid 4277 ? gridOffsetSize 4278 : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize; 4279 float totalTranslationX = translation + modalTranslation; 4280 View child = getChildAt(i); 4281 FloatProperty translationPropertyX = child instanceof TaskView 4282 ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty() 4283 : mOrientationHandler.getPrimaryViewTranslate(); 4284 translationPropertyX.set(child, totalTranslationX); 4285 if (mEnableDrawingLiveTile && i == getRunningTaskIndex()) { 4286 runActionOnRemoteHandles( 4287 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() 4288 .taskPrimaryTranslation.value = totalTranslationX); 4289 redrawLiveTile(); 4290 } 4291 4292 if (showAsGrid && ENABLE_GRID_ONLY_OVERVIEW.get() && child instanceof TaskView) { 4293 float totalTranslationY = getVerticalOffsetSize(i, modalOffset); 4294 FloatProperty translationPropertyY = 4295 ((TaskView) child).getSecondaryTaskOffsetTranslationProperty(); 4296 translationPropertyY.set(child, totalTranslationY); 4297 } 4298 } 4299 updateCurveProperties(); 4300 } 4301 4302 /** 4303 * Computes the child position with persistent translation considered (see 4304 * {@link TaskView#getPersistentTranslationX()}. 4305 */ 4306 private void getPersistentChildPosition(int childIndex, int midPointScroll, RectF outRect) { 4307 View child = getChildAt(childIndex); 4308 outRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom()); 4309 if (child instanceof TaskView) { 4310 TaskView taskView = (TaskView) child; 4311 outRect.offset(taskView.getPersistentTranslationX(), 4312 taskView.getPersistentTranslationY()); 4313 outRect.top += mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx; 4314 4315 mTempMatrix.reset(); 4316 float persistentScale = taskView.getPersistentScale(); 4317 mTempMatrix.postScale(persistentScale, persistentScale, 4318 mIsRtl ? outRect.right : outRect.left, outRect.top); 4319 mTempMatrix.mapRect(outRect); 4320 } 4321 outRect.offset(mOrientationHandler.getPrimaryValue(-midPointScroll, 0), 4322 mOrientationHandler.getSecondaryValue(-midPointScroll, 0)); 4323 } 4324 4325 /** 4326 * Computes the distance to offset the given child such that it is completely offscreen when 4327 * translating away from the given midpoint. 4328 * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen. 4329 */ 4330 private float getHorizontalOffsetSize(int childIndex, int midpointIndex, float offsetProgress) { 4331 if (offsetProgress == 0) { 4332 // Don't bother calculating everything below if we won't offset anyway. 4333 return 0; 4334 } 4335 4336 // First, get the position of the task relative to the midpoint. If there is no midpoint 4337 // then we just use the normal (centered) task position. 4338 RectF taskPosition = mTempRectF; 4339 // Whether the task should be shifted to start direction (i.e. left edge for portrait, top 4340 // edge for landscape/seascape). 4341 boolean isStartShift; 4342 if (midpointIndex > -1) { 4343 // When there is a midpoint reference task, adjacent tasks have less distance to travel 4344 // to reach offscreen. Offset the task position to the task's starting point, and offset 4345 // by current page's scroll diff. 4346 int midpointScroll = getScrollForPage(midpointIndex) 4347 + mOrientationHandler.getPrimaryScroll(this) - getScrollForPage(mCurrentPage); 4348 4349 getPersistentChildPosition(midpointIndex, midpointScroll, taskPosition); 4350 float midpointStart = mOrientationHandler.getStart(taskPosition); 4351 4352 getPersistentChildPosition(childIndex, midpointScroll, taskPosition); 4353 // Assume child does not overlap with midPointChild. 4354 isStartShift = mOrientationHandler.getStart(taskPosition) < midpointStart; 4355 } else { 4356 // Position the task at scroll position. 4357 getPersistentChildPosition(childIndex, getScrollForPage(childIndex), taskPosition); 4358 isStartShift = mIsRtl; 4359 } 4360 4361 // Next, calculate the distance to move the task off screen. We also need to account for 4362 // RecentsView scale, because it moves tasks based on its pivot. To do this, we move the 4363 // task position to where it would be offscreen at scale = 1 (computed above), then we 4364 // apply the scale via getMatrix() to determine how much that moves the task from its 4365 // desired position, and adjust the computed distance accordingly. 4366 float distanceToOffscreen; 4367 if (isStartShift) { 4368 float desiredStart = -mOrientationHandler.getPrimarySize(taskPosition); 4369 distanceToOffscreen = -mOrientationHandler.getEnd(taskPosition); 4370 if (mLastComputedTaskStartPushOutDistance == null) { 4371 taskPosition.offsetTo( 4372 mOrientationHandler.getPrimaryValue(desiredStart, 0f), 4373 mOrientationHandler.getSecondaryValue(desiredStart, 0f)); 4374 getMatrix().mapRect(taskPosition); 4375 mLastComputedTaskStartPushOutDistance = mOrientationHandler.getEnd(taskPosition) 4376 / mOrientationHandler.getPrimaryScale(this); 4377 } 4378 distanceToOffscreen -= mLastComputedTaskStartPushOutDistance; 4379 } else { 4380 float desiredStart = mOrientationHandler.getPrimarySize(this); 4381 distanceToOffscreen = desiredStart - mOrientationHandler.getStart(taskPosition); 4382 if (mLastComputedTaskEndPushOutDistance == null) { 4383 taskPosition.offsetTo( 4384 mOrientationHandler.getPrimaryValue(desiredStart, 0f), 4385 mOrientationHandler.getSecondaryValue(desiredStart, 0f)); 4386 getMatrix().mapRect(taskPosition); 4387 mLastComputedTaskEndPushOutDistance = (mOrientationHandler.getStart(taskPosition) 4388 - desiredStart) / mOrientationHandler.getPrimaryScale(this); 4389 } 4390 distanceToOffscreen -= mLastComputedTaskEndPushOutDistance; 4391 } 4392 return distanceToOffscreen * offsetProgress; 4393 } 4394 4395 /** 4396 * Computes the vertical distance to offset a given child such that it is completely offscreen. 4397 * 4398 * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen. 4399 */ 4400 private float getVerticalOffsetSize(int childIndex, float offsetProgress) { 4401 if (offsetProgress == 0 || !(showAsGrid() && ENABLE_GRID_ONLY_OVERVIEW.get()) 4402 || mSelectedTask == null) { 4403 // Don't bother calculating everything below if we won't offset vertically. 4404 return 0; 4405 } 4406 4407 // First, get the position of the task relative to the top row. 4408 TaskView child = getTaskViewAt(childIndex); 4409 Rect taskPosition = getTaskBounds(child); 4410 4411 boolean isSelectedTaskTopRow = mTopRowIdSet.contains(mSelectedTask.getTaskViewId()); 4412 boolean isChildTopRow = mTopRowIdSet.contains(child.getTaskViewId()); 4413 // Whether the task should be shifted to the top. 4414 boolean isTopShift = !isSelectedTaskTopRow && isChildTopRow; 4415 boolean isBottomShift = isSelectedTaskTopRow && !isChildTopRow; 4416 4417 // Next, calculate the distance to move the task off screen at scale = 1. 4418 float distanceToOffscreen = 0; 4419 if (isTopShift) { 4420 distanceToOffscreen = -taskPosition.bottom; 4421 } else if (isBottomShift) { 4422 distanceToOffscreen = mActivity.getDeviceProfile().heightPx - taskPosition.top; 4423 } 4424 return distanceToOffscreen * offsetProgress; 4425 } 4426 4427 protected void setTaskViewsResistanceTranslation(float translation) { 4428 mTaskViewsSecondaryTranslation = translation; 4429 for (int i = 0; i < getTaskViewCount(); i++) { 4430 TaskView task = requireTaskViewAt(i); 4431 task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY()); 4432 } 4433 runActionOnRemoteHandles( 4434 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() 4435 .recentsViewSecondaryTranslation.value = translation); 4436 } 4437 4438 private void updateTaskViewsSnapshotRadius() { 4439 for (int i = 0; i < getTaskViewCount(); i++) { 4440 requireTaskViewAt(i).updateSnapshotRadius(); 4441 } 4442 } 4443 4444 protected void setTaskViewsPrimarySplitTranslation(float translation) { 4445 mTaskViewsPrimarySplitTranslation = translation; 4446 for (int i = 0; i < getTaskViewCount(); i++) { 4447 TaskView task = requireTaskViewAt(i); 4448 task.getPrimarySplitTranslationProperty().set(task, translation); 4449 } 4450 } 4451 4452 protected void setTaskViewsSecondarySplitTranslation(float translation) { 4453 mTaskViewsSecondarySplitTranslation = translation; 4454 for (int i = 0; i < getTaskViewCount(); i++) { 4455 TaskView taskView = requireTaskViewAt(i); 4456 if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) { 4457 continue; 4458 } 4459 taskView.getSecondarySplitTranslationProperty().set(taskView, translation); 4460 } 4461 } 4462 4463 /** 4464 * Resets the visuals when exit modal state. 4465 */ 4466 public void resetModalVisuals() { 4467 if (mSelectedTask != null) { 4468 mSelectedTask.getThumbnail().getTaskOverlay().resetModalVisuals(); 4469 } 4470 } 4471 4472 /** 4473 * Primarily used by overview actions to initiate split from focused task, logs the source 4474 * of split invocation as such. 4475 */ 4476 public void initiateSplitSelect(TaskView taskView) { 4477 int defaultSplitPosition = mOrientationHandler 4478 .getDefaultSplitPosition(mActivity.getDeviceProfile()); 4479 initiateSplitSelect(taskView, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT); 4480 } 4481 4482 /** TODO(b/266477929): Consolidate this call w/ the one below */ 4483 public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition, 4484 StatsLogManager.EventEnum splitEvent) { 4485 mSplitHiddenTaskView = taskView; 4486 mSplitSelectStateController.setInitialTaskSelect(null /*intent*/, 4487 stagePosition, taskView.getItemInfo(), splitEvent, taskView.mTask.key.id); 4488 mSplitSelectStateController.setAnimateCurrentTaskDismissal( 4489 true /*animateCurrentTaskDismissal*/); 4490 mSplitHiddenTaskViewIndex = indexOfChild(taskView); 4491 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 4492 updateDesktopTaskVisibility(false /* visible */); 4493 } 4494 } 4495 4496 /** 4497 * Called when staging a split from Home/AllApps/Overview (Taskbar), 4498 * using the icon long-press menu. 4499 * Attempts to initiate split with an existing taskView, if one exists 4500 */ 4501 public void initiateSplitSelect(SplitSelectSource splitSelectSource) { 4502 TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "enterSplitSelect"); 4503 mSplitSelectSource = splitSelectSource; 4504 mSplitHiddenTaskView = getTaskViewByTaskId(splitSelectSource.alreadyRunningTaskId); 4505 mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView); 4506 mSplitSelectStateController 4507 .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal); 4508 4509 // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair 4510 mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null 4511 && mSplitHiddenTaskView.containsMultipleTasks()); 4512 mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent, 4513 splitSelectSource.position.stagePosition, splitSelectSource.itemInfo, 4514 splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId); 4515 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 4516 updateDesktopTaskVisibility(false /* visible */); 4517 } 4518 } 4519 4520 private void updateDesktopTaskVisibility(boolean visible) { 4521 if (mDesktopTaskView != null) { 4522 mDesktopTaskView.setVisibility(visible ? VISIBLE : GONE); 4523 } 4524 } 4525 4526 /** 4527 * Modifies a PendingAnimation with the animations for entering split staging 4528 */ 4529 public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) { 4530 boolean isInitiatingSplitFromTaskView = 4531 mSplitSelectStateController.isAnimateCurrentTaskDismissal(); 4532 boolean isInitiatingTaskViewSplitPair = 4533 mSplitSelectStateController.isDismissingFromSplitPair(); 4534 if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) { 4535 // Splitting from Overview for split pair task 4536 createInitialSplitSelectAnimation(builder); 4537 4538 // Animate pair thumbnail into full thumbnail 4539 boolean primaryTaskSelected = 4540 mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id == 4541 mSplitSelectStateController.getInitialTaskId(); 4542 TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView 4543 .getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0]; 4544 TaskThumbnailView thumbnail = taskIdAttributeContainer.getThumbnailView(); 4545 mSplitSelectStateController.getSplitAnimationController() 4546 .addInitialSplitFromPair(taskIdAttributeContainer, builder, 4547 mActivity.getDeviceProfile(), 4548 mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(), 4549 primaryTaskSelected); 4550 builder.addOnFrameCallback(() ->{ 4551 thumbnail.refreshSplashView(); 4552 mSplitHiddenTaskView.updateSnapshotRadius(); 4553 }); 4554 } else if (isInitiatingSplitFromTaskView) { 4555 // Splitting from Overview for fullscreen task 4556 createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration, 4557 true /* dismissingForSplitSelection*/); 4558 } else { 4559 // Splitting from Home 4560 createInitialSplitSelectAnimation(builder); 4561 } 4562 } 4563 4564 /** 4565 * Confirms the selection of the next split task. The extra data is passed through because the 4566 * user may be selecting a subtask in a group. 4567 * 4568 * @param containerTaskView If our second selected app is currently running in Recents, this is 4569 * the "container" TaskView from Recents. If we are starting a fresh 4570 * instance of the app from an Intent, this will be null. 4571 * @param task The Task corresponding to our second selected app. If we are starting a fresh 4572 * instance of the app from an Intent, this will be null. 4573 * @param drawable The Drawable corresponding to our second selected app's icon. 4574 * @param secondView The View representing the current space on the screen where the second app 4575 * is (either the ThumbnailView or the tapped icon). 4576 * @param intent If we are launching a fresh instance of the app, this is the Intent for it. If 4577 * the second app is already running in Recents, this will be null. 4578 * @param user If we are launching a fresh instance of the app, this is the UserHandle for it. 4579 * If the second app is already running in Recents, this will be null. 4580 * @return true if waiting for confirmation of second app or if split animations are running, 4581 * false otherwise 4582 */ 4583 public boolean confirmSplitSelect(TaskView containerTaskView, Task task, Drawable drawable, 4584 View secondView, @Nullable Bitmap thumbnail, Intent intent, UserHandle user) { 4585 if (canLaunchFullscreenTask()) { 4586 return false; 4587 } 4588 if (mSplitSelectStateController.isBothSplitAppsConfirmed()) { 4589 return true; 4590 } 4591 // Second task is selected either as an already-running Task or an Intent 4592 if (task != null) { 4593 if (!task.isDockable) { 4594 // Task does not support split screen 4595 mSplitUnsupportedToast.show(); 4596 return true; 4597 } 4598 mSplitSelectStateController.setSecondTask(task); 4599 } else { 4600 mSplitSelectStateController.setSecondTask(intent, user); 4601 } 4602 4603 RectF secondTaskStartingBounds = new RectF(); 4604 Rect secondTaskEndingBounds = new Rect(); 4605 // TODO(194414938) starting bounds seem slightly off, investigate 4606 Rect firstTaskStartingBounds = new Rect(); 4607 Rect firstTaskEndingBounds = mTempRect; 4608 4609 boolean isTablet = mActivity.getDeviceProfile().isTablet; 4610 SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet); 4611 PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration()); 4612 4613 int halfDividerSize = getResources() 4614 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; 4615 mOrientationHandler.getFinalSplitPlaceholderBounds(halfDividerSize, 4616 mActivity.getDeviceProfile(), 4617 mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds, 4618 secondTaskEndingBounds); 4619 4620 mFirstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds); 4621 mFirstFloatingTaskView.addConfirmAnimation(pendingAnimation, 4622 new RectF(firstTaskStartingBounds), firstTaskEndingBounds, 4623 false /* fadeWithThumbnail */, true /* isStagedTask */); 4624 4625 safeRemoveDragLayerView(mSecondFloatingTaskView); 4626 4627 mSecondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity, secondView, 4628 thumbnail, drawable, secondTaskStartingBounds); 4629 mSecondFloatingTaskView.setAlpha(1); 4630 mSecondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds, 4631 secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */); 4632 4633 pendingAnimation.setViewAlpha(mSplitInstructionsView, 0, clampToProgress(LINEAR, 4634 timings.getInstructionsFadeStartOffset(), 4635 timings.getInstructionsFadeEndOffset())); 4636 4637 pendingAnimation.addEndListener(aBoolean -> { 4638 mSplitSelectStateController.launchSplitTasks( 4639 aBoolean1 -> RecentsView.this.resetFromSplitSelectionState()); 4640 InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); 4641 }); 4642 4643 mSecondSplitHiddenView = containerTaskView; 4644 if (mSecondSplitHiddenView != null) { 4645 mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE, 4646 mSplitSelectStateController.getSecondTaskId()); 4647 } 4648 4649 InteractionJankMonitorWrapper.begin(this, 4650 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Second tile selected"); 4651 4652 // Fade out all other views underneath placeholders 4653 ObjectAnimator tvFade = ObjectAnimator.ofFloat(this, RecentsView.CONTENT_ALPHA,1, 0); 4654 pendingAnimation.add(tvFade, DEACCEL_2, SpringProperty.DEFAULT); 4655 pendingAnimation.buildAnim().start(); 4656 return true; 4657 } 4658 4659 @SuppressLint("WrongCall") 4660 protected void resetFromSplitSelectionState() { 4661 if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1) { 4662 safeRemoveDragLayerView(mFirstFloatingTaskView); 4663 safeRemoveDragLayerView(mSecondFloatingTaskView); 4664 safeRemoveDragLayerView(mSplitInstructionsView); 4665 mFirstFloatingTaskView = null; 4666 mSecondFloatingTaskView = null; 4667 mSplitInstructionsView = null; 4668 mSplitSelectSource = null; 4669 } 4670 4671 if (mSecondSplitHiddenView != null) { 4672 mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID); 4673 mSecondSplitHiddenView = null; 4674 } 4675 4676 // We are leaving split selection state, so it is safe to reset thumbnail translations for 4677 // the next time split is invoked. 4678 setTaskViewsPrimarySplitTranslation(0); 4679 setTaskViewsSecondarySplitTranslation(0); 4680 4681 if (mSplitHiddenTaskViewIndex == -1) { 4682 return; 4683 } 4684 if (!mActivity.getDeviceProfile().isTablet) { 4685 int pageToSnapTo = mCurrentPage; 4686 if (mSplitHiddenTaskViewIndex <= pageToSnapTo) { 4687 pageToSnapTo += 1; 4688 } else { 4689 pageToSnapTo = mSplitHiddenTaskViewIndex; 4690 } 4691 snapToPageImmediately(pageToSnapTo); 4692 } 4693 onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom()); 4694 4695 resetTaskVisuals(); 4696 mSplitHiddenTaskViewIndex = -1; 4697 if (mSplitHiddenTaskView != null) { 4698 mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID); 4699 mSplitHiddenTaskView = null; 4700 } 4701 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 4702 updateDesktopTaskVisibility(true /* visible */); 4703 } 4704 } 4705 4706 private void safeRemoveDragLayerView(@Nullable View viewToRemove) { 4707 if (viewToRemove != null) { 4708 mActivity.getDragLayer().removeView(viewToRemove); 4709 } 4710 } 4711 4712 /** 4713 * Returns how much additional translation there should be for each of the child TaskViews. 4714 * Note that the translation can be its primary or secondary dimension. 4715 */ 4716 public float getSplitSelectTranslation() { 4717 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 4718 PagedOrientationHandler orientationHandler = getPagedOrientationHandler(); 4719 int splitPosition = getSplitSelectController().getActiveSplitStagePosition(); 4720 int splitPlaceholderSize = 4721 mActivity.getResources().getDimensionPixelSize(R.dimen.split_placeholder_size); 4722 int direction = orientationHandler.getSplitTranslationDirectionFactor( 4723 splitPosition, deviceProfile); 4724 4725 if (deviceProfile.isTablet && deviceProfile.isLandscape) { 4726 // Only shift TaskViews if there is not enough space on the side of 4727 // mLastComputedTaskSize to minimize motion. 4728 int sideSpace = mIsRtl 4729 ? deviceProfile.widthPx - mLastComputedTaskSize.right 4730 : mLastComputedTaskSize.left; 4731 int extraSpace = splitPlaceholderSize + mPageSpacing - sideSpace; 4732 if (extraSpace <= 0f) { 4733 return 0f; 4734 } 4735 4736 return extraSpace * direction; 4737 } 4738 4739 return splitPlaceholderSize * direction; 4740 } 4741 4742 protected void onRotateInSplitSelectionState() { 4743 mOrientationHandler.getInitialSplitPlaceholderBounds(mSplitPlaceholderSize, 4744 mSplitPlaceholderInset, mActivity.getDeviceProfile(), 4745 mSplitSelectStateController.getActiveSplitStagePosition(), mTempRect); 4746 mTempRectF.set(mTempRect); 4747 mFirstFloatingTaskView.updateOrientationHandler(mOrientationHandler); 4748 mFirstFloatingTaskView.update(mTempRectF, /*progress=*/1f); 4749 4750 PagedOrientationHandler orientationHandler = getPagedOrientationHandler(); 4751 Pair<FloatProperty, FloatProperty> taskViewsFloat = 4752 orientationHandler.getSplitSelectTaskOffset( 4753 TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION, 4754 mActivity.getDeviceProfile()); 4755 taskViewsFloat.first.set(this, getSplitSelectTranslation()); 4756 taskViewsFloat.second.set(this, 0f); 4757 4758 if (mSplitInstructionsView != null) { 4759 mSplitInstructionsView.ensureProperRotation(); 4760 } 4761 } 4762 4763 private void updateDeadZoneRects() { 4764 // Get the deadzone rect surrounding the clear all button to not dismiss overview to home 4765 mClearAllButtonDeadZoneRect.setEmpty(); 4766 if (mClearAllButton.getWidth() > 0) { 4767 int verticalMargin = getResources() 4768 .getDimensionPixelSize(R.dimen.recents_clear_all_deadzone_vertical_margin); 4769 mClearAllButton.getHitRect(mClearAllButtonDeadZoneRect); 4770 mClearAllButtonDeadZoneRect.inset(-getPaddingRight() / 2, -verticalMargin); 4771 } 4772 4773 // Get the deadzone rect between the task views 4774 mTaskViewDeadZoneRect.setEmpty(); 4775 int count = getTaskViewCount(); 4776 if (count > 0) { 4777 final View taskView = requireTaskViewAt(0); 4778 requireTaskViewAt(count - 1).getHitRect(mTaskViewDeadZoneRect); 4779 mTaskViewDeadZoneRect.union(taskView.getLeft(), taskView.getTop(), taskView.getRight(), 4780 taskView.getBottom()); 4781 } 4782 } 4783 4784 private void updateEmptyStateUi(boolean sizeChanged) { 4785 boolean hasValidSize = getWidth() > 0 && getHeight() > 0; 4786 if (sizeChanged && hasValidSize) { 4787 mEmptyTextLayout = null; 4788 mLastMeasureSize.set(getWidth(), getHeight()); 4789 } 4790 4791 if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { 4792 int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; 4793 mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), 4794 mEmptyMessagePaint, availableWidth) 4795 .setAlignment(Layout.Alignment.ALIGN_CENTER) 4796 .build(); 4797 int totalHeight = mEmptyTextLayout.getHeight() 4798 + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); 4799 4800 int top = (mLastMeasureSize.y - totalHeight) / 2; 4801 int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; 4802 mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), 4803 top + mEmptyIcon.getIntrinsicHeight()); 4804 } 4805 } 4806 4807 @Override 4808 protected boolean verifyDrawable(Drawable who) { 4809 return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); 4810 } 4811 4812 protected void maybeDrawEmptyMessage(Canvas canvas) { 4813 if (mShowEmptyMessage && mEmptyTextLayout != null) { 4814 // Offset to center in the visible (non-padded) part of RecentsView 4815 mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), 4816 mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); 4817 canvas.save(); 4818 canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, 4819 (mTempRect.top - mTempRect.bottom) / 2); 4820 mEmptyIcon.draw(canvas); 4821 canvas.translate(mEmptyMessagePadding, 4822 mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); 4823 mEmptyTextLayout.draw(canvas); 4824 canvas.restore(); 4825 } 4826 } 4827 4828 /** 4829 * Animate adjacent tasks off screen while scaling up. 4830 * 4831 * If launching one of the adjacent tasks, parallax the center task and other adjacent task 4832 * to the right. 4833 */ 4834 @SuppressLint("Recycle") 4835 public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) { 4836 AnimatorSet anim = new AnimatorSet(); 4837 4838 int taskIndex = indexOfChild(tv); 4839 int centerTaskIndex = getCurrentPage(); 4840 4841 float toScale = getMaxScaleForFullScreen(); 4842 boolean showAsGrid = showAsGrid(); 4843 boolean launchingCenterTask = showAsGrid 4844 ? tv.isFocusedTask() && isTaskViewFullyVisible(tv) 4845 : taskIndex == centerTaskIndex; 4846 if (launchingCenterTask) { 4847 anim.play(ObjectAnimator.ofFloat(this, RECENTS_SCALE_PROPERTY, toScale)); 4848 anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1)); 4849 } else if (!showAsGrid) { 4850 // We are launching an adjacent task, so parallax the center and other adjacent task. 4851 float displacementX = tv.getWidth() * (toScale - 1f); 4852 float primaryTranslation = mIsRtl ? -displacementX : displacementX; 4853 anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), 4854 mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation)); 4855 int runningTaskIndex = getRunningTaskIndex(); 4856 if (runningTaskIndex != -1 && runningTaskIndex != taskIndex 4857 && getRemoteTargetHandles() != null) { 4858 for (RemoteTargetHandle remoteHandle : getRemoteTargetHandles()) { 4859 anim.play(ObjectAnimator.ofFloat( 4860 remoteHandle.getTaskViewSimulator().taskPrimaryTranslation, 4861 AnimatedFloat.VALUE, 4862 primaryTranslation)); 4863 } 4864 } 4865 4866 int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); 4867 if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { 4868 PropertyValuesHolder[] properties = new PropertyValuesHolder[3]; 4869 properties[0] = PropertyValuesHolder.ofFloat( 4870 mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation); 4871 properties[1] = PropertyValuesHolder.ofFloat(View.SCALE_X, 1); 4872 properties[2] = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1); 4873 4874 anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex), 4875 properties)); 4876 } 4877 } 4878 anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0, 1)); 4879 return anim; 4880 } 4881 4882 /** 4883 * Returns the scale up required on the view, so that it coves the screen completely 4884 */ 4885 public float getMaxScaleForFullScreen() { 4886 getTaskSize(mTempRect); 4887 return getPagedViewOrientedState().getFullScreenScaleAndPivot( 4888 mTempRect, mActivity.getDeviceProfile(), mTempPointF); 4889 } 4890 4891 public PendingAnimation createTaskLaunchAnimation( 4892 TaskView tv, long duration, Interpolator interpolator) { 4893 if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) { 4894 throw new IllegalStateException("Another pending animation is still running"); 4895 } 4896 4897 int count = getTaskViewCount(); 4898 if (count == 0) { 4899 return new PendingAnimation(duration); 4900 } 4901 4902 // When swiping down from overview to tasks, ensures the snapped page's scroll maintain 4903 // invariant between quick switch and overview, to ensure a smooth animation transition. 4904 updateGridProperties(); 4905 updateScrollSynchronously(); 4906 4907 int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); 4908 final boolean[] passedOverviewThreshold = new boolean[] {false}; 4909 ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1); 4910 progressAnim.addUpdateListener(animator -> { 4911 // Once we pass a certain threshold, update the sysui flags to match the target 4912 // tasks' flags 4913 if (animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD) { 4914 mActivity.getSystemUiController().updateUiState( 4915 UI_STATE_FULLSCREEN_TASK, targetSysUiFlags); 4916 } else { 4917 mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0); 4918 } 4919 4920 // Passing the threshold from taskview to fullscreen app will vibrate 4921 final boolean passed = animator.getAnimatedFraction() >= 4922 SUCCESS_TRANSITION_PROGRESS; 4923 if (passed != passedOverviewThreshold[0]) { 4924 passedOverviewThreshold[0] = passed; 4925 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 4926 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 4927 } 4928 }); 4929 4930 AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv); 4931 4932 DepthController depthController = getDepthController(); 4933 if (depthController != null) { 4934 ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController.stateDepth, 4935 MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(mActivity)); 4936 anim.play(depthAnimator); 4937 } ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0f, 1f)4938 anim.play(ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, 0f, 1f)); 4939 anim.play(progressAnim)4940 anim.play(progressAnim); anim.setInterpolator(interpolator)4941 anim.setInterpolator(interpolator); 4942 4943 mPendingAnimation = new PendingAnimation(duration); mPendingAnimation.add(anim)4944 mPendingAnimation.add(anim); 4945 runActionOnRemoteHandles( 4946 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator() 4947 .addOverviewToAppAnim(mPendingAnimation, interpolator)); mPendingAnimation.addOnFrameCallback(this::redrawLiveTile)4948 mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); 4949 mPendingAnimation.addEndListener(isSuccess -> { 4950 if (isSuccess) { 4951 if (tv.getTaskIds()[1] != -1 && mRemoteTargetHandles != null) { 4952 // TODO(b/194414938): make this part of the animations instead. 4953 TaskViewUtils.createSplitAuxiliarySurfacesAnimator( 4954 mRemoteTargetHandles[0].getTransformParams().getTargetSet().nonApps, 4955 true /*shown*/, (dividerAnimator) -> { 4956 dividerAnimator.start(); 4957 dividerAnimator.end(); 4958 }); 4959 } 4960 if (tv.isRunningTask()) { 4961 finishRecentsAnimation(false /* toRecents */, null); 4962 onTaskLaunchAnimationEnd(true /* success */); 4963 } else { 4964 tv.launchTask(this::onTaskLaunchAnimationEnd); 4965 } 4966 Task task = tv.getTask(); 4967 if (task != null) { 4968 mActivity.getStatsLogManager().logger().withItemInfo(tv.getItemInfo()) 4969 .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN); 4970 } 4971 } else { 4972 onTaskLaunchAnimationEnd(false); 4973 } 4974 mPendingAnimation = null; 4975 }); 4976 return mPendingAnimation; 4977 } 4978 onTaskLaunchAnimationEnd(boolean success)4979 protected void onTaskLaunchAnimationEnd(boolean success) { 4980 if (success) { 4981 resetTaskVisuals(); 4982 } 4983 } 4984 4985 @Override notifyPageSwitchListener(int prevPage)4986 protected void notifyPageSwitchListener(int prevPage) { 4987 super.notifyPageSwitchListener(prevPage); 4988 updateCurrentTaskActionsVisibility(); 4989 loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL); 4990 updateEnabledOverlays(); 4991 } 4992 4993 @Override getCurrentPageDescription()4994 protected String getCurrentPageDescription() { 4995 return ""; 4996 } 4997 4998 @Override addChildrenForAccessibility(ArrayList<View> outChildren)4999 public void addChildrenForAccessibility(ArrayList<View> outChildren) { 5000 // Add children in reverse order 5001 for (int i = getChildCount() - 1; i >= 0; --i) { 5002 outChildren.add(getChildAt(i)); 5003 } 5004 } 5005 5006 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)5007 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 5008 super.onInitializeAccessibilityNodeInfo(info); 5009 final AccessibilityNodeInfo.CollectionInfo 5010 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( 5011 1, getTaskViewCount(), false, 5012 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); 5013 info.setCollectionInfo(collectionInfo); 5014 } 5015 5016 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)5017 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 5018 super.onInitializeAccessibilityEvent(event); 5019 5020 final int taskViewCount = getTaskViewCount(); 5021 event.setScrollable(taskViewCount > 0); 5022 5023 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { 5024 final int[] visibleTasks = getVisibleChildrenRange(); 5025 event.setFromIndex(taskViewCount - visibleTasks[1]); 5026 event.setToIndex(taskViewCount - visibleTasks[0]); 5027 event.setItemCount(taskViewCount); 5028 } 5029 } 5030 5031 @Override getAccessibilityClassName()5032 public CharSequence getAccessibilityClassName() { 5033 // To hear position-in-list related feedback from Talkback. 5034 return ListView.class.getName(); 5035 } 5036 5037 @Override isPageOrderFlipped()5038 protected boolean isPageOrderFlipped() { 5039 return true; 5040 } 5041 setEnableDrawingLiveTile(boolean enableDrawingLiveTile)5042 public void setEnableDrawingLiveTile(boolean enableDrawingLiveTile) { 5043 mEnableDrawingLiveTile = enableDrawingLiveTile; 5044 } 5045 redrawLiveTile()5046 public void redrawLiveTile() { 5047 runActionOnRemoteHandles(remoteTargetHandle -> { 5048 TransformParams params = remoteTargetHandle.getTransformParams(); 5049 if (params.getTargetSet() != null) { 5050 remoteTargetHandle.getTaskViewSimulator().apply(params); 5051 } 5052 }); 5053 } 5054 getRemoteTargetHandles()5055 public RemoteTargetHandle[] getRemoteTargetHandles() { 5056 return mRemoteTargetHandles; 5057 } 5058 5059 // TODO: To be removed in a follow up CL setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController, RecentsAnimationTargets recentsAnimationTargets)5060 public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController, 5061 RecentsAnimationTargets recentsAnimationTargets) { 5062 mRecentsAnimationController = recentsAnimationController; 5063 mSplitSelectStateController.setRecentsAnimationRunning(true); 5064 if (recentsAnimationTargets == null || recentsAnimationTargets.apps.length == 0) { 5065 return; 5066 } 5067 5068 RemoteTargetGluer gluer; 5069 if (DESKTOP_MODE_SUPPORTED && recentsAnimationTargets.hasDesktopTasks()) { 5070 gluer = new RemoteTargetGluer(getContext(), getSizeStrategy(), recentsAnimationTargets, 5071 true /* forDesktop */); 5072 mRemoteTargetHandles = gluer.assignTargetsForDesktop(recentsAnimationTargets); 5073 } else { 5074 gluer = new RemoteTargetGluer(getContext(), getSizeStrategy()); 5075 mRemoteTargetHandles = gluer.assignTargetsForSplitScreen(recentsAnimationTargets); 5076 } 5077 mSplitBoundsConfig = gluer.getSplitBounds(); 5078 // Add release check to the targets from the RemoteTargetGluer and not the targets 5079 // passed in because in the event we're in split screen, we use the passed in targets 5080 // to create new RemoteAnimationTargets in assignTargetsForSplitScreen(), and the 5081 // mSyncTransactionApplier doesn't get transferred over 5082 runActionOnRemoteHandles(remoteTargetHandle -> { 5083 final TransformParams params = remoteTargetHandle.getTransformParams(); 5084 if (mSyncTransactionApplier != null) { 5085 params.setSyncTransactionApplier(mSyncTransactionApplier); 5086 params.getTargetSet().addReleaseCheck(mSyncTransactionApplier); 5087 } 5088 5089 TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator(); 5090 tvs.setOrientationState(mOrientationState); 5091 tvs.setDp(mActivity.getDeviceProfile()); 5092 tvs.recentsViewScale.value = 1; 5093 }); 5094 5095 TaskView runningTaskView = getRunningTaskView(); 5096 if (runningTaskView instanceof GroupedTaskView) { 5097 // We initially create a GroupedTaskView in showCurrentTask() before launcher even 5098 // receives the leashes for the remote apps, so the mSplitBoundsConfig that gets passed 5099 // in there is either null or outdated, so we need to update here as soon as we're 5100 // notified. 5101 ((GroupedTaskView) runningTaskView).updateSplitBoundsConfig(mSplitBoundsConfig); 5102 } 5103 } 5104 5105 /** Helper to avoid writing some for-loops to iterate over {@link #mRemoteTargetHandles} */ runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer)5106 public void runActionOnRemoteHandles(Consumer<RemoteTargetHandle> consumer) { 5107 if (mRemoteTargetHandles == null) { 5108 return; 5109 } 5110 5111 for (RemoteTargetHandle handle : mRemoteTargetHandles) { 5112 consumer.accept(handle); 5113 } 5114 } 5115 5116 /** 5117 * Finish recents animation. 5118 */ finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete)5119 public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) { 5120 finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete); 5121 } 5122 finishRecentsAnimation(boolean toRecents, boolean shouldPip, @Nullable Runnable onFinishComplete)5123 public void finishRecentsAnimation(boolean toRecents, boolean shouldPip, 5124 @Nullable Runnable onFinishComplete) { 5125 // TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe? 5126 cleanupRemoteTargets(); 5127 5128 if (mRecentsAnimationController == null) { 5129 if (onFinishComplete != null) { 5130 onFinishComplete.run(); 5131 } 5132 return; 5133 } 5134 5135 final boolean sendUserLeaveHint = toRecents && shouldPip; 5136 if (sendUserLeaveHint) { 5137 // Notify the SysUI to use fade-in animation when entering PiP from live tile. 5138 final SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(getContext()); 5139 systemUiProxy.setPipAnimationTypeToAlpha(); 5140 systemUiProxy.setShelfHeight(true, mActivity.getDeviceProfile().hotseatBarSizePx); 5141 // Transaction to hide the task to avoid flicker for entering PiP from split-screen. 5142 // See also {@link AbsSwipeUpHandler#maybeFinishSwipeToHome}. 5143 PictureInPictureSurfaceTransaction tx = 5144 new PictureInPictureSurfaceTransaction.Builder() 5145 .setAlpha(0f) 5146 .build(); 5147 tx.setShouldDisableCanAffectSystemUiFlags(false); 5148 int[] taskIds = TopTaskTracker.INSTANCE.get(getContext()).getRunningSplitTaskIds(); 5149 for (int taskId : taskIds) { 5150 mRecentsAnimationController.setFinishTaskTransaction(taskId, 5151 tx, null /* overlay */); 5152 } 5153 } 5154 mRecentsAnimationController.finish(toRecents, () -> { 5155 if (onFinishComplete != null) { 5156 onFinishComplete.run(); 5157 } 5158 onRecentsAnimationComplete(); 5159 }, sendUserLeaveHint); 5160 } 5161 5162 /** 5163 * Called when a running recents animation has finished or canceled. 5164 */ onRecentsAnimationComplete()5165 public void onRecentsAnimationComplete() { 5166 // At this point, the recents animation is not running and if the animation was canceled 5167 // by a display rotation then reset this state to show the screenshot 5168 setRunningTaskViewShowScreenshot(true); 5169 // After we finish the recents animation, the current task id should be correctly 5170 // reset so that when the task is launched from Overview later, it goes through the 5171 // flow of starting a new task instead of finishing recents animation to app. A 5172 // typical example of this is (1) user swipes up from app to Overview (2) user 5173 // taps on QSB (3) user goes back to Overview and launch the most recent task. 5174 setCurrentTask(-1); 5175 mRecentsAnimationController = null; 5176 mSplitSelectStateController.setRecentsAnimationRunning(false); 5177 executeSideTaskLaunchCallback(); 5178 } 5179 setDisallowScrollToClearAll(boolean disallowScrollToClearAll)5180 public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) { 5181 if (mDisallowScrollToClearAll != disallowScrollToClearAll) { 5182 mDisallowScrollToClearAll = disallowScrollToClearAll; 5183 updateMinAndMaxScrollX(); 5184 } 5185 } 5186 5187 /** 5188 * Updates page scroll synchronously after measure and layout child views. 5189 */ 5190 @SuppressLint("WrongCall") updateScrollSynchronously()5191 public void updateScrollSynchronously() { 5192 // onMeasure is needed to update child's measured width which is used in scroll calculation, 5193 // in case TaskView sizes has changed when being focused/unfocused. 5194 onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY), 5195 makeMeasureSpec(getMeasuredHeight(), EXACTLY)); 5196 onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom()); 5197 updateMinAndMaxScrollX(); 5198 } 5199 5200 @Override getChildGap(int fromIndex, int toIndex)5201 protected int getChildGap(int fromIndex, int toIndex) { 5202 int clearAllIndex = indexOfChild(mClearAllButton); 5203 return fromIndex == clearAllIndex || toIndex == clearAllIndex 5204 ? getClearAllExtraPageSpacing() : 0; 5205 } 5206 getClearAllExtraPageSpacing()5207 protected int getClearAllExtraPageSpacing() { 5208 return showAsGrid() 5209 ? Math.max(mActivity.getDeviceProfile().overviewGridSideMargin - mPageSpacing, 0) 5210 : 0; 5211 } 5212 5213 @Override updateMinAndMaxScrollX()5214 protected void updateMinAndMaxScrollX() { 5215 super.updateMinAndMaxScrollX(); 5216 if (DEBUG) { 5217 Log.d(TAG, "updateMinAndMaxScrollX - mMinScroll: " + mMinScroll); 5218 Log.d(TAG, "updateMinAndMaxScrollX - mMaxScroll: " + mMaxScroll); 5219 } 5220 } 5221 5222 @Override computeMinScroll()5223 protected int computeMinScroll() { 5224 if (getTaskViewCount() <= 0) { 5225 return super.computeMinScroll(); 5226 } 5227 5228 return getScrollForPage(mIsRtl ? getLastViewIndex() : getFirstViewIndex()); 5229 } 5230 5231 @Override computeMaxScroll()5232 protected int computeMaxScroll() { 5233 if (getTaskViewCount() <= 0) { 5234 return super.computeMaxScroll(); 5235 } 5236 5237 return getScrollForPage(mIsRtl ? getFirstViewIndex() : getLastViewIndex()); 5238 } 5239 getFirstViewIndex()5240 private int getFirstViewIndex() { 5241 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED && mDesktopTaskView != null) { 5242 // Desktop task is at position 0, that is the first view 5243 return 0; 5244 } 5245 TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null; 5246 return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0; 5247 } 5248 getLastViewIndex()5249 private int getLastViewIndex() { 5250 if (!mDisallowScrollToClearAll) { 5251 return indexOfChild(mClearAllButton); 5252 } 5253 5254 if (!mShowAsGridLastOnLayout) { 5255 return getTaskViewCount() - 1; 5256 } 5257 5258 TaskView lastGridTaskView = getLastGridTaskView(); 5259 if (lastGridTaskView != null) { 5260 return indexOfChild(lastGridTaskView); 5261 } 5262 5263 // Returns focus task if there are no grid tasks. 5264 return indexOfChild(getFocusedTaskView()); 5265 } 5266 5267 /** 5268 * Returns page scroll of ClearAllButton. 5269 */ getClearAllScroll()5270 public int getClearAllScroll() { 5271 return getScrollForPage(indexOfChild(mClearAllButton)); 5272 } 5273 5274 @Override getPageScrolls(int[] outPageScrolls, boolean layoutChildren, ComputePageScrollsLogic scrollLogic)5275 protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, 5276 ComputePageScrollsLogic scrollLogic) { 5277 int[] newPageScrolls = new int[outPageScrolls.length]; 5278 super.getPageScrolls(newPageScrolls, layoutChildren, scrollLogic); 5279 boolean showAsFullscreen = showAsFullscreen(); 5280 boolean showAsGrid = showAsGrid(); 5281 5282 // Align ClearAllButton to the left (RTL) or right (non-RTL), which is different from other 5283 // TaskViews. This must be called after laying out ClearAllButton. 5284 if (layoutChildren) { 5285 int clearAllWidthDiff = mOrientationHandler.getPrimaryValue(mTaskWidth, mTaskHeight) 5286 - mOrientationHandler.getPrimarySize(mClearAllButton); 5287 mClearAllButton.setScrollOffsetPrimary(mIsRtl ? clearAllWidthDiff : -clearAllWidthDiff); 5288 } 5289 5290 boolean pageScrollChanged = false; 5291 5292 int clearAllIndex = indexOfChild(mClearAllButton); 5293 int clearAllScroll = 0; 5294 int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton); 5295 if (clearAllIndex != -1 && clearAllIndex < outPageScrolls.length) { 5296 float scrollDiff = mClearAllButton.getScrollAdjustment(showAsFullscreen, showAsGrid); 5297 clearAllScroll = newPageScrolls[clearAllIndex] + (int) scrollDiff; 5298 if (outPageScrolls[clearAllIndex] != clearAllScroll) { 5299 pageScrollChanged = true; 5300 outPageScrolls[clearAllIndex] = clearAllScroll; 5301 } 5302 } 5303 5304 final int taskCount = getTaskViewCount(); 5305 int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth); 5306 for (int i = 0; i < taskCount; i++) { 5307 TaskView taskView = requireTaskViewAt(i); 5308 float scrollDiff = taskView.getScrollAdjustment(showAsGrid); 5309 int pageScroll = newPageScrolls[i] + (int) scrollDiff; 5310 if ((mIsRtl && pageScroll < lastTaskScroll) 5311 || (!mIsRtl && pageScroll > lastTaskScroll)) { 5312 pageScroll = lastTaskScroll; 5313 } 5314 if (outPageScrolls[i] != pageScroll) { 5315 pageScrollChanged = true; 5316 outPageScrolls[i] = pageScroll; 5317 } 5318 if (DEBUG) { 5319 Log.d(TAG, "getPageScrolls - outPageScrolls[" + i + "]: " + outPageScrolls[i]); 5320 } 5321 } 5322 if (DEBUG) { 5323 Log.d(TAG, "getPageScrolls - clearAllScroll: " + clearAllScroll); 5324 } 5325 return pageScrollChanged; 5326 } 5327 5328 @Override getChildOffset(int index)5329 protected int getChildOffset(int index) { 5330 int childOffset = super.getChildOffset(index); 5331 View child = getChildAt(index); 5332 if (child instanceof TaskView) { 5333 childOffset += ((TaskView) child).getOffsetAdjustment(showAsGrid()); 5334 } else if (child instanceof ClearAllButton) { 5335 childOffset += ((ClearAllButton) child).getOffsetAdjustment(mOverviewFullscreenEnabled, 5336 showAsGrid()); 5337 } 5338 return childOffset; 5339 } 5340 5341 @Override getChildVisibleSize(int index)5342 protected int getChildVisibleSize(int index) { 5343 final TaskView taskView = getTaskViewAt(index); 5344 if (taskView == null) { 5345 return super.getChildVisibleSize(index); 5346 } 5347 return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment( 5348 showAsFullscreen())); 5349 } 5350 getClearAllButton()5351 public ClearAllButton getClearAllButton() { 5352 return mClearAllButton; 5353 } 5354 5355 /** 5356 * @return How many pixels the running task is offset on the currently laid out dominant axis. 5357 */ getScrollOffset()5358 public int getScrollOffset() { 5359 return getScrollOffset(getRunningTaskIndex()); 5360 } 5361 5362 /** 5363 * Sets whether or not we should clamp the scroll offset. 5364 * This is used to avoid x-axis movement when swiping up transient taskbar. 5365 * Should only be set at the beginning and end of the gesture, otherwise a jump may occur. 5366 * @param clampScrollOffset When true, we clamp the scroll to 0 before the clamp threshold is 5367 * met. 5368 */ setClampScrollOffset(boolean clampScrollOffset)5369 public void setClampScrollOffset(boolean clampScrollOffset) { 5370 mShouldClampScrollOffset = clampScrollOffset; 5371 } 5372 5373 /** 5374 * Returns how many pixels the page is offset on the currently laid out dominant axis. 5375 */ getScrollOffset(int pageIndex)5376 public int getScrollOffset(int pageIndex) { 5377 int unclampedOffset = getUnclampedScrollOffset(pageIndex); 5378 if (!mShouldClampScrollOffset) { 5379 return unclampedOffset; 5380 } 5381 if (Math.abs(unclampedOffset) < mClampedScrollOffsetBound) { 5382 return 0; 5383 } 5384 return unclampedOffset 5385 - Math.round(Math.signum(unclampedOffset) * mClampedScrollOffsetBound); 5386 } 5387 5388 /** 5389 * Returns how many pixels the page is offset on the currently laid out dominant axis. 5390 */ getUnclampedScrollOffset(int pageIndex)5391 private int getUnclampedScrollOffset(int pageIndex) { 5392 if (pageIndex == -1) { 5393 return 0; 5394 } 5395 5396 int overScrollShift = getOverScrollShift(); 5397 if (mAdjacentPageHorizontalOffset > 0) { 5398 // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so 5399 // that the page can move freely given there's no visual indication why it shouldn't. 5400 overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset, 5401 overScrollShift, getUndampedOverScrollShift()); 5402 } 5403 return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this) 5404 + overScrollShift + getOffsetFromScrollPosition(pageIndex); 5405 } 5406 5407 /** 5408 * Returns how many pixels the page is offset from its scroll position. 5409 */ getOffsetFromScrollPosition(int pageIndex)5410 private int getOffsetFromScrollPosition(int pageIndex) { 5411 return getOffsetFromScrollPosition(pageIndex, getTopRowIdArray(), getBottomRowIdArray()); 5412 } 5413 getOffsetFromScrollPosition( int pageIndex, IntArray topRowIdArray, IntArray bottomRowIdArray)5414 private int getOffsetFromScrollPosition( 5415 int pageIndex, IntArray topRowIdArray, IntArray bottomRowIdArray) { 5416 if (!showAsGrid()) { 5417 return 0; 5418 } 5419 5420 TaskView taskView = getTaskViewAt(pageIndex); 5421 if (taskView == null) { 5422 return 0; 5423 } 5424 5425 TaskView lastGridTaskView = getLastGridTaskView(topRowIdArray, bottomRowIdArray); 5426 if (lastGridTaskView == null) { 5427 return 0; 5428 } 5429 5430 if (getScrollForPage(pageIndex) != getScrollForPage(indexOfChild(lastGridTaskView))) { 5431 return 0; 5432 } 5433 5434 // Check distance from lastGridTaskView to taskView. 5435 int lastGridTaskViewPosition = 5436 getPositionInRow(lastGridTaskView, topRowIdArray, bottomRowIdArray); 5437 int taskViewPosition = getPositionInRow(taskView, topRowIdArray, bottomRowIdArray); 5438 int gridTaskSizeAndSpacing = mLastComputedGridTaskSize.width() + mPageSpacing; 5439 int positionDiff = gridTaskSizeAndSpacing * (lastGridTaskViewPosition - taskViewPosition); 5440 5441 int taskEnd = getLastTaskEnd() + (mIsRtl ? positionDiff : -positionDiff); 5442 int normalTaskEnd = mIsRtl 5443 ? mLastComputedGridTaskSize.left 5444 : mLastComputedGridTaskSize.right; 5445 return taskEnd - normalTaskEnd; 5446 } 5447 getLastTaskEnd()5448 private int getLastTaskEnd() { 5449 return mIsRtl 5450 ? mLastComputedGridSize.left + mPageSpacing + mClearAllShortTotalWidthTranslation 5451 : mLastComputedGridSize.right - mPageSpacing - mClearAllShortTotalWidthTranslation; 5452 } 5453 getPositionInRow( TaskView taskView, IntArray topRowIdArray, IntArray bottomRowIdArray)5454 private int getPositionInRow( 5455 TaskView taskView, IntArray topRowIdArray, IntArray bottomRowIdArray) { 5456 int position = topRowIdArray.indexOf(taskView.getTaskViewId()); 5457 return position != -1 ? position : bottomRowIdArray.indexOf(taskView.getTaskViewId()); 5458 } 5459 5460 /** 5461 * @return true if the task in on the top of the grid 5462 */ isOnGridBottomRow(TaskView taskView)5463 public boolean isOnGridBottomRow(TaskView taskView) { 5464 return showAsGrid() 5465 && !mTopRowIdSet.contains(taskView.getTaskViewId()) 5466 && taskView.getTaskViewId() != mFocusedTaskViewId; 5467 } 5468 getEventDispatcher(float navbarRotation)5469 public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) { 5470 float degreesRotated; 5471 if (navbarRotation == 0) { 5472 degreesRotated = mOrientationHandler.getDegreesRotated(); 5473 } else { 5474 degreesRotated = -navbarRotation; 5475 } 5476 if (degreesRotated == 0) { 5477 return super::onTouchEvent; 5478 } 5479 5480 // At this point the event coordinates have already been transformed, so we need to 5481 // undo that transformation since PagedView also accommodates for the transformation via 5482 // PagedOrientationHandler 5483 return e -> { 5484 if (navbarRotation != 0 5485 && mOrientationState.isMultipleOrientationSupportedByDevice() 5486 && !mOrientationState.getOrientationHandler().isLayoutNaturalToLauncher()) { 5487 mOrientationState.flipVertical(e); 5488 super.onTouchEvent(e); 5489 mOrientationState.flipVertical(e); 5490 return; 5491 } 5492 mOrientationState.transformEvent(-degreesRotated, e, true); 5493 super.onTouchEvent(e); 5494 mOrientationState.transformEvent(-degreesRotated, e, false); 5495 }; 5496 } 5497 5498 private void updateEnabledOverlays() { 5499 int taskCount = getTaskViewCount(); 5500 for (int i = 0; i < taskCount; i++) { 5501 TaskView taskView = requireTaskViewAt(i); 5502 taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView)); 5503 } 5504 } 5505 5506 public void setOverlayEnabled(boolean overlayEnabled) { 5507 if (mOverlayEnabled != overlayEnabled) { 5508 mOverlayEnabled = overlayEnabled; 5509 updateEnabledOverlays(); 5510 } 5511 } 5512 5513 public void setOverviewGridEnabled(boolean overviewGridEnabled) { 5514 if (mOverviewGridEnabled != overviewGridEnabled) { 5515 mOverviewGridEnabled = overviewGridEnabled; 5516 updateActionsViewFocusedScroll(); 5517 // Request layout to ensure scroll position is recalculated with updated mGridProgress. 5518 requestLayout(); 5519 } 5520 } 5521 5522 public void setOverviewFullscreenEnabled(boolean overviewFullscreenEnabled) { 5523 if (mOverviewFullscreenEnabled != overviewFullscreenEnabled) { 5524 mOverviewFullscreenEnabled = overviewFullscreenEnabled; 5525 // Request layout to ensure scroll position is recalculated with updated 5526 // mFullscreenProgress. 5527 requestLayout(); 5528 } 5529 } 5530 5531 /** 5532 * Update whether RecentsView is in select mode. Should be enabled before transitioning to 5533 * select mode, and only disabled after transitioning from select mode. 5534 */ 5535 public void setOverviewSelectEnabled(boolean overviewSelectEnabled) { 5536 if (mOverviewSelectEnabled != overviewSelectEnabled) { 5537 mOverviewSelectEnabled = overviewSelectEnabled; 5538 updatePivots(); 5539 if (!mOverviewSelectEnabled) { 5540 setSelectedTask(INVALID_TASK_ID); 5541 } 5542 } 5543 } 5544 5545 /** 5546 * Switch the current running task view to static snapshot mode, 5547 * capturing the snapshot at the same time. 5548 */ 5549 public void switchToScreenshot(Runnable onFinishRunnable) { 5550 if (mRecentsAnimationController == null) { 5551 if (onFinishRunnable != null) { 5552 onFinishRunnable.run(); 5553 } 5554 return; 5555 } 5556 5557 switchToScreenshotInternal(onFinishRunnable); 5558 } 5559 5560 private void switchToScreenshotInternal(Runnable onFinishRunnable) { 5561 TaskView taskView = getRunningTaskView(); 5562 if (taskView == null) { 5563 onFinishRunnable.run(); 5564 return; 5565 } 5566 5567 taskView.setShowScreenshot(true); 5568 for (TaskIdAttributeContainer container : 5569 taskView.getTaskIdAttributeContainers()) { 5570 if (container == null) { 5571 continue; 5572 } 5573 5574 ThumbnailData td = 5575 mRecentsAnimationController.screenshotTask(container.getTask().key.id); 5576 TaskThumbnailView thumbnailView = container.getThumbnailView(); 5577 if (td != null) { 5578 thumbnailView.setThumbnail(container.getTask(), td); 5579 } else { 5580 thumbnailView.refresh(); 5581 } 5582 } 5583 ViewUtils.postFrameDrawn(taskView, onFinishRunnable); 5584 } 5585 5586 /** 5587 * Switch the current running task view to static snapshot mode, using the 5588 * provided thumbnail data as the snapshot. 5589 * TODO(b/195609063) Consolidate this method w/ the one above, except this thumbnail data comes 5590 * from gesture state, which is a larger change of it having to keep track of multiple tasks. 5591 * OR. Maybe it doesn't need to pass in a thumbnail and we can use the exact same flow as above 5592 */ 5593 public void switchToScreenshot(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas, 5594 Runnable onFinishRunnable) { 5595 final TaskView taskView = getRunningTaskView(); 5596 if (taskView != null) { 5597 taskView.setShowScreenshot(true); 5598 taskView.refreshThumbnails(thumbnailDatas); 5599 ViewUtils.postFrameDrawn(taskView, onFinishRunnable); 5600 } else { 5601 onFinishRunnable.run(); 5602 } 5603 } 5604 5605 /** 5606 * The current task is fully modal (modalness = 1) when it is shown on its own in a modal 5607 * way. Modalness 0 means the task is shown in context with all the other tasks. 5608 */ 5609 private void setTaskModalness(float modalness) { 5610 mTaskModalness = modalness; 5611 updatePageOffsets(); 5612 if (mSelectedTask != null) { 5613 mSelectedTask.setModalness(modalness); 5614 } else if (getCurrentPageTaskView() != null) { 5615 getCurrentPageTaskView().setModalness(modalness); 5616 } 5617 // Only show actions view when it's modal for in-place landscape mode. 5618 boolean inPlaceLandscape = !mOrientationState.isRecentsActivityRotationAllowed() 5619 && mOrientationState.getTouchRotation() != ROTATION_0; 5620 mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION, modalness < 1 && inPlaceLandscape); 5621 } 5622 5623 @Nullable 5624 protected DepthController getDepthController() { 5625 return null; 5626 } 5627 5628 /** Enables or disables modal state for RecentsView */ 5629 public abstract void setModalStateEnabled(int taskId, boolean animate); 5630 5631 public TaskOverlayFactory getTaskOverlayFactory() { 5632 return mTaskOverlayFactory; 5633 } 5634 5635 public BaseActivityInterface getSizeStrategy() { 5636 return mSizeStrategy; 5637 } 5638 5639 /** 5640 * Set all the task views to color tint scrim mode, dimming or tinting them all. Allows the 5641 * tasks to be dimmed while other elements in the recents view are left alone. 5642 */ 5643 public void showForegroundScrim(boolean show) { 5644 if (!show && mColorTint == 0) { 5645 if (mTintingAnimator != null) { 5646 mTintingAnimator.cancel(); 5647 mTintingAnimator = null; 5648 } 5649 return; 5650 } 5651 5652 mTintingAnimator = ObjectAnimator.ofFloat(this, COLOR_TINT, show ? 0.5f : 0f); 5653 mTintingAnimator.setAutoCancel(true); 5654 mTintingAnimator.start(); 5655 } 5656 5657 /** Tint the RecentsView and TaskViews in to simulate a scrim. */ 5658 // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView 5659 private void setColorTint(float tintAmount) { 5660 mColorTint = tintAmount; 5661 5662 for (int i = 0; i < getTaskViewCount(); i++) { 5663 requireTaskViewAt(i).setColorTint(mColorTint, mTintingColor); 5664 } 5665 5666 Drawable scrimBg = mActivity.getScrimView().getBackground(); 5667 if (scrimBg != null) { 5668 if (tintAmount == 0f) { 5669 scrimBg.setTintList(null); 5670 } else { 5671 scrimBg.setTintBlendMode(BlendMode.SRC_OVER); 5672 scrimBg.setTint( 5673 ColorUtils.setAlphaComponent(mTintingColor, (int) (255 * tintAmount))); 5674 } 5675 } 5676 } 5677 5678 private float getColorTint() { 5679 return mColorTint; 5680 } 5681 5682 /** Returns {@code true} if the overview tasks are displayed as a grid. */ 5683 public boolean showAsGrid() { 5684 return mOverviewGridEnabled || (mCurrentGestureEndTarget != null 5685 && mSizeStrategy.stateFromGestureEndTarget( 5686 mCurrentGestureEndTarget).displayOverviewTasksAsGrid(mActivity.getDeviceProfile())); 5687 } 5688 5689 private boolean showAsFullscreen() { 5690 return mOverviewFullscreenEnabled 5691 && mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS; 5692 } 5693 5694 public void cleanupRemoteTargets() { 5695 mRemoteTargetHandles = null; 5696 } 5697 5698 /** 5699 * Used to register callbacks for when our empty message state changes. 5700 * 5701 * @see #setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener) 5702 * @see #updateEmptyMessage() 5703 */ 5704 public interface OnEmptyMessageUpdatedListener { 5705 /** @param isEmpty Whether RecentsView is empty (i.e. has no children) */ 5706 void onEmptyMessageUpdated(boolean isEmpty); 5707 } 5708 5709 /** 5710 * Adds a listener for scroll changes 5711 */ 5712 public void addOnScrollChangedListener(OnScrollChangedListener listener) { 5713 mScrollListeners.add(listener); 5714 } 5715 5716 /** 5717 * Removes a previously added scroll change listener 5718 */ 5719 public void removeOnScrollChangedListener(OnScrollChangedListener listener) { 5720 mScrollListeners.remove(listener); 5721 } 5722 5723 /** 5724 * @return Corner radius in pixel value for PiP window, which is updated via 5725 * {@link #mIPipAnimationListener} 5726 */ 5727 public int getPipCornerRadius() { 5728 return mPipCornerRadius; 5729 } 5730 5731 /** 5732 * @return Shadow radius in pixel value for PiP window, which is updated via 5733 * {@link #mIPipAnimationListener} 5734 */ 5735 public int getPipShadowRadius() { 5736 return mPipShadowRadius; 5737 } 5738 5739 @Override 5740 public boolean scrollLeft() { 5741 if (!showAsGrid()) { 5742 return super.scrollLeft(); 5743 } 5744 5745 int targetPage = getNextPage(); 5746 if (targetPage >= 0) { 5747 // Find the next page that is not fully visible. 5748 TaskView taskView = getTaskViewAt(targetPage); 5749 while ((taskView == null || isTaskViewFullyVisible(taskView)) && targetPage - 1 >= 0) { 5750 taskView = getTaskViewAt(--targetPage); 5751 } 5752 // Target a scroll where targetPage is on left of screen but still fully visible. 5753 int normalTaskEnd = mIsRtl 5754 ? mLastComputedGridTaskSize.left 5755 : mLastComputedGridTaskSize.right; 5756 int targetScroll = getScrollForPage(targetPage) + normalTaskEnd - getLastTaskEnd(); 5757 // Find a page that is close to targetScroll while not over it. 5758 while (targetPage - 1 >= 0 5759 && (mIsRtl 5760 ? getScrollForPage(targetPage - 1) < targetScroll 5761 : getScrollForPage(targetPage - 1) > targetScroll)) { 5762 targetPage--; 5763 } 5764 snapToPage(targetPage); 5765 return true; 5766 } 5767 5768 return mAllowOverScroll; 5769 } 5770 5771 @Override 5772 public boolean scrollRight() { 5773 if (!showAsGrid()) { 5774 return super.scrollRight(); 5775 } 5776 5777 int targetPage = getNextPage(); 5778 if (targetPage < getChildCount()) { 5779 // Find the next page that is not fully visible. 5780 TaskView taskView = getTaskViewAt(targetPage); 5781 while ((taskView != null && isTaskViewFullyVisible(taskView)) 5782 && targetPage + 1 < getChildCount()) { 5783 taskView = getTaskViewAt(++targetPage); 5784 } 5785 snapToPage(targetPage); 5786 return true; 5787 } 5788 return mAllowOverScroll; 5789 } 5790 5791 @Override 5792 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 5793 super.onScrollChanged(l, t, oldl, oldt); 5794 dispatchScrollChanged(); 5795 } 5796 5797 private void dispatchScrollChanged() { 5798 runActionOnRemoteHandles(remoteTargetHandle -> 5799 remoteTargetHandle.getTaskViewSimulator().setScroll(getScrollOffset())); 5800 for (int i = mScrollListeners.size() - 1; i >= 0; i--) { 5801 mScrollListeners.get(i).onScrollChanged(); 5802 } 5803 } 5804 5805 private static class PinnedStackAnimationListener<T extends BaseActivity> extends 5806 IPipAnimationListener.Stub { 5807 @Nullable 5808 private T mActivity; 5809 @Nullable 5810 private RecentsView mRecentsView; 5811 5812 public void setActivityAndRecentsView(@Nullable T activity, 5813 @Nullable RecentsView recentsView) { 5814 mActivity = activity; 5815 mRecentsView = recentsView; 5816 } 5817 5818 @Override 5819 public void onPipAnimationStarted() { 5820 MAIN_EXECUTOR.execute(() -> { 5821 // Needed for activities that auto-enter PiP, which will not trigger a remote 5822 // animation to be created 5823 if (mActivity != null) { 5824 mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); 5825 } 5826 }); 5827 } 5828 5829 @Override 5830 public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) { 5831 if (mRecentsView != null) { 5832 mRecentsView.mPipCornerRadius = cornerRadius; 5833 mRecentsView.mPipShadowRadius = shadowRadius; 5834 } 5835 } 5836 5837 @Override 5838 public void onExpandPip() { 5839 MAIN_EXECUTOR.execute(() -> { 5840 if (mRecentsView == null 5841 || mRecentsView.mSizeStrategy.getTaskbarController() == null) { 5842 return; 5843 } 5844 // Hide the task bar when leaving PiP to prevent it from flickering once 5845 // the app settles in full-screen mode. 5846 mRecentsView.mSizeStrategy.getTaskbarController().onExpandPip(); 5847 }); 5848 } 5849 } 5850 5851 /** Get the color used for foreground scrimming the RecentsView for sharing. */ 5852 public static int getForegroundScrimDimColor(Context context) { 5853 int baseColor = Themes.getAttrColor(context, R.attr.overviewScrimColor); 5854 // The Black blending is temporary until we have the proper color token. 5855 return ColorUtils.blendARGB(Color.BLACK, baseColor, 0.25f); 5856 } 5857 5858 /** Get the RecentsAnimationController */ 5859 @Nullable 5860 public RecentsAnimationController getRecentsAnimationController() { 5861 return mRecentsAnimationController; 5862 } 5863 5864 @Nullable 5865 public FloatingTaskView getFirstFloatingTaskView() { 5866 return mFirstFloatingTaskView; 5867 } 5868 5869 @Nullable 5870 public SplitInstructionsView getSplitInstructionsView() { 5871 return mSplitInstructionsView; 5872 } 5873 5874 /** Update the current activity locus id to show the enabled state of Overview */ 5875 public void updateLocusId() { 5876 String locusId = "Overview"; 5877 5878 if (mOverviewStateEnabled && mActivity.isStarted()) { 5879 locusId += "|ENABLED"; 5880 } else { 5881 locusId += "|DISABLED"; 5882 } 5883 5884 final LocusId id = new LocusId(locusId); 5885 // Set locus context is a binder call, don't want it to happen during a transition 5886 UI_HELPER_EXECUTOR.post(() -> mActivity.setLocusContext(id, Bundle.EMPTY)); 5887 } 5888 5889 public interface TaskLaunchListener { 5890 void onTaskLaunched(); 5891 } 5892 } 5893