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