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