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