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.support.v7.widget.RecyclerView; 23 import android.util.AttributeSet; 24 import android.util.SparseIntArray; 25 import android.view.MotionEvent; 26 import android.view.View; 27 28 import com.android.launcher3.BaseRecyclerView; 29 import com.android.launcher3.BubbleTextView; 30 import com.android.launcher3.DeviceProfile; 31 import com.android.launcher3.Launcher; 32 import com.android.launcher3.R; 33 import com.android.launcher3.config.FeatureFlags; 34 import com.android.launcher3.graphics.DrawableFactory; 35 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 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 44 private AlphabeticalAppsList mApps; 45 private AllAppsFastScrollHelper mFastScrollHelper; 46 private int mNumAppsPerRow; 47 48 // The specific view heights that we use to calculate scroll 49 private SparseIntArray mViewHeights = new SparseIntArray(); 50 private SparseIntArray mCachedScrollPositions = new SparseIntArray(); 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.VIEW_TYPE_EMPTY_SEARCH, 1); 101 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER, 1); 102 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, 1); 103 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1); 104 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow); 105 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, mNumAppsPerRow); 106 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, 1); 107 } 108 109 /** 110 * Ensures that we can present a stable scrollbar for views of varying types by pre-measuring 111 * all the different view types. 112 */ preMeasureViews(AllAppsGridAdapter adapter)113 public void preMeasureViews(AllAppsGridAdapter adapter) { 114 View icon = adapter.onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON).itemView; 115 final int iconHeight = icon.getLayoutParams().height; 116 mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight); 117 mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight); 118 119 final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( 120 getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST); 121 final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec( 122 getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST); 123 124 putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, 125 AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, 126 AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER); 127 putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, 128 AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER); 129 putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, 130 AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET); 131 putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, 132 AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH); 133 134 if (FeatureFlags.DISCOVERY_ENABLED) { 135 putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, 136 AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER); 137 putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, 138 AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM); 139 } 140 } 141 putSameHeightFor(AllAppsGridAdapter adapter, int w, int h, int... viewTypes)142 private void putSameHeightFor(AllAppsGridAdapter adapter, int w, int h, int... viewTypes) { 143 View view = adapter.onCreateViewHolder(this, viewTypes[0]).itemView; 144 view.measure(w, h); 145 for (int viewType : viewTypes) { 146 mViewHeights.put(viewType, view.getMeasuredHeight()); 147 } 148 } 149 150 /** 151 * Scrolls this recycler view to the top. 152 */ scrollToTop()153 public void scrollToTop() { 154 // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling 155 if (mScrollbar.isThumbDetached()) { 156 mScrollbar.reattachThumbToScroll(); 157 } 158 scrollToPosition(0); 159 if (mElevationController != null) { 160 mElevationController.reset(); 161 } 162 } 163 164 @Override onDraw(Canvas c)165 public void onDraw(Canvas c) { 166 // Draw the background 167 if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { 168 mEmptySearchBackground.draw(c); 169 } 170 171 super.onDraw(c); 172 } 173 174 @Override verifyDrawable(Drawable who)175 protected boolean verifyDrawable(Drawable who) { 176 return who == mEmptySearchBackground || super.verifyDrawable(who); 177 } 178 179 @Override onSizeChanged(int w, int h, int oldw, int oldh)180 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 181 updateEmptySearchBackgroundBounds(); 182 } 183 getContainerType(View v)184 public int getContainerType(View v) { 185 if (mApps.hasFilter()) { 186 return ContainerType.SEARCHRESULT; 187 } else { 188 if (v instanceof BubbleTextView) { 189 BubbleTextView icon = (BubbleTextView) v; 190 int position = getChildPosition(icon); 191 if (position != NO_POSITION) { 192 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 193 AlphabeticalAppsList.AdapterItem item = items.get(position); 194 if (item.viewType == AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON) { 195 return ContainerType.PREDICTION; 196 } 197 } 198 } 199 return ContainerType.ALLAPPS; 200 } 201 } 202 onSearchResultsChanged()203 public void onSearchResultsChanged() { 204 // Always scroll the view to the top so the user can see the changed results 205 scrollToTop(); 206 207 if (mApps.shouldShowEmptySearch()) { 208 if (mEmptySearchBackground == null) { 209 mEmptySearchBackground = DrawableFactory.get(getContext()) 210 .getAllAppsBackground(getContext()); 211 mEmptySearchBackground.setAlpha(0); 212 mEmptySearchBackground.setCallback(this); 213 updateEmptySearchBackgroundBounds(); 214 } 215 mEmptySearchBackground.animateBgAlpha(1f, 150); 216 } else if (mEmptySearchBackground != null) { 217 // For the time being, we just immediately hide the background to ensure that it does 218 // not overlap with the results 219 mEmptySearchBackground.setBgAlpha(0f); 220 } 221 } 222 223 @Override onInterceptTouchEvent(MotionEvent e)224 public boolean onInterceptTouchEvent(MotionEvent e) { 225 boolean result = super.onInterceptTouchEvent(e); 226 if (!result && e.getAction() == MotionEvent.ACTION_DOWN 227 && mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { 228 mEmptySearchBackground.setHotspot(e.getX(), e.getY()); 229 } 230 return result; 231 } 232 233 /** 234 * Maps the touch (from 0..1) to the adapter position that should be visible. 235 */ 236 @Override scrollToPositionAtProgress(float touchFraction)237 public String scrollToPositionAtProgress(float touchFraction) { 238 int rowCount = mApps.getNumAppRows(); 239 if (rowCount == 0) { 240 return ""; 241 } 242 243 // Stop the scroller if it is scrolling 244 stopScroll(); 245 246 // Find the fastscroll section that maps to this touch fraction 247 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 248 mApps.getFastScrollerSections(); 249 AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0); 250 for (int i = 1; i < fastScrollSections.size(); i++) { 251 AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i); 252 if (info.touchFraction > touchFraction) { 253 break; 254 } 255 lastInfo = info; 256 } 257 258 // Update the fast scroll 259 int scrollY = getCurrentScrollY(); 260 int availableScrollHeight = getAvailableScrollHeight(); 261 mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo); 262 return lastInfo.sectionName; 263 } 264 265 @Override onFastScrollCompleted()266 public void onFastScrollCompleted() { 267 super.onFastScrollCompleted(); 268 mFastScrollHelper.onFastScrollCompleted(); 269 } 270 271 @Override setAdapter(Adapter adapter)272 public void setAdapter(Adapter adapter) { 273 super.setAdapter(adapter); 274 adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 275 public void onChanged() { 276 mCachedScrollPositions.clear(); 277 } 278 }); 279 mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter); 280 } 281 282 /** 283 * Updates the bounds for the scrollbar. 284 */ 285 @Override onUpdateScrollbar(int dy)286 public void onUpdateScrollbar(int dy) { 287 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 288 289 // Skip early if there are no items or we haven't been measured 290 if (items.isEmpty() || mNumAppsPerRow == 0) { 291 mScrollbar.setThumbOffsetY(-1); 292 return; 293 } 294 295 // Skip early if, there no child laid out in the container. 296 int scrollY = getCurrentScrollY(); 297 if (scrollY < 0) { 298 mScrollbar.setThumbOffsetY(-1); 299 return; 300 } 301 302 // Only show the scrollbar if there is height to be scrolled 303 int availableScrollBarHeight = getAvailableScrollBarHeight(); 304 int availableScrollHeight = getAvailableScrollHeight(); 305 if (availableScrollHeight <= 0) { 306 mScrollbar.setThumbOffsetY(-1); 307 return; 308 } 309 310 if (mScrollbar.isThumbDetached()) { 311 if (!mScrollbar.isDraggingThumb()) { 312 // Calculate the current scroll position, the scrollY of the recycler view accounts 313 // for the view padding, while the scrollBarY is drawn right up to the background 314 // padding (ignoring padding) 315 int scrollBarY = (int) 316 (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 317 318 int thumbScrollY = mScrollbar.getThumbOffsetY(); 319 int diffScrollY = scrollBarY - thumbScrollY; 320 if (diffScrollY * dy > 0f) { 321 // User is scrolling in the same direction the thumb needs to catch up to the 322 // current scroll position. We do this by mapping the difference in movement 323 // from the original scroll bar position to the difference in movement necessary 324 // in the detached thumb position to ensure that both speed towards the same 325 // position at either end of the list. 326 if (dy < 0) { 327 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 328 thumbScrollY += Math.max(offset, diffScrollY); 329 } else { 330 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 331 (float) (availableScrollBarHeight - scrollBarY)); 332 thumbScrollY += Math.min(offset, diffScrollY); 333 } 334 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 335 mScrollbar.setThumbOffsetY(thumbScrollY); 336 if (scrollBarY == thumbScrollY) { 337 mScrollbar.reattachThumbToScroll(); 338 } 339 } else { 340 // User is scrolling in an opposite direction to the direction that the thumb 341 // needs to catch up to the scroll position. Do nothing except for updating 342 // the scroll bar x to match the thumb width. 343 mScrollbar.setThumbOffsetY(thumbScrollY); 344 } 345 } 346 } else { 347 synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight); 348 } 349 } 350 351 @Override supportsFastScrolling()352 protected boolean supportsFastScrolling() { 353 // Only allow fast scrolling when the user is not searching, since the results are not 354 // grouped in a meaningful order 355 return !mApps.hasFilter(); 356 } 357 358 @Override getCurrentScrollY()359 public int getCurrentScrollY() { 360 // Return early if there are no items or we haven't been measured 361 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 362 if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) { 363 return -1; 364 } 365 366 // Calculate the y and offset for the item 367 View child = getChildAt(0); 368 int position = getChildPosition(child); 369 if (position == NO_POSITION) { 370 return -1; 371 } 372 return getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child)); 373 } 374 getCurrentScrollY(int position, int offset)375 public int getCurrentScrollY(int position, int offset) { 376 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 377 AlphabeticalAppsList.AdapterItem posItem = position < items.size() ? 378 items.get(position) : null; 379 int y = mCachedScrollPositions.get(position, -1); 380 if (y < 0) { 381 y = 0; 382 for (int i = 0; i < position; i++) { 383 AlphabeticalAppsList.AdapterItem item = items.get(i); 384 if (AllAppsGridAdapter.isIconViewType(item.viewType)) { 385 // Break once we reach the desired row 386 if (posItem != null && posItem.viewType == item.viewType && 387 posItem.rowIndex == item.rowIndex) { 388 break; 389 } 390 // Otherwise, only account for the first icon in the row since they are the same 391 // size within a row 392 if (item.rowAppIndex == 0) { 393 y += mViewHeights.get(item.viewType, 0); 394 } 395 } else { 396 // Rest of the views span the full width 397 y += mViewHeights.get(item.viewType, 0); 398 } 399 } 400 mCachedScrollPositions.put(position, y); 401 } 402 403 return getPaddingTop() + y - offset; 404 } 405 406 @Override 407 protected int getScrollbarTrackHeight() { 408 return super.getScrollbarTrackHeight() 409 - Launcher.getLauncher(getContext()).getDragLayer().getInsets().bottom; 410 } 411 412 /** 413 * Returns the available scroll height: 414 * AvailableScrollHeight = Total height of the all items - last page height 415 */ 416 @Override 417 protected int getAvailableScrollHeight() { 418 int paddedHeight = getCurrentScrollY(mApps.getAdapterItems().size(), 0); 419 int totalHeight = paddedHeight + getPaddingBottom(); 420 return totalHeight - getScrollbarTrackHeight(); 421 } 422 423 /** 424 * Updates the bounds of the empty search background. 425 */ 426 private void updateEmptySearchBackgroundBounds() { 427 if (mEmptySearchBackground == null) { 428 return; 429 } 430 431 // Center the empty search background on this new view bounds 432 int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2; 433 int y = mEmptySearchBackgroundTopOffset; 434 mEmptySearchBackground.setBounds(x, y, 435 x + mEmptySearchBackground.getIntrinsicWidth(), 436 y + mEmptySearchBackground.getIntrinsicHeight()); 437 } 438 439 } 440