1 /* 2 * Copyright (C) 2021 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 android.content.pm.PackageManager.FEATURE_PC; 19 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Canvas; 24 import android.graphics.Rect; 25 import android.os.Bundle; 26 import android.util.AttributeSet; 27 import android.view.LayoutInflater; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.accessibility.AccessibilityNodeInfo; 31 import android.widget.FrameLayout; 32 33 import androidx.annotation.LayoutRes; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.launcher3.BubbleTextView; 38 import com.android.launcher3.DeviceProfile; 39 import com.android.launcher3.Insettable; 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.config.FeatureFlags; 43 import com.android.launcher3.folder.FolderIcon; 44 import com.android.launcher3.icons.ThemedIconDrawable; 45 import com.android.launcher3.model.data.FolderInfo; 46 import com.android.launcher3.model.data.ItemInfo; 47 import com.android.launcher3.model.data.WorkspaceItemInfo; 48 import com.android.launcher3.util.DisplayController; 49 import com.android.launcher3.util.LauncherBindableItemsContainer; 50 import com.android.launcher3.util.Themes; 51 import com.android.launcher3.views.ActivityContext; 52 import com.android.launcher3.views.DoubleShadowBubbleTextView; 53 import com.android.launcher3.views.IconButtonView; 54 55 import java.util.function.Predicate; 56 57 /** 58 * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps. 59 */ 60 public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable, 61 DeviceProfile.OnDeviceProfileChangeListener { 62 private static final String TAG = TaskbarView.class.getSimpleName(); 63 64 private static final Rect sTmpRect = new Rect(); 65 66 private final int[] mTempOutLocation = new int[2]; 67 private final Rect mIconLayoutBounds; 68 private final int mIconTouchSize; 69 private final int mItemMarginLeftRight; 70 private final int mItemPadding; 71 private final int mFolderLeaveBehindColor; 72 private final boolean mIsRtl; 73 74 private final TaskbarActivityContext mActivityContext; 75 76 // Initialized in init. 77 private TaskbarViewController.TaskbarViewCallbacks mControllerCallbacks; 78 private View.OnClickListener mIconClickListener; 79 private View.OnLongClickListener mIconLongClickListener; 80 81 // Only non-null when the corresponding Folder is open. 82 private @Nullable FolderIcon mLeaveBehindFolderIcon; 83 84 // Only non-null when device supports having an All Apps button. 85 private @Nullable IconButtonView mAllAppsButton; 86 87 // Only non-null when device supports having an All Apps button. 88 private @Nullable View mTaskbarDivider; 89 90 private View mQsb; 91 92 private float mTransientTaskbarMinWidth; 93 94 private float mTransientTaskbarAllAppsButtonTranslationXOffset; 95 96 private boolean mShouldTryStartAlign; 97 TaskbarView(@onNull Context context)98 public TaskbarView(@NonNull Context context) { 99 this(context, null); 100 } 101 TaskbarView(@onNull Context context, @Nullable AttributeSet attrs)102 public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs) { 103 this(context, attrs, 0); 104 } 105 TaskbarView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)106 public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, 107 int defStyleAttr) { 108 this(context, attrs, defStyleAttr, 0); 109 } 110 TaskbarView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)111 public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, 112 int defStyleRes) { 113 super(context, attrs, defStyleAttr, defStyleRes); 114 mActivityContext = ActivityContext.lookupContext(context); 115 mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds(); 116 Resources resources = getResources(); 117 boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext); 118 mIsRtl = Utilities.isRtl(resources); 119 mTransientTaskbarMinWidth = mContext.getResources().getDimension( 120 R.dimen.transient_taskbar_min_width); 121 mTransientTaskbarAllAppsButtonTranslationXOffset = 122 resources.getDimension(isTransientTaskbar 123 ? R.dimen.transient_taskbar_all_apps_button_translation_x_offset 124 : R.dimen.taskbar_all_apps_button_translation_x_offset); 125 126 onDeviceProfileChanged(mActivityContext.getDeviceProfile()); 127 128 int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing); 129 int actualIconSize = mActivityContext.getDeviceProfile().taskbarIconSize; 130 131 mIconTouchSize = Math.max(actualIconSize, 132 resources.getDimensionPixelSize(R.dimen.taskbar_icon_min_touch_size)); 133 134 // We layout the icons to be of mIconTouchSize in width and height 135 mItemMarginLeftRight = actualMargin - (mIconTouchSize - actualIconSize) / 2; 136 mItemPadding = (mIconTouchSize - actualIconSize) / 2; 137 138 mFolderLeaveBehindColor = Themes.getAttrColor(mActivityContext, 139 android.R.attr.textColorTertiary); 140 141 // Needed to draw folder leave-behind when opening one. 142 setWillNotDraw(false); 143 144 if (!mActivityContext.getPackageManager().hasSystemFeature(FEATURE_PC)) { 145 mAllAppsButton = (IconButtonView) LayoutInflater.from(context) 146 .inflate(R.layout.taskbar_all_apps_button, this, false); 147 mAllAppsButton.setIconDrawable(resources.getDrawable(isTransientTaskbar 148 ? R.drawable.ic_transient_taskbar_all_apps_button 149 : R.drawable.ic_taskbar_all_apps_button)); 150 mAllAppsButton.setScaleX(mIsRtl ? -1 : 1); 151 mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); 152 mAllAppsButton.setForegroundTint( 153 mActivityContext.getColor(R.color.all_apps_button_color)); 154 155 if (FeatureFlags.ENABLE_TASKBAR_PINNING.get()) { 156 mTaskbarDivider = LayoutInflater.from(context).inflate(R.layout.taskbar_divider, 157 this, false); 158 } 159 } 160 161 // TODO: Disable touch events on QSB otherwise it can crash. 162 mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); 163 } 164 165 @Override onAttachedToWindow()166 protected void onAttachedToWindow() { 167 super.onAttachedToWindow(); 168 mActivityContext.addOnDeviceProfileChangeListener(this); 169 } 170 171 @Override onDetachedFromWindow()172 protected void onDetachedFromWindow() { 173 super.onDetachedFromWindow(); 174 mActivityContext.removeOnDeviceProfileChangeListener(this); 175 } 176 177 @Override onDeviceProfileChanged(DeviceProfile dp)178 public void onDeviceProfileChanged(DeviceProfile dp) { 179 mShouldTryStartAlign = mActivityContext.isThreeButtonNav() && dp.startAlignTaskbar; 180 } 181 182 @Override performAccessibilityActionInternal(int action, Bundle arguments)183 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 184 if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) { 185 announceForAccessibility(mContext.getString(R.string.taskbar_a11y_shown_title)); 186 } else if (action == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) { 187 announceForAccessibility(mContext.getString(R.string.taskbar_a11y_hidden_title)); 188 } 189 return super.performAccessibilityActionInternal(action, arguments); 190 191 } 192 announceAccessibilityChanges()193 protected void announceAccessibilityChanges() { 194 this.performAccessibilityAction( 195 isVisibleToUser() ? AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS 196 : AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); 197 198 ActivityContext.lookupContext(getContext()).getDragLayer() 199 .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED); 200 } 201 202 /** 203 * Returns the icon touch size. 204 */ getIconTouchSize()205 public int getIconTouchSize() { 206 return mIconTouchSize; 207 } 208 init(TaskbarViewController.TaskbarViewCallbacks callbacks)209 protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) { 210 mControllerCallbacks = callbacks; 211 mIconClickListener = mControllerCallbacks.getIconOnClickListener(); 212 mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener(); 213 214 setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener()); 215 216 if (mAllAppsButton != null) { 217 mAllAppsButton.setOnClickListener(mControllerCallbacks.getAllAppsButtonClickListener()); 218 } 219 if (mTaskbarDivider != null) { 220 //TODO(b/265434705): set long press listener 221 } 222 } 223 removeAndRecycle(View view)224 private void removeAndRecycle(View view) { 225 removeView(view); 226 view.setOnClickListener(null); 227 view.setOnLongClickListener(null); 228 if (!(view.getTag() instanceof FolderInfo)) { 229 mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view); 230 } 231 view.setTag(null); 232 } 233 234 /** 235 * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos. 236 */ updateHotseatItems(ItemInfo[] hotseatItemInfos)237 protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) { 238 int nextViewIndex = 0; 239 int numViewsAnimated = 0; 240 241 if (mAllAppsButton != null) { 242 removeView(mAllAppsButton); 243 244 if (mTaskbarDivider != null) { 245 removeView(mTaskbarDivider); 246 } 247 } 248 removeView(mQsb); 249 250 251 for (int i = 0; i < hotseatItemInfos.length; i++) { 252 ItemInfo hotseatItemInfo = hotseatItemInfos[i]; 253 if (hotseatItemInfo == null) { 254 continue; 255 } 256 257 // Replace any Hotseat views with the appropriate type if it's not already that type. 258 final int expectedLayoutResId; 259 boolean isFolder = false; 260 if (hotseatItemInfo.isPredictedItem()) { 261 expectedLayoutResId = R.layout.taskbar_predicted_app_icon; 262 } else if (hotseatItemInfo instanceof FolderInfo) { 263 expectedLayoutResId = R.layout.folder_icon; 264 isFolder = true; 265 } else { 266 expectedLayoutResId = R.layout.taskbar_app_icon; 267 } 268 269 View hotseatView = null; 270 while (nextViewIndex < getChildCount()) { 271 hotseatView = getChildAt(nextViewIndex); 272 273 // see if the view can be reused 274 if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId) 275 || (isFolder && (hotseatView.getTag() != hotseatItemInfo))) { 276 // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation, 277 // so if the info changes we need to reinflate. This should only happen if a new 278 // folder is dragged to the position that another folder previously existed. 279 removeAndRecycle(hotseatView); 280 hotseatView = null; 281 } else { 282 // View found 283 break; 284 } 285 } 286 287 if (hotseatView == null) { 288 if (isFolder) { 289 FolderInfo folderInfo = (FolderInfo) hotseatItemInfo; 290 FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId, 291 mActivityContext, this, folderInfo); 292 folderIcon.setTextVisible(false); 293 hotseatView = folderIcon; 294 } else { 295 hotseatView = inflate(expectedLayoutResId); 296 } 297 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize); 298 hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); 299 addView(hotseatView, nextViewIndex, lp); 300 } 301 302 // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index. 303 if (hotseatView instanceof BubbleTextView 304 && hotseatItemInfo instanceof WorkspaceItemInfo) { 305 BubbleTextView btv = (BubbleTextView) hotseatView; 306 WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo; 307 308 boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo); 309 btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated); 310 if (animate) { 311 numViewsAnimated++; 312 } 313 } 314 setClickAndLongClickListenersForIcon(hotseatView); 315 nextViewIndex++; 316 } 317 // Remove remaining views 318 while (nextViewIndex < getChildCount()) { 319 removeAndRecycle(getChildAt(nextViewIndex)); 320 } 321 322 if (mAllAppsButton != null) { 323 mAllAppsButton.setTranslationXForTaskbarAllAppsIcon(getChildCount() > 0 324 ? mTransientTaskbarAllAppsButtonTranslationXOffset : 0f); 325 addView(mAllAppsButton, mIsRtl ? getChildCount() : 0); 326 327 // if only all apps button present, don't include divider view. 328 if (mTaskbarDivider != null && getChildCount() > 1) { 329 addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1); 330 } 331 } 332 if (mActivityContext.getDeviceProfile().isQsbInline) { 333 addView(mQsb, mIsRtl ? getChildCount() : 0); 334 // Always set QSB to invisible after re-adding. 335 mQsb.setVisibility(View.INVISIBLE); 336 } 337 } 338 339 /** 340 * Traverse all the child views and change the background of themeIcons 341 **/ setThemedIconsBackgroundColor(int color)342 public void setThemedIconsBackgroundColor(int color) { 343 for (View icon : getIconViews()) { 344 if (icon instanceof DoubleShadowBubbleTextView) { 345 DoubleShadowBubbleTextView textView = ((DoubleShadowBubbleTextView) icon); 346 if (textView.getIcon() != null 347 && textView.getIcon() instanceof ThemedIconDrawable) { 348 ((ThemedIconDrawable) textView.getIcon()).changeBackgroundColor(color); 349 } 350 } 351 } 352 } 353 354 /** 355 * Sets OnClickListener and OnLongClickListener for the given view. 356 */ setClickAndLongClickListenersForIcon(View icon)357 public void setClickAndLongClickListenersForIcon(View icon) { 358 icon.setOnClickListener(mIconClickListener); 359 icon.setOnLongClickListener(mIconLongClickListener); 360 } 361 362 @Override onLayout(boolean changed, int left, int top, int right, int bottom)363 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 364 int count = getChildCount(); 365 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 366 int spaceNeeded = getIconLayoutWidth(); 367 // We are removing the margin from taskbar divider item in taskbar, 368 // so remove it from spacing also. 369 if (FeatureFlags.ENABLE_TASKBAR_PINNING.get() && count > 1) { 370 spaceNeeded -= mIconTouchSize; 371 } 372 int navSpaceNeeded = deviceProfile.hotseatBarEndOffset; 373 boolean layoutRtl = isLayoutRtl(); 374 int centerAlignIconEnd = right - (right - left - spaceNeeded) / 2; 375 int iconEnd; 376 377 if (mShouldTryStartAlign) { 378 // Taskbar is aligned to the start 379 int startSpacingPx = deviceProfile.inlineNavButtonsEndSpacingPx; 380 381 if (layoutRtl) { 382 iconEnd = right - startSpacingPx; 383 } else { 384 iconEnd = startSpacingPx + spaceNeeded; 385 } 386 } else { 387 iconEnd = centerAlignIconEnd; 388 } 389 390 boolean needMoreSpaceForNav = layoutRtl 391 ? navSpaceNeeded > (iconEnd - spaceNeeded) 392 : iconEnd > (right - navSpaceNeeded); 393 if (needMoreSpaceForNav) { 394 // Add offset to account for nav bar when taskbar is centered 395 int offset = layoutRtl 396 ? navSpaceNeeded - (centerAlignIconEnd - spaceNeeded) 397 : (right - navSpaceNeeded) - centerAlignIconEnd; 398 399 iconEnd = centerAlignIconEnd + offset; 400 } 401 402 sTmpRect.set(mIconLayoutBounds); 403 404 // Layout the children 405 mIconLayoutBounds.right = iconEnd; 406 mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2; 407 mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize; 408 for (int i = count; i > 0; i--) { 409 View child = getChildAt(i - 1); 410 if (child == mQsb) { 411 int qsbStart; 412 int qsbEnd; 413 if (layoutRtl) { 414 qsbStart = iconEnd + mItemMarginLeftRight; 415 qsbEnd = qsbStart + deviceProfile.hotseatQsbWidth; 416 } else { 417 qsbEnd = iconEnd - mItemMarginLeftRight; 418 qsbStart = qsbEnd - deviceProfile.hotseatQsbWidth; 419 } 420 int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2; 421 int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight; 422 child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom); 423 } else if (child == mTaskbarDivider) { 424 iconEnd += mItemMarginLeftRight; 425 int iconStart = iconEnd - mIconTouchSize; 426 child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); 427 iconEnd = iconStart + mItemMarginLeftRight; 428 } else { 429 iconEnd -= mItemMarginLeftRight; 430 int iconStart = iconEnd - mIconTouchSize; 431 child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom); 432 iconEnd = iconStart - mItemMarginLeftRight; 433 } 434 } 435 436 mIconLayoutBounds.left = iconEnd; 437 438 if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) { 439 int center = mIconLayoutBounds.centerX(); 440 int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2; 441 mIconLayoutBounds.right = center + distanceFromCenter; 442 mIconLayoutBounds.left = center - distanceFromCenter; 443 } 444 445 if (!sTmpRect.equals(mIconLayoutBounds)) { 446 mControllerCallbacks.notifyIconLayoutBoundsChanged(); 447 } 448 } 449 450 @Override onTouchEvent(MotionEvent event)451 public boolean onTouchEvent(MotionEvent event) { 452 if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) { 453 // Don't allow long pressing between icons, or above/below them. 454 return true; 455 } 456 if (mControllerCallbacks.onTouchEvent(event)) { 457 int oldAction = event.getAction(); 458 try { 459 event.setAction(MotionEvent.ACTION_CANCEL); 460 return super.onTouchEvent(event); 461 } finally { 462 event.setAction(oldAction); 463 } 464 } 465 return super.onTouchEvent(event); 466 } 467 468 /** 469 * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's 470 * touch bounds. 471 */ isEventOverAnyItem(MotionEvent ev)472 public boolean isEventOverAnyItem(MotionEvent ev) { 473 getLocationOnScreen(mTempOutLocation); 474 int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0]; 475 int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1]; 476 return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates); 477 } 478 getIconLayoutBounds()479 public Rect getIconLayoutBounds() { 480 return mIconLayoutBounds; 481 } 482 483 /** 484 * Returns the space used by the icons 485 */ getIconLayoutWidth()486 public int getIconLayoutWidth() { 487 int countExcludingQsb = getChildCount(); 488 DeviceProfile deviceProfile = mActivityContext.getDeviceProfile(); 489 if (deviceProfile.isQsbInline) { 490 countExcludingQsb--; 491 } 492 return countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize); 493 } 494 495 /** 496 * Returns the app icons currently shown in the taskbar. 497 */ getIconViews()498 public View[] getIconViews() { 499 final int count = getChildCount(); 500 View[] icons = new View[count]; 501 for (int i = 0; i < count; i++) { 502 icons[i] = getChildAt(i); 503 } 504 return icons; 505 } 506 507 /** 508 * Returns the all apps button in the taskbar. 509 */ 510 @Nullable getAllAppsButtonView()511 public View getAllAppsButtonView() { 512 return mAllAppsButton; 513 } 514 515 /** 516 * Returns the QSB in the taskbar. 517 */ getQsb()518 public View getQsb() { 519 return mQsb; 520 } 521 522 // FolderIconParent implemented methods. 523 524 @Override drawFolderLeaveBehindForIcon(FolderIcon child)525 public void drawFolderLeaveBehindForIcon(FolderIcon child) { 526 mLeaveBehindFolderIcon = child; 527 invalidate(); 528 } 529 530 @Override clearFolderLeaveBehind(FolderIcon child)531 public void clearFolderLeaveBehind(FolderIcon child) { 532 mLeaveBehindFolderIcon = null; 533 invalidate(); 534 } 535 536 // End FolderIconParent implemented methods. 537 538 @Override onDraw(Canvas canvas)539 protected void onDraw(Canvas canvas) { 540 super.onDraw(canvas); 541 if (mLeaveBehindFolderIcon != null) { 542 canvas.save(); 543 canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop()); 544 mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas, 545 mFolderLeaveBehindColor); 546 canvas.restore(); 547 } 548 } 549 inflate(@ayoutRes int layoutResId)550 private View inflate(@LayoutRes int layoutResId) { 551 return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this); 552 } 553 554 @Override setInsets(Rect insets)555 public void setInsets(Rect insets) { 556 // Ignore, we just implement Insettable to draw behind system insets. 557 } 558 areIconsVisible()559 public boolean areIconsVisible() { 560 // Consider the overall visibility 561 return getVisibility() == VISIBLE; 562 } 563 564 /** 565 * Maps {@code op} over all the child views. 566 */ mapOverItems(LauncherBindableItemsContainer.ItemOperator op)567 public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) { 568 // map over all the shortcuts on the taskbar 569 for (int i = 0; i < getChildCount(); i++) { 570 View item = getChildAt(i); 571 if (op.evaluate((ItemInfo) item.getTag(), item)) { 572 return; 573 } 574 } 575 } 576 577 /** 578 * Finds the first icon to match one of the given matchers, from highest to lowest priority. 579 * 580 * @return The first match, or All Apps button if no match was found. 581 */ getFirstMatch(Predicate<ItemInfo>.... matchers)582 public View getFirstMatch(Predicate<ItemInfo>... matchers) { 583 for (Predicate<ItemInfo> matcher : matchers) { 584 for (int i = 0; i < getChildCount(); i++) { 585 View item = getChildAt(i); 586 if (!(item.getTag() instanceof ItemInfo)) { 587 // Should only happen for All Apps button. 588 continue; 589 } 590 ItemInfo info = (ItemInfo) item.getTag(); 591 if (matcher.test(info)) { 592 return item; 593 } 594 } 595 } 596 return mAllAppsButton; 597 } 598 } 599