• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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