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.content.Context; 19 import android.content.res.Resources; 20 import android.graphics.Canvas; 21 import android.graphics.drawable.Drawable; 22 import android.os.Bundle; 23 import android.support.v7.widget.RecyclerView; 24 import android.util.AttributeSet; 25 import android.view.View; 26 27 import com.android.launcher3.BaseRecyclerView; 28 import com.android.launcher3.BubbleTextView; 29 import com.android.launcher3.DeviceProfile; 30 import com.android.launcher3.R; 31 import com.android.launcher3.Stats; 32 import com.android.launcher3.Utilities; 33 34 import java.util.List; 35 36 /** 37 * A RecyclerView with custom fast scroll support for the all apps view. 38 */ 39 public class AllAppsRecyclerView extends BaseRecyclerView 40 implements Stats.LaunchSourceProvider { 41 42 private AlphabeticalAppsList mApps; 43 private AllAppsFastScrollHelper mFastScrollHelper; 44 private BaseRecyclerView.ScrollPositionState mScrollPosState = 45 new BaseRecyclerView.ScrollPositionState(); 46 private int mNumAppsPerRow; 47 48 // The specific icon heights that we use to calculate scroll 49 private int mPredictionIconHeight; 50 private int mIconHeight; 51 52 // The empty-search result background 53 private AllAppsBackgroundDrawable mEmptySearchBackground; 54 private int mEmptySearchBackgroundTopOffset; 55 56 private HeaderElevationController mElevationController; 57 AllAppsRecyclerView(Context context)58 public AllAppsRecyclerView(Context context) { 59 this(context, null); 60 } 61 AllAppsRecyclerView(Context context, AttributeSet attrs)62 public AllAppsRecyclerView(Context context, AttributeSet attrs) { 63 this(context, attrs, 0); 64 } 65 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)66 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 67 this(context, attrs, defStyleAttr, 0); 68 } 69 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)70 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, 71 int defStyleRes) { 72 super(context, attrs, defStyleAttr); 73 Resources res = getResources(); 74 addOnItemTouchListener(this); 75 mScrollbar.setDetachThumbOnFastScroll(); 76 mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( 77 R.dimen.all_apps_empty_search_bg_top_offset); 78 } 79 80 /** 81 * Sets the list of apps in this view, used to determine the fastscroll position. 82 */ setApps(AlphabeticalAppsList apps)83 public void setApps(AlphabeticalAppsList apps) { 84 mApps = apps; 85 mFastScrollHelper = new AllAppsFastScrollHelper(this, apps); 86 } 87 setElevationController(HeaderElevationController elevationController)88 public void setElevationController(HeaderElevationController elevationController) { 89 mElevationController = elevationController; 90 } 91 92 /** 93 * Sets the number of apps per row in this recycler view. 94 */ setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow)95 public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) { 96 mNumAppsPerRow = numAppsPerRow; 97 98 RecyclerView.RecycledViewPool pool = getRecycledViewPool(); 99 int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); 100 pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1); 101 pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1); 102 pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1); 103 pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); 104 pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); 105 pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); 106 } 107 108 /** 109 * Sets the heights of the icons in this view (for scroll calculations). 110 */ setPremeasuredIconHeights(int predictionIconHeight, int iconHeight)111 public void setPremeasuredIconHeights(int predictionIconHeight, int iconHeight) { 112 mPredictionIconHeight = predictionIconHeight; 113 mIconHeight = iconHeight; 114 } 115 116 /** 117 * Scrolls this recycler view to the top. 118 */ scrollToTop()119 public void scrollToTop() { 120 // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling 121 if (mScrollbar.isThumbDetached()) { 122 mScrollbar.reattachThumbToScroll(); 123 } 124 scrollToPosition(0); 125 if (mElevationController != null) { 126 mElevationController.reset(); 127 } 128 } 129 130 /** 131 * We need to override the draw to ensure that we don't draw the overscroll effect beyond the 132 * background bounds. 133 */ 134 @Override dispatchDraw(Canvas canvas)135 protected void dispatchDraw(Canvas canvas) { 136 // Clip to ensure that we don't draw the overscroll effect beyond the background bounds 137 canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, 138 getWidth() - mBackgroundPadding.right, 139 getHeight() - mBackgroundPadding.bottom); 140 super.dispatchDraw(canvas); 141 } 142 143 @Override onDraw(Canvas c)144 public void onDraw(Canvas c) { 145 // Draw the background 146 if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { 147 c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, 148 getWidth() - mBackgroundPadding.right, 149 getHeight() - mBackgroundPadding.bottom); 150 151 mEmptySearchBackground.draw(c); 152 } 153 154 super.onDraw(c); 155 } 156 157 @Override verifyDrawable(Drawable who)158 protected boolean verifyDrawable(Drawable who) { 159 return who == mEmptySearchBackground || super.verifyDrawable(who); 160 } 161 162 @Override onSizeChanged(int w, int h, int oldw, int oldh)163 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 164 updateEmptySearchBackgroundBounds(); 165 } 166 167 @Override fillInLaunchSourceData(View v, Bundle sourceData)168 public void fillInLaunchSourceData(View v, 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 if (v instanceof BubbleTextView) { 175 BubbleTextView icon = (BubbleTextView) v; 176 int position = getChildPosition(icon); 177 if (position != NO_POSITION) { 178 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 179 AlphabeticalAppsList.AdapterItem item = items.get(position); 180 if (item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 181 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, 182 Stats.SUB_CONTAINER_ALL_APPS_PREDICTION); 183 return; 184 } 185 } 186 } 187 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, 188 Stats.SUB_CONTAINER_ALL_APPS_A_Z); 189 } 190 } 191 onSearchResultsChanged()192 public void onSearchResultsChanged() { 193 // Always scroll the view to the top so the user can see the changed results 194 scrollToTop(); 195 196 if (mApps.hasNoFilteredResults()) { 197 if (mEmptySearchBackground == null) { 198 mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext()); 199 mEmptySearchBackground.setAlpha(0); 200 mEmptySearchBackground.setCallback(this); 201 updateEmptySearchBackgroundBounds(); 202 } 203 mEmptySearchBackground.animateBgAlpha(1f, 150); 204 } else if (mEmptySearchBackground != null) { 205 // For the time being, we just immediately hide the background to ensure that it does 206 // not overlap with the results 207 mEmptySearchBackground.setBgAlpha(0f); 208 } 209 } 210 211 /** 212 * Maps the touch (from 0..1) to the adapter position that should be visible. 213 */ 214 @Override scrollToPositionAtProgress(float touchFraction)215 public String scrollToPositionAtProgress(float touchFraction) { 216 int rowCount = mApps.getNumAppRows(); 217 if (rowCount == 0) { 218 return ""; 219 } 220 221 // Stop the scroller if it is scrolling 222 stopScroll(); 223 224 // Find the fastscroll section that maps to this touch fraction 225 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 226 mApps.getFastScrollerSections(); 227 AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); 228 for (int i = 1; i < fastScrollSections.size(); i++) { 229 AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); 230 if (info.touchFraction > touchFraction) { 231 break; 232 } 233 lastInfo = info; 234 } 235 236 // Update the fast scroll 237 int scrollY = getScrollTop(mScrollPosState); 238 int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows()); 239 mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo); 240 return lastInfo.sectionName; 241 } 242 243 @Override onFastScrollCompleted()244 public void onFastScrollCompleted() { 245 super.onFastScrollCompleted(); 246 mFastScrollHelper.onFastScrollCompleted(); 247 } 248 249 @Override setAdapter(Adapter adapter)250 public void setAdapter(Adapter adapter) { 251 super.setAdapter(adapter); 252 mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter); 253 } 254 255 /** 256 * Updates the bounds for the scrollbar. 257 */ 258 @Override onUpdateScrollbar(int dy)259 public void onUpdateScrollbar(int dy) { 260 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 261 262 // Skip early if there are no items or we haven't been measured 263 if (items.isEmpty() || mNumAppsPerRow == 0) { 264 mScrollbar.setThumbOffset(-1, -1); 265 return; 266 } 267 268 // Find the index and height of the first visible row (all rows have the same height) 269 int rowCount = mApps.getNumAppRows(); 270 getCurScrollState(mScrollPosState, -1); 271 if (mScrollPosState.rowIndex < 0) { 272 mScrollbar.setThumbOffset(-1, -1); 273 return; 274 } 275 276 // Only show the scrollbar if there is height to be scrolled 277 int availableScrollBarHeight = getAvailableScrollBarHeight(); 278 int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows()); 279 if (availableScrollHeight <= 0) { 280 mScrollbar.setThumbOffset(-1, -1); 281 return; 282 } 283 284 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 285 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 286 // padding) 287 int scrollY = getScrollTop(mScrollPosState); 288 int scrollBarY = mBackgroundPadding.top + 289 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 290 291 if (mScrollbar.isThumbDetached()) { 292 int scrollBarX; 293 if (Utilities.isRtl(getResources())) { 294 scrollBarX = mBackgroundPadding.left; 295 } else { 296 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); 297 } 298 299 if (mScrollbar.isDraggingThumb()) { 300 // If the thumb is detached, then just update the thumb to the current 301 // touch position 302 mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY()); 303 } else { 304 int thumbScrollY = mScrollbar.getThumbOffset().y; 305 int diffScrollY = scrollBarY - thumbScrollY; 306 if (diffScrollY * dy > 0f) { 307 // User is scrolling in the same direction the thumb needs to catch up to the 308 // current scroll position. We do this by mapping the difference in movement 309 // from the original scroll bar position to the difference in movement necessary 310 // in the detached thumb position to ensure that both speed towards the same 311 // position at either end of the list. 312 if (dy < 0) { 313 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 314 thumbScrollY += Math.max(offset, diffScrollY); 315 } else { 316 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 317 (float) (availableScrollBarHeight - scrollBarY)); 318 thumbScrollY += Math.min(offset, diffScrollY); 319 } 320 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 321 mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); 322 if (scrollBarY == thumbScrollY) { 323 mScrollbar.reattachThumbToScroll(); 324 } 325 } else { 326 // User is scrolling in an opposite direction to the direction that the thumb 327 // needs to catch up to the scroll position. Do nothing except for updating 328 // the scroll bar x to match the thumb width. 329 mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); 330 } 331 } 332 } else { 333 synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); 334 } 335 } 336 337 /** 338 * Returns the current scroll state of the apps rows. 339 */ getCurScrollState(ScrollPositionState stateOut, int viewTypeMask)340 protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) { 341 stateOut.rowIndex = -1; 342 stateOut.rowTopOffset = -1; 343 stateOut.itemPos = -1; 344 345 // Return early if there are no items or we haven't been measured 346 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 347 if (items.isEmpty() || mNumAppsPerRow == 0) { 348 return; 349 } 350 351 int childCount = getChildCount(); 352 for (int i = 0; i < childCount; i++) { 353 View child = getChildAt(i); 354 int position = getChildPosition(child); 355 if (position != NO_POSITION) { 356 AlphabeticalAppsList.AdapterItem item = items.get(position); 357 if ((item.viewType & viewTypeMask) != 0) { 358 stateOut.rowIndex = item.rowIndex; 359 stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); 360 stateOut.itemPos = position; 361 return; 362 } 363 } 364 } 365 return; 366 } 367 368 @Override supportsFastScrolling()369 protected boolean supportsFastScrolling() { 370 // Only allow fast scrolling when the user is not searching, since the results are not 371 // grouped in a meaningful order 372 return !mApps.hasFilter(); 373 } 374 getTop(int rowIndex)375 protected int getTop(int rowIndex) { 376 if (getChildCount() == 0 || rowIndex <= 0) { 377 return 0; 378 } 379 380 // The prediction bar icons have more padding, so account for that in the row offset 381 return mPredictionIconHeight + (rowIndex - 1) * mIconHeight; 382 } 383 384 /** 385 * Updates the bounds of the empty search background. 386 */ updateEmptySearchBackgroundBounds()387 private void updateEmptySearchBackgroundBounds() { 388 if (mEmptySearchBackground == null) { 389 return; 390 } 391 392 // Center the empty search background on this new view bounds 393 int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2; 394 int y = mEmptySearchBackgroundTopOffset; 395 mEmptySearchBackground.setBounds(x, y, 396 x + mEmptySearchBackground.getIntrinsicWidth(), 397 y + mEmptySearchBackground.getIntrinsicHeight()); 398 } 399 } 400