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 */ 16 package com.android.launcher3.allapps; 17 18 import android.animation.ObjectAnimator; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.drawable.Drawable; 23 import android.os.Bundle; 24 import android.support.v7.widget.LinearLayoutManager; 25 import android.support.v7.widget.RecyclerView; 26 import android.util.AttributeSet; 27 import android.view.View; 28 29 import com.android.launcher3.BaseRecyclerView; 30 import com.android.launcher3.BaseRecyclerViewFastScrollBar; 31 import com.android.launcher3.DeviceProfile; 32 import com.android.launcher3.R; 33 import com.android.launcher3.Stats; 34 import com.android.launcher3.Utilities; 35 import com.android.launcher3.util.Thunk; 36 37 import java.util.List; 38 39 /** 40 * A RecyclerView with custom fast scroll support for the all apps view. 41 */ 42 public class AllAppsRecyclerView extends BaseRecyclerView 43 implements Stats.LaunchSourceProvider { 44 45 private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0; 46 private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1; 47 48 private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0; 49 private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1; 50 51 private AlphabeticalAppsList mApps; 52 private int mNumAppsPerRow; 53 54 @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView; 55 @Thunk int mPrevFastScrollFocusedPosition; 56 @Thunk int mFastScrollFrameIndex; 57 @Thunk final int[] mFastScrollFrames = new int[10]; 58 59 private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON; 60 private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW; 61 62 private ScrollPositionState mScrollPosState = new ScrollPositionState(); 63 64 private AllAppsBackgroundDrawable mEmptySearchBackground; 65 private int mEmptySearchBackgroundTopOffset; 66 AllAppsRecyclerView(Context context)67 public AllAppsRecyclerView(Context context) { 68 this(context, null); 69 } 70 AllAppsRecyclerView(Context context, AttributeSet attrs)71 public AllAppsRecyclerView(Context context, AttributeSet attrs) { 72 this(context, attrs, 0); 73 } 74 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)75 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 76 this(context, attrs, defStyleAttr, 0); 77 } 78 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)79 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, 80 int defStyleRes) { 81 super(context, attrs, defStyleAttr); 82 83 Resources res = getResources(); 84 mScrollbar.setDetachThumbOnFastScroll(); 85 mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( 86 R.dimen.all_apps_empty_search_bg_top_offset); 87 } 88 89 /** 90 * Sets the list of apps in this view, used to determine the fastscroll position. 91 */ setApps(AlphabeticalAppsList apps)92 public void setApps(AlphabeticalAppsList apps) { 93 mApps = apps; 94 } 95 96 /** 97 * Sets the number of apps per row in this recycler view. 98 */ setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow)99 public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) { 100 mNumAppsPerRow = numAppsPerRow; 101 102 RecyclerView.RecycledViewPool pool = getRecycledViewPool(); 103 int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); 104 pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1); 105 pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1); 106 pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1); 107 pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); 108 pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); 109 pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); 110 } 111 112 /** 113 * Scrolls this recycler view to the top. 114 */ scrollToTop()115 public void scrollToTop() { 116 // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling 117 if (mScrollbar.isThumbDetached()) { 118 mScrollbar.reattachThumbToScroll(); 119 } 120 scrollToPosition(0); 121 } 122 123 /** 124 * We need to override the draw to ensure that we don't draw the overscroll effect beyond the 125 * background bounds. 126 */ 127 @Override dispatchDraw(Canvas canvas)128 protected void dispatchDraw(Canvas canvas) { 129 canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, 130 getWidth() - mBackgroundPadding.right, 131 getHeight() - mBackgroundPadding.bottom); 132 super.dispatchDraw(canvas); 133 } 134 135 @Override onDraw(Canvas c)136 public void onDraw(Canvas c) { 137 // Draw the background 138 if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { 139 c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, 140 getWidth() - mBackgroundPadding.right, 141 getHeight() - mBackgroundPadding.bottom); 142 143 mEmptySearchBackground.draw(c); 144 } 145 146 super.onDraw(c); 147 } 148 149 @Override verifyDrawable(Drawable who)150 protected boolean verifyDrawable(Drawable who) { 151 return who == mEmptySearchBackground || super.verifyDrawable(who); 152 } 153 154 @Override onSizeChanged(int w, int h, int oldw, int oldh)155 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 156 updateEmptySearchBackgroundBounds(); 157 } 158 159 @Override onFinishInflate()160 protected void onFinishInflate() { 161 super.onFinishInflate(); 162 163 // Bind event handlers 164 addOnItemTouchListener(this); 165 } 166 167 @Override fillInLaunchSourceData(Bundle sourceData)168 public void fillInLaunchSourceData(Bundle sourceData) { 169 sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS); 170 if (mApps.hasFilter()) { 171 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, 172 Stats.SUB_CONTAINER_ALL_APPS_SEARCH); 173 } else { 174 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, 175 Stats.SUB_CONTAINER_ALL_APPS_A_Z); 176 } 177 } 178 onSearchResultsChanged()179 public void onSearchResultsChanged() { 180 // Always scroll the view to the top so the user can see the changed results 181 scrollToTop(); 182 183 if (mApps.hasNoFilteredResults()) { 184 if (mEmptySearchBackground == null) { 185 mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext()); 186 mEmptySearchBackground.setAlpha(0); 187 mEmptySearchBackground.setCallback(this); 188 updateEmptySearchBackgroundBounds(); 189 } 190 mEmptySearchBackground.animateBgAlpha(1f, 150); 191 } else if (mEmptySearchBackground != null) { 192 // For the time being, we just immediately hide the background to ensure that it does 193 // not overlap with the results 194 mEmptySearchBackground.setBgAlpha(0f); 195 } 196 } 197 198 /** 199 * Maps the touch (from 0..1) to the adapter position that should be visible. 200 */ 201 @Override scrollToPositionAtProgress(float touchFraction)202 public String scrollToPositionAtProgress(float touchFraction) { 203 int rowCount = mApps.getNumAppRows(); 204 if (rowCount == 0) { 205 return ""; 206 } 207 208 // Stop the scroller if it is scrolling 209 stopScroll(); 210 211 // Find the fastscroll section that maps to this touch fraction 212 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 213 mApps.getFastScrollerSections(); 214 AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); 215 if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) { 216 for (int i = 1; i < fastScrollSections.size(); i++) { 217 AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); 218 if (info.touchFraction > touchFraction) { 219 break; 220 } 221 lastInfo = info; 222 } 223 } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){ 224 lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1))); 225 } else { 226 throw new RuntimeException("Unexpected scroll bar mode"); 227 } 228 229 // Map the touch position back to the scroll of the recycler view 230 getCurScrollState(mScrollPosState); 231 int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); 232 LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); 233 if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { 234 layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); 235 } 236 237 if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) { 238 mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position; 239 240 // Reset the last focused view 241 if (mLastFastScrollFocusedView != null) { 242 mLastFastScrollFocusedView.setFastScrollFocused(false, true); 243 mLastFastScrollFocusedView = null; 244 } 245 246 if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) { 247 smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState); 248 } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { 249 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); 250 if (vh != null && 251 vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { 252 mLastFastScrollFocusedView = 253 (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; 254 mLastFastScrollFocusedView.setFastScrollFocused(true, true); 255 } 256 } else { 257 throw new RuntimeException("Unexpected fast scroll mode"); 258 } 259 } 260 return lastInfo.sectionName; 261 } 262 263 @Override onFastScrollCompleted()264 public void onFastScrollCompleted() { 265 super.onFastScrollCompleted(); 266 // Reset and clean up the last focused view 267 if (mLastFastScrollFocusedView != null) { 268 mLastFastScrollFocusedView.setFastScrollFocused(false, true); 269 mLastFastScrollFocusedView = null; 270 } 271 mPrevFastScrollFocusedPosition = -1; 272 } 273 274 /** 275 * Updates the bounds for the scrollbar. 276 */ 277 @Override onUpdateScrollbar(int dy)278 public void onUpdateScrollbar(int dy) { 279 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 280 281 // Skip early if there are no items or we haven't been measured 282 if (items.isEmpty() || mNumAppsPerRow == 0) { 283 mScrollbar.setThumbOffset(-1, -1); 284 return; 285 } 286 287 // Find the index and height of the first visible row (all rows have the same height) 288 int rowCount = mApps.getNumAppRows(); 289 getCurScrollState(mScrollPosState); 290 if (mScrollPosState.rowIndex < 0) { 291 mScrollbar.setThumbOffset(-1, -1); 292 return; 293 } 294 295 // Only show the scrollbar if there is height to be scrolled 296 int availableScrollBarHeight = getAvailableScrollBarHeight(); 297 int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); 298 if (availableScrollHeight <= 0) { 299 mScrollbar.setThumbOffset(-1, -1); 300 return; 301 } 302 303 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 304 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 305 // padding) 306 int scrollY = getPaddingTop() + 307 (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; 308 int scrollBarY = mBackgroundPadding.top + 309 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 310 311 if (mScrollbar.isThumbDetached()) { 312 int scrollBarX; 313 if (Utilities.isRtl(getResources())) { 314 scrollBarX = mBackgroundPadding.left; 315 } else { 316 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); 317 } 318 319 if (mScrollbar.isDraggingThumb()) { 320 // If the thumb is detached, then just update the thumb to the current 321 // touch position 322 mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY()); 323 } else { 324 int thumbScrollY = mScrollbar.getThumbOffset().y; 325 int diffScrollY = scrollBarY - thumbScrollY; 326 if (diffScrollY * dy > 0f) { 327 // User is scrolling in the same direction the thumb needs to catch up to the 328 // current scroll position. We do this by mapping the difference in movement 329 // from the original scroll bar position to the difference in movement necessary 330 // in the detached thumb position to ensure that both speed towards the same 331 // position at either end of the list. 332 if (dy < 0) { 333 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 334 thumbScrollY += Math.max(offset, diffScrollY); 335 } else { 336 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 337 (float) (availableScrollBarHeight - scrollBarY)); 338 thumbScrollY += Math.min(offset, diffScrollY); 339 } 340 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 341 mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); 342 if (scrollBarY == thumbScrollY) { 343 mScrollbar.reattachThumbToScroll(); 344 } 345 } else { 346 // User is scrolling in an opposite direction to the direction that the thumb 347 // needs to catch up to the scroll position. Do nothing except for updating 348 // the scroll bar x to match the thumb width. 349 mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); 350 } 351 } 352 } else { 353 synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); 354 } 355 } 356 357 /** 358 * This runnable runs a single frame of the smooth scroll animation and posts the next frame 359 * if necessary. 360 */ 361 @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() { 362 @Override 363 public void run() { 364 if (mFastScrollFrameIndex < mFastScrollFrames.length) { 365 scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]); 366 mFastScrollFrameIndex++; 367 postOnAnimation(mSmoothSnapNextFrameRunnable); 368 } else { 369 // Animation completed, set the fast scroll state on the target view 370 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition); 371 if (vh != null && 372 vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView && 373 mLastFastScrollFocusedView != vh.itemView) { 374 mLastFastScrollFocusedView = 375 (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView; 376 mLastFastScrollFocusedView.setFastScrollFocused(true, true); 377 } 378 } 379 } 380 }; 381 382 /** 383 * Smoothly snaps to a given position. We do this manually by calculating the keyframes 384 * ourselves and animating the scroll on the recycler view. 385 */ smoothSnapToPosition(final int position, ScrollPositionState scrollPosState)386 private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) { 387 removeCallbacks(mSmoothSnapNextFrameRunnable); 388 389 // Calculate the full animation from the current scroll position to the final scroll 390 // position, and then run the animation for the duration. 391 int curScrollY = getPaddingTop() + 392 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; 393 int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight); 394 int numFrames = mFastScrollFrames.length; 395 for (int i = 0; i < numFrames; i++) { 396 // TODO(winsonc): We can interpolate this as well. 397 mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames; 398 } 399 mFastScrollFrameIndex = 0; 400 postOnAnimation(mSmoothSnapNextFrameRunnable); 401 } 402 403 /** 404 * Returns the current scroll state of the apps rows. 405 */ getCurScrollState(ScrollPositionState stateOut)406 protected void getCurScrollState(ScrollPositionState stateOut) { 407 stateOut.rowIndex = -1; 408 stateOut.rowTopOffset = -1; 409 stateOut.rowHeight = -1; 410 411 // Return early if there are no items or we haven't been measured 412 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 413 if (items.isEmpty() || mNumAppsPerRow == 0) { 414 return; 415 } 416 417 int childCount = getChildCount(); 418 for (int i = 0; i < childCount; i++) { 419 View child = getChildAt(i); 420 int position = getChildPosition(child); 421 if (position != NO_POSITION) { 422 AlphabeticalAppsList.AdapterItem item = items.get(position); 423 if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || 424 item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 425 stateOut.rowIndex = item.rowIndex; 426 stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); 427 stateOut.rowHeight = child.getHeight(); 428 break; 429 } 430 } 431 } 432 } 433 434 /** 435 * Returns the scrollY for the given position in the adapter. 436 */ getScrollAtPosition(int position, int rowHeight)437 private int getScrollAtPosition(int position, int rowHeight) { 438 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 439 if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || 440 item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 441 int offset = item.rowIndex > 0 ? getPaddingTop() : 0; 442 return offset + item.rowIndex * rowHeight; 443 } else { 444 return 0; 445 } 446 } 447 448 /** 449 * Updates the bounds of the empty search background. 450 */ updateEmptySearchBackgroundBounds()451 private void updateEmptySearchBackgroundBounds() { 452 if (mEmptySearchBackground == null) { 453 return; 454 } 455 456 // Center the empty search background on this new view bounds 457 int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2; 458 int y = mEmptySearchBackgroundTopOffset; 459 mEmptySearchBackground.setBounds(x, y, 460 x + mEmptySearchBackground.getIntrinsicWidth(), 461 y + mEmptySearchBackground.getIntrinsicHeight()); 462 } 463 } 464