2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.android.launcher3.allapps;
18 import android.content.Context;
19 import android.graphics.Canvas;
20 import android.os.Bundle;
21 import android.support.v7.widget.LinearLayoutManager;
22 import android.support.v7.widget.RecyclerView;
23 import android.util.AttributeSet;
24 import android.view.View;
26 import com.android.launcher3.BaseRecyclerView;
27 import com.android.launcher3.BaseRecyclerViewFastScrollBar;
28 import com.android.launcher3.DeviceProfile;
29 import com.android.launcher3.Stats;
30 import com.android.launcher3.Utilities;
31 import com.android.launcher3.util.Thunk;
33 import java.util.List;
36 * A RecyclerView with custom fast scroll support for the all apps view.
38 public class AllAppsRecyclerView extends BaseRecyclerView
39 implements Stats.LaunchSourceProvider {
41 private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
42 private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
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;
47 private AlphabeticalAppsList mApps;
48 private int mNumAppsPerRow;
50 @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
51 @Thunk int mPrevFastScrollFocusedPosition;
52 @Thunk int mFastScrollFrameIndex;
53 @Thunk final int[] mFastScrollFrames = new int[10];
55 private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
56 private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
58 private ScrollPositionState mScrollPosState = new ScrollPositionState();
60 public AllAppsRecyclerView(Context context) {
64 public AllAppsRecyclerView(Context context, AttributeSet attrs) {
65 this(context, attrs, 0);
68 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
69 this(context, attrs, defStyleAttr, 0);
72 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
74 super(context, attrs, defStyleAttr);
78 * Sets the list of apps in this view, used to determine the fastscroll position.
80 public void setApps(AlphabeticalAppsList apps) {
85 * Sets the number of apps per row in this recycler view.
87 public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
88 mNumAppsPerRow = numAppsPerRow;
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);
99 * Scrolls this recycler view to the top.
101 public void scrollToTop() {
106 * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
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);
118 protected void onFinishInflate() {
119 super.onFinishInflate();
121 // Bind event handlers
122 addOnItemTouchListener(this);
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);
132 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
133 Stats.SUB_CONTAINER_ALL_APPS_A_Z);
138 * Maps the touch (from 0..1) to the adapter position that should be visible.
141 public String scrollToPositionAtProgress(float touchFraction) {
142 int rowCount = mApps.getNumAppRows();
147 // Stop the scroller if it is scrolling
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) {
162 } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
163 lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
165 throw new RuntimeException("Unexpected scroll bar mode");
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));
176 if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
177 mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
179 // Reset the last focused view
180 if (mLastFastScrollFocusedView != null) {
181 mLastFastScrollFocusedView.setFastScrollFocused(false, true);
182 mLastFastScrollFocusedView = null;
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);
190 vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
191 mLastFastScrollFocusedView =
192 (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
193 mLastFastScrollFocusedView.setFastScrollFocused(true, true);
196 throw new RuntimeException("Unexpected fast scroll mode");
199 return lastInfo.sectionName;
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;
210 mPrevFastScrollFocusedPosition = -1;
214 * Updates the bounds for the scrollbar.
217 public void onUpdateScrollbar() {
218 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
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);
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);
234 synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
238 * This runnable runs a single frame of the smooth scroll animation and posts the next frame
241 @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
244 if (mFastScrollFrameIndex < mFastScrollFrames.length) {
245 scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
246 mFastScrollFrameIndex++;
247 postOnAnimation(mSmoothSnapNextFrameRunnable);
249 // Animation completed, set the fast scroll state on the target view
250 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
252 vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
253 mLastFastScrollFocusedView != vh.itemView) {
254 mLastFastScrollFocusedView =
255 (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
256 mLastFastScrollFocusedView.setFastScrollFocused(true, true);
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.
266 private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
267 removeCallbacks(mSmoothSnapNextFrameRunnable);
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;
279 mFastScrollFrameIndex = 0;
280 postOnAnimation(mSmoothSnapNextFrameRunnable);
284 * Returns the current scroll state of the apps rows.
286 private void getCurScrollState(ScrollPositionState stateOut,
287 List<AlphabeticalAppsList.AdapterItem> items) {
288 stateOut.rowIndex = -1;
289 stateOut.rowTopOffset = -1;
290 stateOut.rowHeight = -1;
292 // Return early if there are no items or we haven't been measured
293 if (items.isEmpty() || mNumAppsPerRow == 0) {
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();
315 * Returns the scrollY for the given position in the adapter.
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;