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 static android.view.View.MeasureSpec.EXACTLY; 19 import static android.view.View.MeasureSpec.UNSPECIFIED; 20 import static android.view.View.MeasureSpec.makeMeasureSpec; 21 22 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; 23 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.graphics.Canvas; 27 import android.graphics.drawable.Drawable; 28 import android.util.AttributeSet; 29 import android.util.SparseIntArray; 30 import android.view.MotionEvent; 31 import android.view.View; 32 33 import androidx.recyclerview.widget.RecyclerView; 34 35 import com.android.launcher3.BaseDraggingActivity; 36 import com.android.launcher3.BaseRecyclerView; 37 import com.android.launcher3.DeviceProfile; 38 import com.android.launcher3.LauncherAppState; 39 import com.android.launcher3.R; 40 import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager; 41 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider; 42 import com.android.launcher3.model.data.ItemInfo; 43 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 44 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 45 import com.android.launcher3.views.RecyclerViewFastScroller; 46 47 import java.util.ArrayList; 48 import java.util.List; 49 50 /** 51 * A RecyclerView with custom fast scroll support for the all apps view. 52 */ 53 public class AllAppsRecyclerView extends BaseRecyclerView implements LogContainerProvider { 54 55 private AlphabeticalAppsList mApps; 56 private final int mNumAppsPerRow; 57 58 // The specific view heights that we use to calculate scroll 59 private final SparseIntArray mViewHeights = new SparseIntArray(); 60 private final SparseIntArray mCachedScrollPositions = new SparseIntArray(); 61 private final AllAppsFastScrollHelper mFastScrollHelper; 62 63 // The empty-search result background 64 private AllAppsBackgroundDrawable mEmptySearchBackground; 65 private int mEmptySearchBackgroundTopOffset; 66 67 private ArrayList<View> mAutoSizedOverlays = new ArrayList<>(); 68 AllAppsRecyclerView(Context context)69 public AllAppsRecyclerView(Context context) { 70 this(context, null); 71 } 72 AllAppsRecyclerView(Context context, AttributeSet attrs)73 public AllAppsRecyclerView(Context context, AttributeSet attrs) { 74 this(context, attrs, 0); 75 } 76 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)77 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 78 this(context, attrs, defStyleAttr, 0); 79 } 80 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)81 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, 82 int defStyleRes) { 83 super(context, attrs, defStyleAttr); 84 Resources res = getResources(); 85 mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( 86 R.dimen.all_apps_empty_search_bg_top_offset); 87 mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns; 88 mFastScrollHelper = new AllAppsFastScrollHelper(this); 89 } 90 91 /** 92 * Sets the list of apps in this view, used to determine the fastscroll position. 93 */ setApps(AlphabeticalAppsList apps)94 public void setApps(AlphabeticalAppsList apps) { 95 mApps = apps; 96 } 97 getApps()98 public AlphabeticalAppsList getApps() { 99 return mApps; 100 } 101 updatePoolSize()102 private void updatePoolSize() { 103 DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile(); 104 RecyclerView.RecycledViewPool pool = getRecycledViewPool(); 105 int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); 106 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); 107 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); 108 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1); 109 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow); 110 111 mViewHeights.clear(); 112 mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx); 113 } 114 115 /** 116 * Scrolls this recycler view to the top. 117 */ scrollToTop()118 public void scrollToTop() { 119 // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling 120 if (mScrollbar != null) { 121 mScrollbar.reattachThumbToScroll(); 122 } 123 if (getLayoutManager() instanceof AppsGridLayoutManager) { 124 AppsGridLayoutManager layoutManager = (AppsGridLayoutManager) getLayoutManager(); 125 if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) { 126 // We are at the top, so don't scrollToPosition (would cause unnecessary relayout). 127 return; 128 } 129 } 130 scrollToPosition(0); 131 } 132 133 @Override onDraw(Canvas c)134 public void onDraw(Canvas c) { 135 // Draw the background 136 if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { 137 mEmptySearchBackground.draw(c); 138 } 139 140 super.onDraw(c); 141 } 142 143 @Override verifyDrawable(Drawable who)144 protected boolean verifyDrawable(Drawable who) { 145 return who == mEmptySearchBackground || super.verifyDrawable(who); 146 } 147 148 @Override onSizeChanged(int w, int h, int oldw, int oldh)149 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 150 updateEmptySearchBackgroundBounds(); 151 updatePoolSize(); 152 for (int i = 0; i < mAutoSizedOverlays.size(); i++) { 153 View overlay = mAutoSizedOverlays.get(i); 154 overlay.measure(makeMeasureSpec(w, EXACTLY), makeMeasureSpec(w, EXACTLY)); 155 overlay.layout(0, 0, w, h); 156 } 157 } 158 159 /** 160 * Adds an overlay that automatically rescales with the recyclerview. 161 */ addAutoSizedOverlay(View overlay)162 public void addAutoSizedOverlay(View overlay) { 163 mAutoSizedOverlays.add(overlay); 164 getOverlay().add(overlay); 165 onSizeChanged(getWidth(), getHeight(), getWidth(), getHeight()); 166 } 167 168 /** 169 * Clears auto scaling overlay views added by #addAutoSizedOverlay 170 */ clearAutoSizedOverlays()171 public void clearAutoSizedOverlays() { 172 for (View v : mAutoSizedOverlays) { 173 getOverlay().remove(v); 174 } 175 mAutoSizedOverlays.clear(); 176 } 177 178 @Override fillInLogContainerData(ItemInfo childInfo, Target child, ArrayList<Target> parents)179 public void fillInLogContainerData(ItemInfo childInfo, Target child, 180 ArrayList<Target> parents) { 181 parents.add(newContainerTarget( 182 getApps().hasFilter() ? ContainerType.SEARCHRESULT : ContainerType.ALLAPPS)); 183 } 184 onSearchResultsChanged()185 public void onSearchResultsChanged() { 186 // Always scroll the view to the top so the user can see the changed results 187 scrollToTop(); 188 189 if (mApps.hasNoFilteredResults()) { 190 if (mEmptySearchBackground == null) { 191 mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext()); 192 mEmptySearchBackground.setAlpha(0); 193 mEmptySearchBackground.setCallback(this); 194 updateEmptySearchBackgroundBounds(); 195 } 196 mEmptySearchBackground.animateBgAlpha(1f, 150); 197 } else if (mEmptySearchBackground != null) { 198 // For the time being, we just immediately hide the background to ensure that it does 199 // not overlap with the results 200 mEmptySearchBackground.setBgAlpha(0f); 201 } 202 } 203 204 @Override onInterceptTouchEvent(MotionEvent e)205 public boolean onInterceptTouchEvent(MotionEvent e) { 206 boolean result = super.onInterceptTouchEvent(e); 207 if (!result && e.getAction() == MotionEvent.ACTION_DOWN 208 && mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { 209 mEmptySearchBackground.setHotspot(e.getX(), e.getY()); 210 } 211 return result; 212 } 213 214 /** 215 * Maps the touch (from 0..1) to the adapter position that should be visible. 216 */ 217 @Override scrollToPositionAtProgress(float touchFraction)218 public String scrollToPositionAtProgress(float touchFraction) { 219 int rowCount = mApps.getNumAppRows(); 220 if (rowCount == 0) { 221 return ""; 222 } 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 mFastScrollHelper.smoothScrollToSection(lastInfo); 237 return lastInfo.sectionName; 238 } 239 240 @Override onFastScrollCompleted()241 public void onFastScrollCompleted() { 242 super.onFastScrollCompleted(); 243 mFastScrollHelper.onFastScrollCompleted(); 244 } 245 246 @Override setAdapter(Adapter adapter)247 public void setAdapter(Adapter adapter) { 248 super.setAdapter(adapter); 249 adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 250 public void onChanged() { 251 mCachedScrollPositions.clear(); 252 } 253 }); 254 } 255 256 @Override getBottomFadingEdgeStrength()257 protected float getBottomFadingEdgeStrength() { 258 // No bottom fading edge. 259 return 0; 260 } 261 262 @Override isPaddingOffsetRequired()263 protected boolean isPaddingOffsetRequired() { 264 return true; 265 } 266 267 @Override getTopPaddingOffset()268 protected int getTopPaddingOffset() { 269 return -getPaddingTop(); 270 } 271 272 /** 273 * Updates the bounds for the scrollbar. 274 */ 275 @Override onUpdateScrollbar(int dy)276 public void onUpdateScrollbar(int dy) { 277 if (mApps == null) { 278 return; 279 } 280 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 281 282 // Skip early if there are no items or we haven't been measured 283 if (items.isEmpty() || mNumAppsPerRow == 0) { 284 mScrollbar.setThumbOffsetY(-1); 285 return; 286 } 287 288 // Skip early if, there no child laid out in the container. 289 int scrollY = getCurrentScrollY(); 290 if (scrollY < 0) { 291 mScrollbar.setThumbOffsetY(-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(); 298 if (availableScrollHeight <= 0) { 299 mScrollbar.setThumbOffsetY(-1); 300 return; 301 } 302 303 if (mScrollbar.isThumbDetached()) { 304 if (!mScrollbar.isDraggingThumb()) { 305 // Calculate the current scroll position, the scrollY of the recycler view accounts 306 // for the view padding, while the scrollBarY is drawn right up to the background 307 // padding (ignoring padding) 308 int scrollBarY = (int) 309 (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 310 311 int thumbScrollY = mScrollbar.getThumbOffsetY(); 312 int diffScrollY = scrollBarY - thumbScrollY; 313 if (diffScrollY * dy > 0f) { 314 // User is scrolling in the same direction the thumb needs to catch up to the 315 // current scroll position. We do this by mapping the difference in movement 316 // from the original scroll bar position to the difference in movement necessary 317 // in the detached thumb position to ensure that both speed towards the same 318 // position at either end of the list. 319 if (dy < 0) { 320 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 321 thumbScrollY += Math.max(offset, diffScrollY); 322 } else { 323 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 324 (float) (availableScrollBarHeight - scrollBarY)); 325 thumbScrollY += Math.min(offset, diffScrollY); 326 } 327 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 328 mScrollbar.setThumbOffsetY(thumbScrollY); 329 if (scrollBarY == thumbScrollY) { 330 mScrollbar.reattachThumbToScroll(); 331 } 332 } else { 333 // User is scrolling in an opposite direction to the direction that the thumb 334 // needs to catch up to the scroll position. Do nothing except for updating 335 // the scroll bar x to match the thumb width. 336 mScrollbar.setThumbOffsetY(thumbScrollY); 337 } 338 } 339 } else { 340 synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight); 341 } 342 } 343 344 @Override supportsFastScrolling()345 public boolean supportsFastScrolling() { 346 // Only allow fast scrolling when the user is not searching, since the results are not 347 // grouped in a meaningful order 348 return !mApps.hasFilter(); 349 } 350 351 @Override getCurrentScrollY()352 public int getCurrentScrollY() { 353 // Return early if there are no items or we haven't been measured 354 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 355 if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) { 356 return -1; 357 } 358 359 // Calculate the y and offset for the item 360 View child = getChildAt(0); 361 int position = getChildPosition(child); 362 if (position == NO_POSITION) { 363 return -1; 364 } 365 return getPaddingTop() + 366 getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child)); 367 } 368 getCurrentScrollY(int position, int offset)369 public int getCurrentScrollY(int position, int offset) { 370 List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); 371 AlphabeticalAppsList.AdapterItem posItem = position < items.size() ? 372 items.get(position) : null; 373 int y = mCachedScrollPositions.get(position, -1); 374 if (y < 0) { 375 y = 0; 376 for (int i = 0; i < position; i++) { 377 AlphabeticalAppsList.AdapterItem item = items.get(i); 378 if (AllAppsGridAdapter.isIconViewType(item.viewType)) { 379 // Break once we reach the desired row 380 if (posItem != null && posItem.viewType == item.viewType && 381 posItem.rowIndex == item.rowIndex) { 382 break; 383 } 384 // Otherwise, only account for the first icon in the row since they are the same 385 // size within a row 386 if (item.rowAppIndex == 0) { 387 y += mViewHeights.get(item.viewType, 0); 388 } 389 } else { 390 // Rest of the views span the full width 391 int elHeight = mViewHeights.get(item.viewType); 392 if (elHeight == 0) { 393 ViewHolder holder = findViewHolderForAdapterPosition(i); 394 if (holder == null) { 395 holder = getAdapter().createViewHolder(this, item.viewType); 396 getAdapter().onBindViewHolder(holder, i); 397 holder.itemView.measure(UNSPECIFIED, UNSPECIFIED); 398 elHeight = holder.itemView.getMeasuredHeight(); 399 400 getRecycledViewPool().putRecycledView(holder); 401 } else { 402 elHeight = holder.itemView.getMeasuredHeight(); 403 } 404 } 405 y += elHeight; 406 } 407 } 408 mCachedScrollPositions.put(position, y); 409 } 410 return y - offset; 411 } 412 413 /** 414 * Returns the available scroll height: 415 * AvailableScrollHeight = Total height of the all items - last page height 416 */ 417 @Override 418 protected int getAvailableScrollHeight() { 419 return getPaddingTop() + getCurrentScrollY(getAdapter().getItemCount(), 0) 420 - getHeight() + getPaddingBottom(); 421 } 422 423 public int getScrollBarTop() { 424 return getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding); 425 } 426 427 public RecyclerViewFastScroller getScrollbar() { 428 return mScrollbar; 429 } 430 431 /** 432 * Updates the bounds of the empty search background. 433 */ 434 private void updateEmptySearchBackgroundBounds() { 435 if (mEmptySearchBackground == null) { 436 return; 437 } 438 439 // Center the empty search background on this new view bounds 440 int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2; 441 int y = mEmptySearchBackgroundTopOffset; 442 mEmptySearchBackground.setBounds(x, y, 443 x + mEmptySearchBackground.getIntrinsicWidth(), 444 y + mEmptySearchBackground.getIntrinsicHeight()); 445 } 446 447 @Override 448 public boolean hasOverlappingRendering() { 449 return false; 450 } 451 } 452