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