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