AllAppsGridAdapter.java revision a92447a7fdcdd8392628bb15dad9730428b71160
1/* 2 * Copyright (C) 2015 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 */ 16package com.android.launcher3.allapps; 17 18import android.content.Context; 19import android.content.Intent; 20import android.content.res.Resources; 21import android.graphics.Canvas; 22import android.graphics.Paint; 23import android.graphics.PointF; 24import android.graphics.Rect; 25import android.support.v4.view.accessibility.AccessibilityEventCompat; 26import android.support.v4.view.accessibility.AccessibilityRecordCompat; 27import android.support.v7.widget.GridLayoutManager; 28import android.support.v7.widget.RecyclerView; 29import android.view.Gravity; 30import android.view.LayoutInflater; 31import android.view.View; 32import android.view.View.OnFocusChangeListener; 33import android.view.ViewConfiguration; 34import android.view.ViewGroup; 35import android.view.accessibility.AccessibilityEvent; 36import android.widget.TextView; 37 38import com.android.launcher3.AppInfo; 39import com.android.launcher3.BubbleTextView; 40import com.android.launcher3.Launcher; 41import com.android.launcher3.R; 42import com.android.launcher3.Utilities; 43import com.android.launcher3.shortcuts.DeepShortcutManager; 44import com.android.launcher3.shortcuts.ShortcutsContainerListener; 45 46import java.util.HashMap; 47import java.util.List; 48 49 50/** 51 * The grid view adapter of all the apps. 52 */ 53public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { 54 55 public static final String TAG = "AppsGridAdapter"; 56 private static final boolean DEBUG = false; 57 58 // A section break in the grid 59 public static final int VIEW_TYPE_SECTION_BREAK = 1 << 0; 60 // A normal icon 61 public static final int VIEW_TYPE_ICON = 1 << 1; 62 // A prediction icon 63 public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2; 64 // The message shown when there are no filtered results 65 public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3; 66 // The message to continue to a market search when there are no filtered results 67 public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4; 68 69 // We use various dividers for various purposes. They share enough attributes to reuse layouts, 70 // but differ in enough attributes to require different view types 71 72 // A divider that separates the apps list and the search market button 73 public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5; 74 // The divider under the search field 75 public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6; 76 // The divider that separates prediction icons from the app list 77 public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7; 78 79 // Common view type masks 80 public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER 81 | VIEW_TYPE_SEARCH_MARKET_DIVIDER 82 | VIEW_TYPE_PREDICTION_DIVIDER 83 | VIEW_TYPE_SECTION_BREAK; 84 public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON 85 | VIEW_TYPE_PREDICTION_ICON; 86 87 88 public interface BindViewCallback { 89 public void onBindView(ViewHolder holder); 90 } 91 92 /** 93 * ViewHolder for each icon. 94 */ 95 public static class ViewHolder extends RecyclerView.ViewHolder { 96 public View mContent; 97 98 public ViewHolder(View v) { 99 super(v); 100 mContent = v; 101 } 102 } 103 104 /** 105 * A subclass of GridLayoutManager that overrides accessibility values during app search. 106 */ 107 public class AppsGridLayoutManager extends GridLayoutManager { 108 109 public AppsGridLayoutManager(Context context) { 110 super(context, 1, GridLayoutManager.VERTICAL, false); 111 } 112 113 @Override 114 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 115 super.onInitializeAccessibilityEvent(event); 116 117 // Ensure that we only report the number apps for accessibility not including other 118 // adapter views 119 final AccessibilityRecordCompat record = AccessibilityEventCompat 120 .asRecord(event); 121 record.setItemCount(mApps.getNumFilteredApps()); 122 } 123 124 @Override 125 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 126 RecyclerView.State state) { 127 if (mApps.hasNoFilteredResults()) { 128 // Disregard the no-search-results text as a list item for accessibility 129 return 0; 130 } else { 131 return super.getRowCountForAccessibility(recycler, state); 132 } 133 } 134 } 135 136 /** 137 * Helper class to size the grid items. 138 */ 139 public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { 140 141 public GridSpanSizer() { 142 super(); 143 setSpanIndexCacheEnabled(true); 144 } 145 146 @Override 147 public int getSpanSize(int position) { 148 if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) { 149 return 1; 150 } else { 151 // Section breaks span the full width 152 return mAppsPerRow; 153 } 154 } 155 } 156 157 /** 158 * Helper class to draw the section headers 159 */ 160 public class GridItemDecoration extends RecyclerView.ItemDecoration { 161 162 private static final boolean DEBUG_SECTION_MARGIN = false; 163 private static final boolean FADE_OUT_SECTIONS = false; 164 165 private HashMap<String, PointF> mCachedSectionBounds = new HashMap<>(); 166 private Rect mTmpBounds = new Rect(); 167 168 @Override 169 public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 170 if (mApps.hasFilter() || mAppsPerRow == 0) { 171 return; 172 } 173 174 if (DEBUG_SECTION_MARGIN) { 175 Paint p = new Paint(); 176 p.setColor(0x33ff0000); 177 c.drawRect(mBackgroundPadding.left, 0, mBackgroundPadding.left + mSectionNamesMargin, 178 parent.getMeasuredHeight(), p); 179 } 180 181 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 182 boolean showSectionNames = mSectionNamesMargin > 0; 183 int childCount = parent.getChildCount(); 184 int lastSectionTop = 0; 185 int lastSectionHeight = 0; 186 for (int i = 0; i < childCount; i++) { 187 View child = parent.getChildAt(i); 188 ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child); 189 if (!isValidHolderAndChild(holder, child, items)) { 190 continue; 191 } 192 193 if (showSectionNames && shouldDrawItemSection(holder, i, items)) { 194 // At this point, we only draw sections for each section break; 195 int viewTopOffset = (2 * child.getPaddingTop()); 196 int pos = holder.getPosition(); 197 AlphabeticalAppsList.AdapterItem item = items.get(pos); 198 AlphabeticalAppsList.SectionInfo sectionInfo = item.sectionInfo; 199 200 // Draw all the sections for this index 201 String lastSectionName = item.sectionName; 202 for (int j = item.sectionAppIndex; j < sectionInfo.numApps; j++, pos++) { 203 AlphabeticalAppsList.AdapterItem nextItem = items.get(pos); 204 String sectionName = nextItem.sectionName; 205 if (nextItem.sectionInfo != sectionInfo) { 206 break; 207 } 208 if (j > item.sectionAppIndex && sectionName.equals(lastSectionName)) { 209 continue; 210 } 211 212 // Find the section name bounds 213 PointF sectionBounds = getAndCacheSectionBounds(sectionName); 214 215 // Calculate where to draw the section 216 int sectionBaseline = (int) (viewTopOffset + sectionBounds.y); 217 int x = mIsRtl ? 218 parent.getWidth() - mBackgroundPadding.left - mSectionNamesMargin : 219 mBackgroundPadding.left; 220 x += (int) ((mSectionNamesMargin - sectionBounds.x) / 2f); 221 int y = child.getTop() + sectionBaseline; 222 223 // Determine whether this is the last row with apps in that section, if 224 // so, then fix the section to the row allowing it to scroll past the 225 // baseline, otherwise, bound it to the baseline so it's in the viewport 226 int appIndexInSection = items.get(pos).sectionAppIndex; 227 int nextRowPos = Math.min(items.size() - 1, 228 pos + mAppsPerRow - (appIndexInSection % mAppsPerRow)); 229 AlphabeticalAppsList.AdapterItem nextRowItem = items.get(nextRowPos); 230 boolean fixedToRow = !sectionName.equals(nextRowItem.sectionName); 231 if (!fixedToRow) { 232 y = Math.max(sectionBaseline, y); 233 } 234 235 // In addition, if it overlaps with the last section that was drawn, then 236 // offset it so that it does not overlap 237 if (lastSectionHeight > 0 && y <= (lastSectionTop + lastSectionHeight)) { 238 y += lastSectionTop - y + lastSectionHeight; 239 } 240 241 // Draw the section header 242 if (FADE_OUT_SECTIONS) { 243 int alpha = 255; 244 if (fixedToRow) { 245 alpha = Math.min(255, 246 (int) (255 * (Math.max(0, y) / (float) sectionBaseline))); 247 } 248 mSectionTextPaint.setAlpha(alpha); 249 } 250 c.drawText(sectionName, x, y, mSectionTextPaint); 251 252 lastSectionTop = y; 253 lastSectionHeight = (int) (sectionBounds.y + mSectionHeaderOffset); 254 lastSectionName = sectionName; 255 } 256 i += (sectionInfo.numApps - item.sectionAppIndex); 257 } 258 } 259 } 260 261 @Override 262 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 263 RecyclerView.State state) { 264 // Do nothing 265 } 266 267 /** 268 * Given a section name, return the bounds of the given section name. 269 */ 270 private PointF getAndCacheSectionBounds(String sectionName) { 271 PointF bounds = mCachedSectionBounds.get(sectionName); 272 if (bounds == null) { 273 mSectionTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTmpBounds); 274 bounds = new PointF(mSectionTextPaint.measureText(sectionName), mTmpBounds.height()); 275 mCachedSectionBounds.put(sectionName, bounds); 276 } 277 return bounds; 278 } 279 280 /** 281 * Returns whether we consider this a valid view holder for us to draw a divider or section for. 282 */ 283 private boolean isValidHolderAndChild(ViewHolder holder, View child, 284 List<AlphabeticalAppsList.AdapterItem> items) { 285 // Ensure item is not already removed 286 GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) 287 child.getLayoutParams(); 288 if (lp.isItemRemoved()) { 289 return false; 290 } 291 // Ensure we have a valid holder 292 if (holder == null) { 293 return false; 294 } 295 // Ensure we have a holder position 296 int pos = holder.getPosition(); 297 if (pos < 0 || pos >= items.size()) { 298 return false; 299 } 300 return true; 301 } 302 303 /** 304 * Returns whether to draw the section for the given child. 305 */ 306 private boolean shouldDrawItemSection(ViewHolder holder, int childIndex, 307 List<AlphabeticalAppsList.AdapterItem> items) { 308 int pos = holder.getPosition(); 309 AlphabeticalAppsList.AdapterItem item = items.get(pos); 310 311 // Ensure it's an icon 312 if (item.viewType != AllAppsGridAdapter.VIEW_TYPE_ICON) { 313 return false; 314 } 315 // Draw the section header for the first item in each section 316 return (childIndex == 0) || 317 (items.get(pos - 1).viewType == AllAppsGridAdapter.VIEW_TYPE_SECTION_BREAK); 318 } 319 } 320 321 private final Launcher mLauncher; 322 private final LayoutInflater mLayoutInflater; 323 private final AlphabeticalAppsList mApps; 324 private final GridLayoutManager mGridLayoutMgr; 325 private final GridSpanSizer mGridSizer; 326 private final GridItemDecoration mItemDecoration; 327 private final View.OnClickListener mIconClickListener; 328 private final View.OnLongClickListener mIconLongClickListener; 329 330 private final Rect mBackgroundPadding = new Rect(); 331 private final boolean mIsRtl; 332 333 // Section drawing 334 private final int mSectionNamesMargin; 335 private final int mSectionHeaderOffset; 336 private final Paint mSectionTextPaint; 337 338 private int mAppsPerRow; 339 340 private BindViewCallback mBindViewCallback; 341 private AllAppsSearchBarController mSearchController; 342 private OnFocusChangeListener mIconFocusListener; 343 344 // The text to show when there are no search results and no market search handler. 345 private String mEmptySearchMessage; 346 // The intent to send off to the market app, updated each time the search query changes. 347 private Intent mMarketSearchIntent; 348 349 public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener 350 iconClickListener, View.OnLongClickListener iconLongClickListener) { 351 Resources res = launcher.getResources(); 352 mLauncher = launcher; 353 mApps = apps; 354 mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); 355 mGridSizer = new GridSpanSizer(); 356 mGridLayoutMgr = new AppsGridLayoutManager(launcher); 357 mGridLayoutMgr.setSpanSizeLookup(mGridSizer); 358 mItemDecoration = new GridItemDecoration(); 359 mLayoutInflater = LayoutInflater.from(launcher); 360 mIconClickListener = iconClickListener; 361 mIconLongClickListener = iconLongClickListener; 362 mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); 363 mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset); 364 mIsRtl = Utilities.isRtl(res); 365 366 mSectionTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 367 mSectionTextPaint.setTextSize(res.getDimensionPixelSize( 368 R.dimen.all_apps_grid_section_text_size)); 369 mSectionTextPaint.setColor(Utilities.getColorAccent(launcher)); 370 } 371 372 public static boolean isDividerViewType(int viewType) { 373 return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); 374 } 375 376 public static boolean isIconViewType(int viewType) { 377 return isViewType(viewType, VIEW_TYPE_MASK_ICON); 378 } 379 380 public static boolean isViewType(int viewType, int viewTypeMask) { 381 return (viewType & viewTypeMask) != 0; 382 } 383 384 /** 385 * Sets the number of apps per row. 386 */ 387 public void setNumAppsPerRow(int appsPerRow) { 388 mAppsPerRow = appsPerRow; 389 mGridLayoutMgr.setSpanCount(appsPerRow); 390 } 391 392 public void setSearchController(AllAppsSearchBarController searchController) { 393 mSearchController = searchController; 394 } 395 396 public void setIconFocusListener(OnFocusChangeListener focusListener) { 397 mIconFocusListener = focusListener; 398 } 399 400 /** 401 * Sets the last search query that was made, used to show when there are no results and to also 402 * seed the intent for searching the market. 403 */ 404 public void setLastSearchQuery(String query) { 405 Resources res = mLauncher.getResources(); 406 mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); 407 mMarketSearchIntent = mSearchController.createMarketSearchIntent(query); 408 } 409 410 /** 411 * Sets the callback for when views are bound. 412 */ 413 public void setBindViewCallback(BindViewCallback cb) { 414 mBindViewCallback = cb; 415 } 416 417 /** 418 * Notifies the adapter of the background padding so that it can draw things correctly in the 419 * item decorator. 420 */ 421 public void updateBackgroundPadding(Rect padding) { 422 mBackgroundPadding.set(padding); 423 } 424 425 /** 426 * Returns the grid layout manager. 427 */ 428 public GridLayoutManager getLayoutManager() { 429 return mGridLayoutMgr; 430 } 431 432 /** 433 * Returns the item decoration for the recycler view. 434 */ 435 public RecyclerView.ItemDecoration getItemDecoration() { 436 // We don't draw any headers when we are uncomfortably dense 437 return mItemDecoration; 438 } 439 440 @Override 441 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 442 switch (viewType) { 443 case VIEW_TYPE_SECTION_BREAK: 444 return new ViewHolder(new View(parent.getContext())); 445 case VIEW_TYPE_ICON: 446 /* falls through */ 447 case VIEW_TYPE_PREDICTION_ICON: { 448 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 449 R.layout.all_apps_icon, parent, false); 450 icon.setOnClickListener(mIconClickListener); 451 icon.setOnLongClickListener(mIconLongClickListener); 452 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) 453 .getLongPressTimeout()); 454 icon.setOnFocusChangeListener(mIconFocusListener); 455 return new ViewHolder(icon); 456 } 457 case VIEW_TYPE_EMPTY_SEARCH: 458 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 459 parent, false)); 460 case VIEW_TYPE_SEARCH_MARKET: 461 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 462 parent, false); 463 searchMarketView.setOnClickListener(new View.OnClickListener() { 464 @Override 465 public void onClick(View v) { 466 mLauncher.startActivitySafely(v, mMarketSearchIntent, null); 467 } 468 }); 469 return new ViewHolder(searchMarketView); 470 case VIEW_TYPE_SEARCH_DIVIDER: 471 return new ViewHolder(mLayoutInflater.inflate( 472 R.layout.all_apps_search_divider, parent, false)); 473 case VIEW_TYPE_PREDICTION_DIVIDER: 474 /* falls through */ 475 case VIEW_TYPE_SEARCH_MARKET_DIVIDER: 476 return new ViewHolder(mLayoutInflater.inflate( 477 R.layout.all_apps_divider, parent, false)); 478 default: 479 throw new RuntimeException("Unexpected view type"); 480 } 481 } 482 483 @Override 484 public void onBindViewHolder(ViewHolder holder, int position) { 485 switch (holder.getItemViewType()) { 486 case VIEW_TYPE_ICON: { 487 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 488 BubbleTextView icon = (BubbleTextView) holder.mContent; 489 icon.applyFromApplicationInfo(info); 490 if (DeepShortcutManager.supportsShortcuts(info)) { 491 // TODO: only add this listener if the item has shortcuts associated with it. 492 icon.setOnTouchListener(new ShortcutsContainerListener(icon)); 493 } 494 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 495 break; 496 } 497 case VIEW_TYPE_PREDICTION_ICON: { 498 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 499 BubbleTextView icon = (BubbleTextView) holder.mContent; 500 icon.applyFromApplicationInfo(info); 501 if (DeepShortcutManager.supportsShortcuts(info)) { 502 // TODO: only add this listener if the item has shortcuts associated with it. 503 icon.setOnTouchListener(new ShortcutsContainerListener(icon)); 504 } 505 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 506 break; 507 } 508 case VIEW_TYPE_EMPTY_SEARCH: 509 TextView emptyViewText = (TextView) holder.mContent; 510 emptyViewText.setText(mEmptySearchMessage); 511 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 512 Gravity.START | Gravity.CENTER_VERTICAL); 513 break; 514 case VIEW_TYPE_SEARCH_MARKET: 515 TextView searchView = (TextView) holder.mContent; 516 if (mMarketSearchIntent != null) { 517 searchView.setVisibility(View.VISIBLE); 518 } else { 519 searchView.setVisibility(View.GONE); 520 } 521 break; 522 } 523 if (mBindViewCallback != null) { 524 mBindViewCallback.onBindView(holder); 525 } 526 } 527 528 @Override 529 public boolean onFailedToRecycleView(ViewHolder holder) { 530 // Always recycle and we will reset the view when it is bound 531 return true; 532 } 533 534 @Override 535 public int getItemCount() { 536 return mApps.getAdapterItems().size(); 537 } 538 539 @Override 540 public int getItemViewType(int position) { 541 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 542 return item.viewType; 543 } 544} 545