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