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