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