AllAppsRecyclerView.java revision 5f4e0fdd2e4edeb9211e2dcd1c99497f175731f8
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.animation.ObjectAnimator;
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.support.v7.widget.LinearLayoutManager;
27import android.support.v7.widget.RecyclerView;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewConfiguration;
32import com.android.launcher3.BaseRecyclerView;
33import com.android.launcher3.DeviceProfile;
34import com.android.launcher3.Launcher;
35import com.android.launcher3.R;
36import com.android.launcher3.Utilities;
37
38import java.util.List;
39
40/**
41 * A RecyclerView with custom fastscroll support.  This is the main container for the all apps
42 * icons.
43 */
44public class AllAppsRecyclerView extends BaseRecyclerView {
45
46    /**
47     * The current scroll state of the recycler view.  We use this in updateVerticalScrollbarBounds()
48     * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
49     * that we can calculate what the scroll bar looks like, and where to jump to from the fast
50     * scroller.
51     */
52    private static class ScrollPositionState {
53        // The index of the first visible row
54        int rowIndex;
55        // The offset of the first visible row
56        int rowTopOffset;
57        // The height of a given row (they are currently all the same height)
58        int rowHeight;
59    }
60
61    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
62
63    private AlphabeticalAppsList mApps;
64    private int mNumAppsPerRow;
65    private int mNumPredictedAppsPerRow;
66
67    private Drawable mScrollbar;
68    private Drawable mFastScrollerBg;
69    private Rect mTmpFastScrollerInvalidateRect = new Rect();
70    private Rect mFastScrollerBounds = new Rect();
71    private Rect mVerticalScrollbarBounds = new Rect();
72    private boolean mDraggingFastScroller;
73    private String mFastScrollSectionName;
74    private Paint mFastScrollTextPaint;
75    private Rect mFastScrollTextBounds = new Rect();
76    private float mFastScrollAlpha;
77    private int mPredictionBarHeight;
78    private int mDownX;
79    private int mDownY;
80    private int mLastX;
81    private int mLastY;
82    private int mScrollbarWidth;
83    private int mScrollbarMinHeight;
84    private int mScrollbarInset;
85    private Rect mBackgroundPadding = new Rect();
86    private ScrollPositionState mScrollPosState = new ScrollPositionState();
87
88    private Launcher mLauncher;
89
90    public AllAppsRecyclerView(Context context) {
91        this(context, null);
92    }
93
94    public AllAppsRecyclerView(Context context, AttributeSet attrs) {
95        this(context, attrs, 0);
96    }
97
98    public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
99        this(context, attrs, defStyleAttr, 0);
100    }
101
102    public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
103            int defStyleRes) {
104        super(context, attrs, defStyleAttr);
105
106        mLauncher = (Launcher) context;
107        Resources res = context.getResources();
108        int fastScrollerSize = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_popup_size);
109        mScrollbar = res.getDrawable(R.drawable.all_apps_scrollbar_thumb);
110        mFastScrollerBg = res.getDrawable(R.drawable.all_apps_fastscroll_bg);
111        mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
112        mFastScrollTextPaint = new Paint();
113        mFastScrollTextPaint.setColor(Color.WHITE);
114        mFastScrollTextPaint.setAntiAlias(true);
115        mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
116                R.dimen.all_apps_fast_scroll_text_size));
117        mScrollbarWidth = res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_width);
118        mScrollbarMinHeight =
119                res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_bar_min_height);
120        mScrollbarInset =
121                res.getDimensionPixelSize(R.dimen.all_apps_fast_scroll_scrubber_touch_inset);
122        setFastScrollerAlpha(getFastScrollerAlpha());
123        setOverScrollMode(View.OVER_SCROLL_NEVER);
124    }
125
126    /**
127     * Sets the list of apps in this view, used to determine the fastscroll position.
128     */
129    public void setApps(AlphabeticalAppsList apps) {
130        mApps = apps;
131    }
132
133    /**
134     * Sets the number of apps per row in this recycler view.
135     */
136    public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
137        mNumAppsPerRow = numAppsPerRow;
138        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
139
140        DeviceProfile grid = mLauncher.getDeviceProfile();
141        RecyclerView.RecycledViewPool pool = getRecycledViewPool();
142        int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
143        pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_BAR_SPACER_TYPE, 1);
144        pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
145        pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
146        pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
147    }
148
149    public void updateBackgroundPadding(Drawable background) {
150        background.getPadding(mBackgroundPadding);
151    }
152
153    /**
154     * Sets the prediction bar height.
155     */
156    public void setPredictionBarHeight(int height) {
157        mPredictionBarHeight = height;
158    }
159
160    /**
161     * Sets the fast scroller alpha.
162     */
163    public void setFastScrollerAlpha(float alpha) {
164        mFastScrollAlpha = alpha;
165        invalidateFastScroller(mFastScrollerBounds);
166    }
167
168    /**
169     * Gets the fast scroller alpha.
170     */
171    public float getFastScrollerAlpha() {
172        return mFastScrollAlpha;
173    }
174
175    /**
176     * Returns the scroll bar width.
177     */
178    public int getScrollbarWidth() {
179        return mScrollbarWidth;
180    }
181
182    /**
183     * Scrolls this recycler view to the top.
184     */
185    public void scrollToTop() {
186        scrollToPosition(0);
187    }
188
189    /**
190     * Returns the current scroll position.
191     */
192    public int getScrollPosition() {
193        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
194        getCurScrollState(mScrollPosState, items);
195        if (mScrollPosState.rowIndex != -1) {
196            int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
197            return getPaddingTop() + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) +
198                    predictionBarHeight - mScrollPosState.rowTopOffset;
199        }
200        return 0;
201    }
202
203    @Override
204    protected void onFinishInflate() {
205        super.onFinishInflate();
206        addOnItemTouchListener(this);
207    }
208
209    @Override
210    protected void dispatchDraw(Canvas canvas) {
211        super.dispatchDraw(canvas);
212        drawVerticalScrubber(canvas);
213        drawFastScrollerPopup(canvas);
214    }
215
216    /**
217     * We intercept the touch handling only to support fast scrolling when initiated from the
218     * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
219     */
220    @Override
221    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
222        return handleTouchEvent(ev);
223    }
224
225    @Override
226    public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
227        handleTouchEvent(ev);
228    }
229
230    /**
231     * Handles the touch event and determines whether to show the fast scroller (or updates it if
232     * it is already showing).
233     */
234    private boolean handleTouchEvent(MotionEvent ev) {
235        ViewConfiguration config = ViewConfiguration.get(getContext());
236
237        int action = ev.getAction();
238        int x = (int) ev.getX();
239        int y = (int) ev.getY();
240        switch (action) {
241            case MotionEvent.ACTION_DOWN:
242                // Keep track of the down positions
243                mDownX = mLastX = x;
244                mDownY = mLastY = y;
245                if (shouldStopScroll(ev)) {
246                    stopScroll();
247                }
248                break;
249            case MotionEvent.ACTION_MOVE:
250                // Check if we are scrolling
251                if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
252                        Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
253                    getParent().requestDisallowInterceptTouchEvent(true);
254                    mDraggingFastScroller = true;
255                    animateFastScrollerVisibility(true);
256                }
257                if (mDraggingFastScroller) {
258                    mLastX = x;
259                    mLastY = y;
260
261                    // Scroll to the right position, and update the section name
262                    int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
263                    int bottom = getHeight() - getPaddingBottom() -
264                            (mFastScrollerBg.getBounds().height() / 2);
265                    float boundedY = (float) Math.max(top, Math.min(bottom, y));
266                    mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
267                            (bottom - top));
268
269                    // Combine the old and new fast scroller bounds to create the full invalidate
270                    // rect
271                    mTmpFastScrollerInvalidateRect.set(mFastScrollerBounds);
272                    updateFastScrollerBounds();
273                    mTmpFastScrollerInvalidateRect.union(mFastScrollerBounds);
274                    invalidateFastScroller(mTmpFastScrollerInvalidateRect);
275                }
276                break;
277            case MotionEvent.ACTION_UP:
278            case MotionEvent.ACTION_CANCEL:
279                mDraggingFastScroller = false;
280                animateFastScrollerVisibility(false);
281                break;
282        }
283        return mDraggingFastScroller;
284    }
285
286    /**
287     * Animates the visibility of the fast scroller popup.
288     */
289    private void animateFastScrollerVisibility(boolean visible) {
290        ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
291        anim.setDuration(visible ? 200 : 150);
292        anim.start();
293    }
294
295    /**
296     * Returns whether a given point is near the scrollbar.
297     */
298    private boolean isPointNearScrollbar(int x, int y) {
299        // Check if we are scrolling
300        updateVerticalScrollbarBounds();
301        mVerticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
302        return mVerticalScrollbarBounds.contains(x, y);
303    }
304
305    /**
306     * Draws the fast scroller popup.
307     */
308    private void drawFastScrollerPopup(Canvas canvas) {
309        if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
310            // Draw the fast scroller popup
311            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
312            canvas.translate(mFastScrollerBounds.left, mFastScrollerBounds.top);
313            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
314            mFastScrollerBg.draw(canvas);
315            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
316            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
317                    mFastScrollSectionName.length(), mFastScrollTextBounds);
318            float textWidth = mFastScrollTextPaint.measureText(mFastScrollSectionName);
319            canvas.drawText(mFastScrollSectionName,
320                    (mFastScrollerBounds.width() - textWidth) / 2,
321                    mFastScrollerBounds.height() -
322                            (mFastScrollerBounds.height() - mFastScrollTextBounds.height()) / 2,
323                    mFastScrollTextPaint);
324            canvas.restoreToCount(restoreCount);
325        }
326    }
327
328    /**
329     * Draws the vertical scrollbar.
330     */
331    private void drawVerticalScrubber(Canvas canvas) {
332        updateVerticalScrollbarBounds();
333
334        // Draw the scroll bar
335        int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
336        canvas.translate(mVerticalScrollbarBounds.left, mVerticalScrollbarBounds.top);
337        mScrollbar.setBounds(0, 0, mScrollbarWidth, mVerticalScrollbarBounds.height());
338        mScrollbar.draw(canvas);
339        canvas.restoreToCount(restoreCount);
340    }
341
342    /**
343     * Invalidates the fast scroller popup.
344     */
345    private void invalidateFastScroller(Rect bounds) {
346        invalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
347    }
348
349    /**
350     * Maps the touch (from 0..1) to the adapter position that should be visible.
351     */
352    private String scrollToPositionAtProgress(float touchFraction) {
353        // Ensure that we have any sections
354        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
355                mApps.getFastScrollerSections();
356        if (fastScrollSections.isEmpty()) {
357            return "";
358        }
359
360        // Stop the scroller if it is scrolling
361        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
362        stopScroll();
363
364        // If there is a prediction bar, then capture the appropriate area for the prediction bar
365        float predictionBarFraction = 0f;
366        if (!mApps.getPredictedApps().isEmpty()) {
367            predictionBarFraction = (float) mNumPredictedAppsPerRow / mApps.getSize();
368            if (touchFraction <= predictionBarFraction) {
369                // Scroll to the top of the view, where the prediction bar is
370                layoutManager.scrollToPositionWithOffset(0, 0);
371                return "";
372            }
373        }
374
375        // Since the app ranges are from 0..1, we need to map the touch fraction back to 0..1 from
376        // predictionBarFraction..1
377        touchFraction = (touchFraction - predictionBarFraction) *
378                (1f / (1f - predictionBarFraction));
379        AlphabeticalAppsList.FastScrollSectionInfo lastScrollSection = fastScrollSections.get(0);
380        for (int i = 1; i < fastScrollSections.size(); i++) {
381            AlphabeticalAppsList.FastScrollSectionInfo scrollSection = fastScrollSections.get(i);
382            if (lastScrollSection.appRangeFraction <= touchFraction &&
383                    touchFraction < scrollSection.appRangeFraction) {
384                break;
385            }
386            lastScrollSection = scrollSection;
387        }
388
389        // Scroll to the view at the position, anchored at the top of the screen. We call the scroll
390        // method on the LayoutManager directly since it is not exposed by RecyclerView.
391        layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
392
393        return lastScrollSection.sectionName;
394    }
395
396    /**
397     * Updates the bounds for the scrollbar.
398     */
399    private void updateVerticalScrollbarBounds() {
400        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
401
402        // Skip early if there are no items
403        if (items.isEmpty()) {
404            mVerticalScrollbarBounds.setEmpty();
405            return;
406        }
407
408        // Find the index and height of the first visible row (all rows have the same height)
409        int x;
410        int y;
411        int predictionBarHeight = mApps.getPredictedApps().isEmpty() ? 0 : mPredictionBarHeight;
412        int rowCount = getNumRows();
413        getCurScrollState(mScrollPosState, items);
414        if (mScrollPosState.rowIndex != -1) {
415            int height = getHeight() - getPaddingTop() - getPaddingBottom();
416            int totalScrollHeight = rowCount * mScrollPosState.rowHeight + predictionBarHeight;
417            if (totalScrollHeight > height) {
418                int scrollbarHeight = Math.max(mScrollbarMinHeight,
419                        (int) (height / ((float) totalScrollHeight / height)));
420
421                // Calculate the position and size of the scroll bar
422                if (Utilities.isRtl(getResources())) {
423                    x = mBackgroundPadding.left;
424                } else {
425                    x = getWidth() - mBackgroundPadding.right - mScrollbarWidth;
426                }
427
428                // To calculate the offset, we compute the percentage of the total scrollable height
429                // that the user has already scrolled and then map that to the scroll bar bounds
430                int availableY = totalScrollHeight - height;
431                int availableScrollY = height - scrollbarHeight;
432                y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) + predictionBarHeight
433                        - mScrollPosState.rowTopOffset;
434                y = getPaddingTop() +
435                        (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
436
437                mVerticalScrollbarBounds.set(x, y, x + mScrollbarWidth, y + scrollbarHeight);
438                return;
439            }
440        }
441        mVerticalScrollbarBounds.setEmpty();
442    }
443
444    /**
445     * Updates the bounds for the fast scroller.
446     */
447    private void updateFastScrollerBounds() {
448        if (mFastScrollAlpha > 0f && !mFastScrollSectionName.isEmpty()) {
449            int x;
450            int y;
451
452            // Calculate the position for the fast scroller popup
453            Rect bgBounds = mFastScrollerBg.getBounds();
454            if (Utilities.isRtl(getResources())) {
455                x = mBackgroundPadding.left + getScrollBarSize();
456            } else {
457                x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
458            }
459            y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
460            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
461                    bgBounds.height()));
462            mFastScrollerBounds.set(x, y, x + bgBounds.width(), y + bgBounds.height());
463        } else {
464            mFastScrollerBounds.setEmpty();
465        }
466    }
467
468    /**
469     * Returns the row index for a app index in the list.
470     */
471    private int findRowForAppIndex(int index) {
472        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
473        int appIndex = 0;
474        int rowCount = 0;
475        for (AlphabeticalAppsList.SectionInfo info : sections) {
476            int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow);
477            if (appIndex + info.numApps > index) {
478                return rowCount + ((index - appIndex) / mNumAppsPerRow);
479            }
480            appIndex += info.numApps;
481            rowCount += numRowsInSection;
482        }
483        return appIndex;
484    }
485
486    /**
487     * Returns the total number of rows in the list.
488     */
489    private int getNumRows() {
490        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
491        int rowCount = 0;
492        for (AlphabeticalAppsList.SectionInfo info : sections) {
493            int numRowsInSection = (int) Math.ceil((float) info.numApps / mNumAppsPerRow);
494            rowCount += numRowsInSection;
495        }
496        return rowCount;
497    }
498
499    /**
500     * Returns the current scroll state.
501     */
502    private void getCurScrollState(ScrollPositionState stateOut,
503            List<AlphabeticalAppsList.AdapterItem> items) {
504        stateOut.rowIndex = -1;
505        stateOut.rowTopOffset = -1;
506        stateOut.rowHeight = -1;
507
508        // Return early if there are no items
509        if (items.isEmpty()) {
510            return;
511        }
512
513        int childCount = getChildCount();
514        for (int i = 0; i < childCount; i++) {
515            View child = getChildAt(i);
516            int position = getChildPosition(child);
517            if (position != NO_POSITION) {
518                AlphabeticalAppsList.AdapterItem item = items.get(position);
519                if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) {
520                    stateOut.rowIndex = findRowForAppIndex(item.appIndex);
521                    stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
522                    stateOut.rowHeight = child.getHeight();
523                    break;
524                }
525            }
526        }
527    }
528}
529