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