AllAppsRecyclerView.java revision 1ae7a5018b48dba562bc18821f0f1e778192ee85
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.graphics.Canvas;
20import android.os.Bundle;
21import android.support.v7.widget.LinearLayoutManager;
22import android.support.v7.widget.RecyclerView;
23import android.util.AttributeSet;
24import android.view.View;
25
26import com.android.launcher3.BaseRecyclerView;
27import com.android.launcher3.BaseRecyclerViewFastScrollBar;
28import com.android.launcher3.DeviceProfile;
29import com.android.launcher3.Stats;
30import com.android.launcher3.Utilities;
31import com.android.launcher3.util.Thunk;
32
33import java.util.List;
34
35/**
36 * A RecyclerView with custom fast scroll support for the all apps view.
37 */
38public class AllAppsRecyclerView extends BaseRecyclerView
39        implements Stats.LaunchSourceProvider {
40
41    private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
42    private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
43
44    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
45    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
46
47    private AlphabeticalAppsList mApps;
48    private int mNumAppsPerRow;
49
50    @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
51    @Thunk int mPrevFastScrollFocusedPosition;
52    @Thunk int mFastScrollFrameIndex;
53    @Thunk final int[] mFastScrollFrames = new int[10];
54
55    private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
56    private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
57
58    private ScrollPositionState mScrollPosState = new ScrollPositionState();
59
60    public AllAppsRecyclerView(Context context) {
61        this(context, null);
62    }
63
64    public AllAppsRecyclerView(Context context, AttributeSet attrs) {
65        this(context, attrs, 0);
66    }
67
68    public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
69        this(context, attrs, defStyleAttr, 0);
70    }
71
72    public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
73            int defStyleRes) {
74        super(context, attrs, defStyleAttr);
75    }
76
77    /**
78     * Sets the list of apps in this view, used to determine the fastscroll position.
79     */
80    public void setApps(AlphabeticalAppsList apps) {
81        mApps = apps;
82    }
83
84    /**
85     * Sets the number of apps per row in this recycler view.
86     */
87    public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
88        mNumAppsPerRow = numAppsPerRow;
89
90        RecyclerView.RecycledViewPool pool = getRecycledViewPool();
91        int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
92        pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
93        pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
94        pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow);
95        pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
96    }
97
98    /**
99     * Scrolls this recycler view to the top.
100     */
101    public void scrollToTop() {
102        scrollToPosition(0);
103    }
104
105    /**
106     * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
107     * background bounds.
108     */
109    @Override
110    protected void dispatchDraw(Canvas canvas) {
111        canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
112                getWidth() - mBackgroundPadding.right,
113                getHeight() - mBackgroundPadding.bottom);
114        super.dispatchDraw(canvas);
115    }
116
117    @Override
118    protected void onFinishInflate() {
119        super.onFinishInflate();
120
121        // Bind event handlers
122        addOnItemTouchListener(this);
123    }
124
125    @Override
126    public void fillInLaunchSourceData(Bundle sourceData) {
127        sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
128        if (mApps.hasFilter()) {
129            sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
130                    Stats.SUB_CONTAINER_ALL_APPS_SEARCH);
131        } else {
132            sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
133                    Stats.SUB_CONTAINER_ALL_APPS_A_Z);
134        }
135    }
136
137    /**
138     * Maps the touch (from 0..1) to the adapter position that should be visible.
139     */
140    @Override
141    public String scrollToPositionAtProgress(float touchFraction) {
142        int rowCount = mApps.getNumAppRows();
143        if (rowCount == 0) {
144            return "";
145        }
146
147        // Stop the scroller if it is scrolling
148        stopScroll();
149
150        // Find the fastscroll section that maps to this touch fraction
151        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
152                mApps.getFastScrollerSections();
153        AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
154        if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
155            for (int i = 1; i < fastScrollSections.size(); i++) {
156                AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
157                if (info.touchFraction > touchFraction) {
158                    break;
159                }
160                lastInfo = info;
161            }
162        } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
163            lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
164        } else {
165            throw new RuntimeException("Unexpected scroll bar mode");
166        }
167
168        // Map the touch position back to the scroll of the recycler view
169        getCurScrollState(mScrollPosState, mApps.getAdapterItems());
170        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
171        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
172        if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
173            layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
174        }
175
176        if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
177            mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
178
179            // Reset the last focused view
180            if (mLastFastScrollFocusedView != null) {
181                mLastFastScrollFocusedView.setFastScrollFocused(false, true);
182                mLastFastScrollFocusedView = null;
183            }
184
185            if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
186                smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
187            } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
188                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
189                if (vh != null &&
190                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
191                    mLastFastScrollFocusedView =
192                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
193                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
194                }
195            } else {
196                throw new RuntimeException("Unexpected fast scroll mode");
197            }
198        }
199        return lastInfo.sectionName;
200    }
201
202    @Override
203    public void onFastScrollCompleted() {
204        super.onFastScrollCompleted();
205        // Reset and clean up the last focused view
206        if (mLastFastScrollFocusedView != null) {
207            mLastFastScrollFocusedView.setFastScrollFocused(false, true);
208            mLastFastScrollFocusedView = null;
209        }
210        mPrevFastScrollFocusedPosition = -1;
211    }
212
213    /**
214     * Updates the bounds for the scrollbar.
215     */
216    @Override
217    public void onUpdateScrollbar() {
218        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
219
220        // Skip early if there are no items or we haven't been measured
221        if (items.isEmpty() || mNumAppsPerRow == 0) {
222            mScrollbar.setScrollbarThumbOffset(-1, -1);
223            return;
224        }
225
226        // Find the index and height of the first visible row (all rows have the same height)
227        int rowCount = mApps.getNumAppRows();
228        getCurScrollState(mScrollPosState, items);
229        if (mScrollPosState.rowIndex < 0) {
230            mScrollbar.setScrollbarThumbOffset(-1, -1);
231            return;
232        }
233
234        synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
235    }
236
237    /**
238     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
239     * if necessary.
240     */
241    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
242        @Override
243        public void run() {
244            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
245                scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
246                mFastScrollFrameIndex++;
247                postOnAnimation(mSmoothSnapNextFrameRunnable);
248            } else {
249                // Animation completed, set the fast scroll state on the target view
250                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
251                if (vh != null &&
252                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
253                        mLastFastScrollFocusedView != vh.itemView) {
254                    mLastFastScrollFocusedView =
255                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
256                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
257                }
258            }
259        }
260    };
261
262    /**
263     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
264     * ourselves and animating the scroll on the recycler view.
265     */
266    private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
267        removeCallbacks(mSmoothSnapNextFrameRunnable);
268
269        // Calculate the full animation from the current scroll position to the final scroll
270        // position, and then run the animation for the duration.
271        int curScrollY = getPaddingTop() +
272                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
273        int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
274        int numFrames = mFastScrollFrames.length;
275        for (int i = 0; i < numFrames; i++) {
276            // TODO(winsonc): We can interpolate this as well.
277            mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
278        }
279        mFastScrollFrameIndex = 0;
280        postOnAnimation(mSmoothSnapNextFrameRunnable);
281    }
282
283    /**
284     * Returns the current scroll state of the apps rows.
285     */
286    private void getCurScrollState(ScrollPositionState stateOut,
287            List<AlphabeticalAppsList.AdapterItem> items) {
288        stateOut.rowIndex = -1;
289        stateOut.rowTopOffset = -1;
290        stateOut.rowHeight = -1;
291
292        // Return early if there are no items or we haven't been measured
293        if (items.isEmpty() || mNumAppsPerRow == 0) {
294            return;
295        }
296
297        int childCount = getChildCount();
298        for (int i = 0; i < childCount; i++) {
299            View child = getChildAt(i);
300            int position = getChildPosition(child);
301            if (position != NO_POSITION) {
302                AlphabeticalAppsList.AdapterItem item = items.get(position);
303                if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
304                        item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
305                    stateOut.rowIndex = item.rowIndex;
306                    stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
307                    stateOut.rowHeight = child.getHeight();
308                    break;
309                }
310            }
311        }
312    }
313
314    /**
315     * Returns the scrollY for the given position in the adapter.
316     */
317    private int getScrollAtPosition(int position, int rowHeight) {
318        AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
319        if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
320                item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
321            int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
322            return offset + item.rowIndex * rowHeight;
323        } else {
324            return 0;
325        }
326    }
327}
328