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