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