1 /* 2 * Copyright (C) 2023 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 package com.android.launcher3.taskbar; 17 18 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID; 19 20 import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.AnimatorSet; 25 import android.animation.ObjectAnimator; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Outline; 29 import android.graphics.Rect; 30 import android.icu.text.MessageFormat; 31 import android.util.AttributeSet; 32 import android.view.KeyEvent; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewOutlineProvider; 36 import android.view.ViewTreeObserver; 37 import android.view.animation.Interpolator; 38 import android.widget.HorizontalScrollView; 39 import android.widget.ImageView; 40 import android.widget.TextView; 41 42 import androidx.annotation.LayoutRes; 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.constraintlayout.widget.ConstraintLayout; 46 import androidx.core.content.res.ResourcesCompat; 47 48 import com.android.app.animation.Interpolators; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.anim.AnimatedFloat; 52 import com.android.launcher3.testing.TestLogging; 53 import com.android.launcher3.testing.shared.TestProtocol; 54 import com.android.quickstep.util.DesktopTask; 55 import com.android.quickstep.util.GroupTask; 56 57 import java.util.HashMap; 58 import java.util.List; 59 import java.util.Locale; 60 61 /** 62 * View that allows quick switching between recent tasks through keyboard alt-tab and alt-shift-tab 63 * commands. 64 */ 65 public class KeyboardQuickSwitchView extends ConstraintLayout { 66 67 private static final long OUTLINE_ANIMATION_DURATION_MS = 333; 68 private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f; 69 private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f; 70 private static final Interpolator OPEN_OUTLINE_INTERPOLATOR = 71 Interpolators.EMPHASIZED_DECELERATE; 72 private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR = 73 Interpolators.EMPHASIZED_ACCELERATE; 74 75 private static final long ALPHA_ANIMATION_DURATION_MS = 83; 76 private static final long ALPHA_ANIMATION_START_DELAY_MS = 67; 77 78 private static final long CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS = 500; 79 private static final long CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS = 333; 80 private static final float CONTENT_START_TRANSLATION_X_DP = 32; 81 private static final float CONTENT_START_TRANSLATION_Y_DP = 40; 82 private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED; 83 private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR = 84 Interpolators.EMPHASIZED_DECELERATE; 85 private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR = 86 Interpolators.EMPHASIZED_ACCELERATE; 87 88 private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83; 89 private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83; 90 91 private final AnimatedFloat mOutlineAnimationProgress = new AnimatedFloat( 92 this::invalidateOutline); 93 94 private boolean mDisplayingRecentTasks; 95 private View mNoRecentItemsPane; 96 private HorizontalScrollView mScrollView; 97 private ConstraintLayout mContent; 98 99 private int mTaskViewWidth; 100 private int mTaskViewHeight; 101 private int mSpacing; 102 private int mOutlineRadius; 103 private boolean mIsRtl; 104 105 @Nullable private AnimatorSet mOpenAnimation; 106 107 @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; 108 KeyboardQuickSwitchView(@onNull Context context)109 public KeyboardQuickSwitchView(@NonNull Context context) { 110 this(context, null); 111 } 112 KeyboardQuickSwitchView(@onNull Context context, @Nullable AttributeSet attrs)113 public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) { 114 this(context, attrs, 0); 115 } 116 KeyboardQuickSwitchView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)117 public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, 118 int defStyleAttr) { 119 this(context, attrs, defStyleAttr, 0); 120 } 121 KeyboardQuickSwitchView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)122 public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs, 123 int defStyleAttr, 124 int defStyleRes) { 125 super(context, attrs, defStyleAttr, defStyleRes); 126 } 127 128 @Override onFinishInflate()129 protected void onFinishInflate() { 130 super.onFinishInflate(); 131 mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane); 132 mScrollView = findViewById(R.id.scroll_view); 133 mContent = findViewById(R.id.content); 134 135 Resources resources = getResources(); 136 mTaskViewWidth = resources.getDimensionPixelSize( 137 R.dimen.keyboard_quick_switch_taskview_width); 138 mTaskViewHeight = resources.getDimensionPixelSize( 139 R.dimen.keyboard_quick_switch_taskview_height); 140 mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing); 141 mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius); 142 mIsRtl = Utilities.isRtl(resources); 143 } 144 createAndAddTaskView( int index, boolean isFinalView, @LayoutRes int resId, @NonNull LayoutInflater layoutInflater, @Nullable View previousView)145 private KeyboardQuickSwitchTaskView createAndAddTaskView( 146 int index, 147 boolean isFinalView, 148 @LayoutRes int resId, 149 @NonNull LayoutInflater layoutInflater, 150 @Nullable View previousView) { 151 KeyboardQuickSwitchTaskView taskView = (KeyboardQuickSwitchTaskView) layoutInflater.inflate( 152 resId, mContent, false); 153 taskView.setId(View.generateViewId()); 154 taskView.setOnClickListener(v -> mViewCallbacks.launchTaskAt(index)); 155 156 LayoutParams lp = new LayoutParams(mTaskViewWidth, mTaskViewHeight); 157 // Create a left-to-right ordering of views (or right-to-left in RTL locales) 158 if (previousView != null) { 159 lp.startToEnd = previousView.getId(); 160 } else { 161 lp.startToStart = PARENT_ID; 162 } 163 lp.topToTop = PARENT_ID; 164 lp.bottomToBottom = PARENT_ID; 165 // Add spacing between views 166 lp.setMarginStart(mSpacing); 167 if (isFinalView) { 168 // Add spacing to the end of the final view so that scrolling ends with some padding. 169 lp.endToEnd = PARENT_ID; 170 lp.setMarginEnd(mSpacing); 171 lp.horizontalBias = 1f; 172 } 173 174 mContent.addView(taskView, lp); 175 176 return taskView; 177 } 178 applyLoadPlan( @onNull Context context, @NonNull List<GroupTask> groupTasks, int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride, @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks)179 protected void applyLoadPlan( 180 @NonNull Context context, 181 @NonNull List<GroupTask> groupTasks, 182 int numHiddenTasks, 183 boolean updateTasks, 184 int currentFocusIndexOverride, 185 @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) { 186 mViewCallbacks = viewCallbacks; 187 Resources resources = context.getResources(); 188 Resources.Theme theme = context.getTheme(); 189 190 View previousTaskView = null; 191 LayoutInflater layoutInflater = LayoutInflater.from(context); 192 int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size()); 193 for (int i = 0; i < tasksToDisplay; i++) { 194 GroupTask groupTask = groupTasks.get(i); 195 KeyboardQuickSwitchTaskView currentTaskView = createAndAddTaskView( 196 i, 197 /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0, 198 groupTask instanceof DesktopTask 199 ? R.layout.keyboard_quick_switch_textonly_taskview 200 : R.layout.keyboard_quick_switch_taskview, 201 layoutInflater, 202 previousTaskView); 203 204 if (groupTask instanceof DesktopTask desktopTask) { 205 HashMap<String, Integer> args = new HashMap<>(); 206 args.put("count", desktopTask.tasks.size()); 207 208 currentTaskView.<ImageView>findViewById(R.id.icon).setImageDrawable( 209 ResourcesCompat.getDrawable(resources, R.drawable.ic_desktop, theme)); 210 currentTaskView.<TextView>findViewById(R.id.text).setText(new MessageFormat( 211 resources.getString(R.string.quick_switch_desktop), 212 Locale.getDefault()).format(args)); 213 } else { 214 currentTaskView.setThumbnails( 215 groupTask.task1, 216 groupTask.task2, 217 updateTasks ? mViewCallbacks::updateThumbnailInBackground : null, 218 updateTasks ? mViewCallbacks::updateIconInBackground : null); 219 } 220 previousTaskView = currentTaskView; 221 } 222 223 if (numHiddenTasks > 0) { 224 HashMap<String, Integer> args = new HashMap<>(); 225 args.put("count", numHiddenTasks); 226 227 View overviewButton = createAndAddTaskView( 228 MAX_TASKS, 229 /* isFinalView= */ true, 230 R.layout.keyboard_quick_switch_textonly_taskview, 231 layoutInflater, 232 previousTaskView); 233 234 overviewButton.<ImageView>findViewById(R.id.icon).setImageDrawable( 235 ResourcesCompat.getDrawable(resources, R.drawable.view_carousel, theme)); 236 overviewButton.<TextView>findViewById(R.id.text).setText(new MessageFormat( 237 resources.getString(R.string.quick_switch_overflow), 238 Locale.getDefault()).format(args)); 239 } 240 mDisplayingRecentTasks = !groupTasks.isEmpty(); 241 242 getViewTreeObserver().addOnGlobalLayoutListener( 243 new ViewTreeObserver.OnGlobalLayoutListener() { 244 @Override 245 public void onGlobalLayout() { 246 animateOpen(currentFocusIndexOverride); 247 248 getViewTreeObserver().removeOnGlobalLayoutListener(this); 249 } 250 }); 251 } 252 getCloseAnimation()253 protected Animator getCloseAnimation() { 254 AnimatorSet closeAnimation = new AnimatorSet(); 255 256 Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(0f); 257 outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS); 258 outlineAnimation.setInterpolator(CLOSE_OUTLINE_INTERPOLATOR); 259 closeAnimation.play(outlineAnimation); 260 261 Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0f); 262 alphaAnimation.setStartDelay(ALPHA_ANIMATION_START_DELAY_MS); 263 alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS); 264 closeAnimation.play(alphaAnimation); 265 266 View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane; 267 Animator translationYAnimation = ObjectAnimator.ofFloat( 268 displayedContent, 269 TRANSLATION_Y, 270 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); 271 translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); 272 translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); 273 closeAnimation.play(translationYAnimation); 274 275 Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 1f, 0f); 276 contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); 277 closeAnimation.play(contentAlphaAnimation); 278 279 closeAnimation.addListener(new AnimatorListenerAdapter() { 280 @Override 281 public void onAnimationStart(Animator animation) { 282 super.onAnimationStart(animation); 283 if (mOpenAnimation != null) { 284 mOpenAnimation.cancel(); 285 } 286 } 287 }); 288 289 return closeAnimation; 290 } 291 animateOpen(int currentFocusIndexOverride)292 private void animateOpen(int currentFocusIndexOverride) { 293 if (mOpenAnimation != null) { 294 // Restart animation since currentFocusIndexOverride can change the initial scroll. 295 mOpenAnimation.cancel(); 296 } 297 mOpenAnimation = new AnimatorSet(); 298 299 Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f); 300 outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS); 301 mOpenAnimation.play(outlineAnimation); 302 303 Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 0f, 1f); 304 alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS); 305 mOpenAnimation.play(alphaAnimation); 306 307 View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane; 308 Animator translationXAnimation = ObjectAnimator.ofFloat( 309 displayedContent, 310 TRANSLATION_X, 311 -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); 312 translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); 313 translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); 314 mOpenAnimation.play(translationXAnimation); 315 316 Animator translationYAnimation = ObjectAnimator.ofFloat( 317 displayedContent, 318 TRANSLATION_Y, 319 -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); 320 translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); 321 translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); 322 mOpenAnimation.play(translationYAnimation); 323 324 Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 0f, 1f); 325 contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); 326 contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); 327 mOpenAnimation.play(contentAlphaAnimation); 328 329 ViewOutlineProvider outlineProvider = getOutlineProvider(); 330 mOpenAnimation.addListener(new AnimatorListenerAdapter() { 331 @Override 332 public void onAnimationStart(Animator animation) { 333 super.onAnimationStart(animation); 334 setClipToPadding(false); 335 setOutlineProvider(new ViewOutlineProvider() { 336 @Override 337 public void getOutline(View view, Outline outline) { 338 outline.setRoundRect( 339 /* rect= */ new Rect( 340 /* left= */ 0, 341 /* top= */ 0, 342 /* right= */ getWidth(), 343 /* bottom= */ 344 (int) (getHeight() * Utilities.mapBoundToRange( 345 mOutlineAnimationProgress.value, 346 /* lowerBound= */ 0f, 347 /* upperBound= */ 1f, 348 /* toMin= */ OUTLINE_START_HEIGHT_FACTOR, 349 /* toMax= */ 1f, 350 OPEN_OUTLINE_INTERPOLATOR))), 351 /* radius= */ mOutlineRadius * Utilities.mapBoundToRange( 352 mOutlineAnimationProgress.value, 353 /* lowerBound= */ 0f, 354 /* upperBound= */ 1f, 355 /* toMin= */ OUTLINE_START_RADIUS_FACTOR, 356 /* toMax= */ 1f, 357 OPEN_OUTLINE_INTERPOLATOR)); 358 } 359 }); 360 animateFocusMove(-1, Math.min( 361 mContent.getChildCount() - 1, 362 currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride)); 363 displayedContent.setVisibility(VISIBLE); 364 setVisibility(VISIBLE); 365 requestFocus(); 366 } 367 368 @Override 369 public void onAnimationEnd(Animator animation) { 370 super.onAnimationEnd(animation); 371 setClipToPadding(true); 372 setOutlineProvider(outlineProvider); 373 invalidateOutline(); 374 mOpenAnimation = null; 375 } 376 }); 377 378 mOpenAnimation.start(); 379 } 380 animateFocusMove(int fromIndex, int toIndex)381 protected void animateFocusMove(int fromIndex, int toIndex) { 382 if (!mDisplayingRecentTasks) { 383 return; 384 } 385 KeyboardQuickSwitchTaskView focusedTask = getTaskAt(toIndex); 386 if (focusedTask == null) { 387 return; 388 } 389 AnimatorSet focusAnimation = new AnimatorSet(); 390 focusAnimation.play(focusedTask.getFocusAnimator(true)); 391 392 KeyboardQuickSwitchTaskView previouslyFocusedTask = getTaskAt(fromIndex); 393 if (previouslyFocusedTask != null) { 394 focusAnimation.play(previouslyFocusedTask.getFocusAnimator(false)); 395 } 396 397 focusAnimation.addListener(new AnimatorListenerAdapter() { 398 @Override 399 public void onAnimationStart(Animator animation) { 400 super.onAnimationStart(animation); 401 focusedTask.requestAccessibilityFocus(); 402 if (fromIndex == -1) { 403 int firstVisibleTaskIndex = toIndex == 0 404 ? toIndex 405 : getTaskAt(toIndex - 1) == null 406 ? toIndex : toIndex - 1; 407 // Scroll so that the previous task view is truncated as a visual hint that 408 // there are more tasks 409 initializeScroll( 410 firstVisibleTaskIndex, 411 /* shouldTruncateTarget= */ firstVisibleTaskIndex != 0 412 && firstVisibleTaskIndex != toIndex); 413 } else if (toIndex > fromIndex || toIndex == 0) { 414 // Scrolling to next task view 415 if (mIsRtl) { 416 scrollLeftTo(focusedTask); 417 } else { 418 scrollRightTo(focusedTask); 419 } 420 } else { 421 // Scrolling to previous task view 422 if (mIsRtl) { 423 scrollRightTo(focusedTask); 424 } else { 425 scrollLeftTo(focusedTask); 426 } 427 } 428 if (mViewCallbacks != null) { 429 mViewCallbacks.updateCurrentFocusIndex(toIndex); 430 } 431 } 432 }); 433 434 focusAnimation.start(); 435 } 436 437 @Override dispatchKeyEvent(KeyEvent event)438 public boolean dispatchKeyEvent(KeyEvent event) { 439 TestLogging.recordKeyEvent( 440 TestProtocol.SEQUENCE_MAIN, "KeyboardQuickSwitchView key event", event); 441 return super.dispatchKeyEvent(event); 442 } 443 444 @Override onKeyUp(int keyCode, KeyEvent event)445 public boolean onKeyUp(int keyCode, KeyEvent event) { 446 return (mViewCallbacks != null 447 && mViewCallbacks.onKeyUp(keyCode, event, mIsRtl, mDisplayingRecentTasks)) 448 || super.onKeyUp(keyCode, event); 449 } 450 initializeScroll(int index, boolean shouldTruncateTarget)451 private void initializeScroll(int index, boolean shouldTruncateTarget) { 452 if (!mDisplayingRecentTasks) { 453 return; 454 } 455 View task = getTaskAt(index); 456 if (task == null) { 457 return; 458 } 459 if (mIsRtl) { 460 scrollLeftTo( 461 task, 462 shouldTruncateTarget, 463 /* smoothScroll= */ false, 464 /* waitForLayout= */ true); 465 } else { 466 scrollRightTo( 467 task, 468 shouldTruncateTarget, 469 /* smoothScroll= */ false, 470 /* waitForLayout= */ true); 471 } 472 } 473 scrollRightTo(@onNull View targetTask)474 private void scrollRightTo(@NonNull View targetTask) { 475 scrollRightTo( 476 targetTask, 477 /* shouldTruncateTarget= */ false, 478 /* smoothScroll= */ true, 479 /* waitForLayout= */ false); 480 } 481 scrollRightTo( @onNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll, boolean waitForLayout)482 private void scrollRightTo( 483 @NonNull View targetTask, 484 boolean shouldTruncateTarget, 485 boolean smoothScroll, 486 boolean waitForLayout) { 487 if (!mDisplayingRecentTasks) { 488 return; 489 } 490 if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) { 491 return; 492 } 493 runScrollCommand(waitForLayout, () -> { 494 int scrollTo = targetTask.getLeft() - mSpacing 495 + (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0); 496 // Scroll so that the focused task is to the left of the list 497 if (smoothScroll) { 498 mScrollView.smoothScrollTo(scrollTo, 0); 499 } else { 500 mScrollView.scrollTo(scrollTo, 0); 501 } 502 }); 503 } 504 scrollLeftTo(@onNull View targetTask)505 private void scrollLeftTo(@NonNull View targetTask) { 506 scrollLeftTo( 507 targetTask, 508 /* shouldTruncateTarget= */ false, 509 /* smoothScroll= */ true, 510 /* waitForLayout= */ false); 511 } 512 scrollLeftTo( @onNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll, boolean waitForLayout)513 private void scrollLeftTo( 514 @NonNull View targetTask, 515 boolean shouldTruncateTarget, 516 boolean smoothScroll, 517 boolean waitForLayout) { 518 if (!mDisplayingRecentTasks) { 519 return; 520 } 521 if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) { 522 return; 523 } 524 runScrollCommand(waitForLayout, () -> { 525 int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth() 526 - (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0); 527 // Scroll so that the focused task is to the right of the list 528 if (smoothScroll) { 529 mScrollView.smoothScrollTo(scrollTo, 0); 530 } else { 531 mScrollView.scrollTo(scrollTo, 0); 532 } 533 }); 534 } 535 shouldScroll(@onNull View targetTask, boolean shouldTruncateTarget)536 private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) { 537 boolean isTargetTruncated = 538 targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth() 539 || Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX(); 540 541 return isTargetTruncated && !shouldTruncateTarget; 542 } 543 544 private void runScrollCommand(boolean waitForLayout, @NonNull Runnable scrollCommand) { 545 if (!waitForLayout) { 546 scrollCommand.run(); 547 return; 548 } 549 mScrollView.getViewTreeObserver().addOnGlobalLayoutListener( 550 new ViewTreeObserver.OnGlobalLayoutListener() { 551 @Override 552 public void onGlobalLayout() { 553 scrollCommand.run(); 554 mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 555 } 556 }); 557 } 558 559 @Nullable 560 protected KeyboardQuickSwitchTaskView getTaskAt(int index) { 561 return !mDisplayingRecentTasks || index < 0 || index >= mContent.getChildCount() 562 ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index); 563 } 564 } 565