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