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