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