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