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