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