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