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 androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT; 19 import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; 20 21 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo; 22 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION; 26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN; 27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP; 28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN; 29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END; 30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE; 31 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND; 32 import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT; 33 import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT; 34 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING; 35 36 import android.content.Context; 37 import android.graphics.Canvas; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.view.LayoutInflater; 41 import android.view.View; 42 import android.widget.TextView; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.constraintlayout.widget.ConstraintLayout; 47 import androidx.constraintlayout.widget.ConstraintSet; 48 import androidx.core.util.Consumer; 49 import androidx.recyclerview.widget.RecyclerView; 50 51 import com.android.launcher3.DeviceProfile; 52 import com.android.launcher3.ExtendedEditText; 53 import com.android.launcher3.FastScrollRecyclerView; 54 import com.android.launcher3.Flags; 55 import com.android.launcher3.LauncherAppState; 56 import com.android.launcher3.R; 57 import com.android.launcher3.Utilities; 58 import com.android.launcher3.logging.StatsLogManager; 59 import com.android.launcher3.views.ActivityContext; 60 61 import java.util.ArrayList; 62 import java.util.Arrays; 63 import java.util.List; 64 65 /** 66 * A RecyclerView with custom fast scroll support for the all apps view. 67 */ 68 public class AllAppsRecyclerView extends FastScrollRecyclerView { 69 protected static final String TAG = "AllAppsRecyclerView"; 70 private static final boolean DEBUG = false; 71 private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING); 72 private Consumer<View> mChildAttachedConsumer; 73 74 protected final int mNumAppsPerRow; 75 private final AllAppsFastScrollHelper mFastScrollHelper; 76 private int mCumulativeVerticalScroll; 77 private ConstraintLayout mLetterList; 78 79 protected AlphabeticalAppsList<?> mApps; 80 AllAppsRecyclerView(Context context)81 public AllAppsRecyclerView(Context context) { 82 this(context, null); 83 } 84 AllAppsRecyclerView(Context context, AttributeSet attrs)85 public AllAppsRecyclerView(Context context, AttributeSet attrs) { 86 this(context, attrs, 0); 87 } 88 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)89 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 90 this(context, attrs, defStyleAttr, 0); 91 } 92 AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)93 public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, 94 int defStyleRes) { 95 super(context, attrs, defStyleAttr); 96 mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns; 97 mFastScrollHelper = new AllAppsFastScrollHelper(this); 98 } 99 100 /** 101 * Sets the list of apps in this view, used to determine the fastscroll position. 102 */ setApps(AlphabeticalAppsList<?> apps)103 public void setApps(AlphabeticalAppsList<?> apps) { 104 mApps = apps; 105 } 106 getApps()107 public AlphabeticalAppsList<?> getApps() { 108 return mApps; 109 } 110 updatePoolSize()111 protected void updatePoolSize() { 112 updatePoolSize(false); 113 } 114 updatePoolSize(boolean hasWorkProfile)115 void updatePoolSize(boolean hasWorkProfile) { 116 DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile(); 117 RecyclerView.RecycledViewPool pool = getRecycledViewPool(); 118 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); 119 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); 120 121 // By default the max num of pool size for app icons is num of app icons in one page of 122 // all apps. 123 int maxPoolSizeForAppIcons = grid.getMaxAllAppsRowCount() 124 * grid.numShownAllAppsColumns; 125 // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to 126 // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows + 127 // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly. 128 maxPoolSizeForAppIcons += 129 PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT; 130 if (hasWorkProfile) { 131 maxPoolSizeForAppIcons *= 2; 132 } 133 pool.setMaxRecycledViews( 134 AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons); 135 } 136 137 @Override onDraw(Canvas c)138 public void onDraw(Canvas c) { 139 if (DEBUG) { 140 Log.d(TAG, "onDraw at = " + System.currentTimeMillis()); 141 } 142 if (DEBUG_LATENCY) { 143 Log.d(SEARCH_LOGGING, getClass().getSimpleName() + " onDraw; time stamp = " 144 + System.currentTimeMillis()); 145 } 146 super.onDraw(c); 147 } 148 149 @Override onSizeChanged(int w, int h, int oldw, int oldh)150 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 151 updatePoolSize(); 152 } 153 onSearchResultsChanged()154 public void onSearchResultsChanged() { 155 // Always scroll the view to the top so the user can see the changed results 156 scrollToTop(); 157 } 158 159 @Override onScrollStateChanged(int state)160 public void onScrollStateChanged(int state) { 161 super.onScrollStateChanged(state); 162 163 StatsLogManager mgr = ActivityContext.lookupContext(getContext()).getStatsLogManager(); 164 switch (state) { 165 case SCROLL_STATE_DRAGGING: 166 mCumulativeVerticalScroll = 0; 167 requestFocus(); 168 mgr.logger().sendToInteractionJankMonitor( 169 LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this); 170 ActivityContext.lookupContext(getContext()).hideKeyboard(); 171 break; 172 case SCROLL_STATE_IDLE: 173 mgr.logger().sendToInteractionJankMonitor( 174 LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END, this); 175 logCumulativeVerticalScroll(); 176 break; 177 } 178 } 179 180 @Override onScrolled(int dx, int dy)181 public void onScrolled(int dx, int dy) { 182 super.onScrolled(dx, dy); 183 mCumulativeVerticalScroll += dy; 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 CharSequence scrollToPositionAtProgress(float touchFraction) { 191 int rowCount = mApps.getNumAppRows(); 192 if (rowCount == 0) { 193 return ""; 194 } 195 196 // Find the fastscroll section that maps to this touch fraction 197 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 198 mApps.getFastScrollerSections(); 199 int count = fastScrollSections.size(); 200 if (count == 0) { 201 return ""; 202 } 203 int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1); 204 AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index); 205 mFastScrollHelper.smoothScrollToSection(section); 206 return section.sectionName; 207 } 208 209 @Override onFastScrollCompleted()210 public void onFastScrollCompleted() { 211 super.onFastScrollCompleted(); 212 mFastScrollHelper.onFastScrollCompleted(); 213 } 214 215 @Override isPaddingOffsetRequired()216 protected boolean isPaddingOffsetRequired() { 217 return true; 218 } 219 220 @Override getTopPaddingOffset()221 protected int getTopPaddingOffset() { 222 return -getPaddingTop(); 223 } 224 225 /** 226 * Updates the bounds for the scrollbar. 227 */ 228 @Override onUpdateScrollbar(int dy)229 public void onUpdateScrollbar(int dy) { 230 if (mApps == null) { 231 return; 232 } 233 List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems(); 234 235 // Skip early if there are no items or we haven't been measured 236 if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) { 237 mScrollbar.setThumbOffsetY(-1); 238 return; 239 } 240 241 // Skip early if, there no child laid out in the container. 242 int scrollY = computeVerticalScrollOffset(); 243 if (scrollY < 0) { 244 mScrollbar.setThumbOffsetY(-1); 245 return; 246 } 247 248 if (Flags.letterFastScroller() && !mScrollbar.isDraggingThumb()) { 249 setLettersToScrollLayout(mApps.getFastScrollerSections()); 250 } 251 // Only show the scrollbar if there is height to be scrolled 252 int availableScrollBarHeight = getAvailableScrollBarHeight(); 253 int availableScrollHeight = getAvailableScrollHeight(); 254 if (availableScrollHeight <= 0) { 255 mScrollbar.setThumbOffsetY(-1); 256 return; 257 } 258 259 if (mScrollbar.isThumbDetached()) { 260 if (!mScrollbar.isDraggingThumb()) { 261 // Calculate the current scroll position, the scrollY of the recycler view accounts 262 // for the view padding, while the scrollBarY is drawn right up to the background 263 // padding (ignoring padding) 264 int scrollBarY = (int) 265 (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 266 267 int thumbScrollY = mScrollbar.getThumbOffsetY(); 268 int diffScrollY = scrollBarY - thumbScrollY; 269 if (diffScrollY * dy > 0f) { 270 // User is scrolling in the same direction the thumb needs to catch up to the 271 // current scroll position. We do this by mapping the difference in movement 272 // from the original scroll bar position to the difference in movement necessary 273 // in the detached thumb position to ensure that both speed towards the same 274 // position at either end of the list. 275 if (dy < 0) { 276 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 277 thumbScrollY += Math.max(offset, diffScrollY); 278 } else { 279 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 280 (float) (availableScrollBarHeight - scrollBarY)); 281 thumbScrollY += Math.min(offset, diffScrollY); 282 } 283 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 284 mScrollbar.setThumbOffsetY(thumbScrollY); 285 if (scrollBarY == thumbScrollY) { 286 mScrollbar.reattachThumbToScroll(); 287 } 288 } else { 289 // User is scrolling in an opposite direction to the direction that the thumb 290 // needs to catch up to the scroll position. Do nothing except for updating 291 // the scroll bar x to match the thumb width. 292 mScrollbar.setThumbOffsetY(thumbScrollY); 293 } 294 } 295 } else { 296 synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight); 297 } 298 } 299 300 /** 301 * This will be called just before a new child is attached to the window. Passing in null will 302 * remove the consumer. 303 */ setChildAttachedConsumer(@ullable Consumer<View> childAttachedConsumer)304 protected void setChildAttachedConsumer(@Nullable Consumer<View> childAttachedConsumer) { 305 mChildAttachedConsumer = childAttachedConsumer; 306 } 307 308 @Override onChildAttachedToWindow(@onNull View child)309 public void onChildAttachedToWindow(@NonNull View child) { 310 if (mChildAttachedConsumer != null) { 311 mChildAttachedConsumer.accept(child); 312 } 313 super.onChildAttachedToWindow(child); 314 } 315 316 @Override getScrollBarTop()317 public int getScrollBarTop() { 318 return getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding); 319 } 320 321 @Override getScrollBarMarginBottom()322 public int getScrollBarMarginBottom() { 323 return getRootWindowInsets() == null ? 0 324 : getRootWindowInsets().getSystemWindowInsetBottom(); 325 } 326 327 @Override hasOverlappingRendering()328 public boolean hasOverlappingRendering() { 329 return false; 330 } 331 setLettersToScrollLayout( List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections)332 public void setLettersToScrollLayout( 333 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections) { 334 if (fastScrollSections.isEmpty()) { 335 return; 336 } 337 if (mLetterList != null) { 338 mLetterList.removeAllViews(); 339 } 340 Context context = getContext(); 341 ActivityAllAppsContainerView<?> allAppsContainerView = 342 ActivityContext.lookupContext(context).getAppsView(); 343 mLetterList = allAppsContainerView.getFastScrollerLetterList(); 344 mLetterList.setPadding(0, getScrollBarTop(), 0, getScrollBarMarginBottom()); 345 List<LetterListTextView> textViews = new ArrayList<>(); 346 for (int i = 0; i < fastScrollSections.size(); i++) { 347 AlphabeticalAppsList.FastScrollSectionInfo sectionInfo = fastScrollSections.get(i); 348 LetterListTextView textView = 349 (LetterListTextView) LayoutInflater.from(context).inflate( 350 R.layout.fast_scroller_letter_list_text_view, mLetterList, false); 351 int viewId = View.generateViewId(); 352 textView.apply(sectionInfo /* FastScrollSectionInfo */, viewId /* viewId */); 353 sectionInfo.setId(viewId); 354 if (i == fastScrollSections.size() - 1) { 355 // The last section info is just a duplicate so that user can scroll to the bottom. 356 textView.setVisibility(INVISIBLE); 357 } 358 textViews.add(textView); 359 mLetterList.addView(textView); 360 } 361 // Need to add an extra textview to be aligned. 362 LetterListTextView lastLetterListTextView = new LetterListTextView(context); 363 int currentId = View.generateViewId(); 364 lastLetterListTextView.setId(currentId); 365 lastLetterListTextView.setVisibility(INVISIBLE); 366 textViews.add(lastLetterListTextView); 367 mLetterList.addView(lastLetterListTextView); 368 constraintTextViewsVertically(mLetterList, textViews); 369 mLetterList.setVisibility(VISIBLE); 370 // Set the alpha to 0 to avoid the letter list being shown when it shouldn't be. 371 mLetterList.setAlpha(0); 372 } 373 constraintTextViewsVertically(ConstraintLayout constraintLayout, List<LetterListTextView> textViews)374 private void constraintTextViewsVertically(ConstraintLayout constraintLayout, 375 List<LetterListTextView> textViews) { 376 ConstraintSet chain = new ConstraintSet(); 377 chain.clone(constraintLayout); 378 for (int i = 0; i < textViews.size(); i++) { 379 LetterListTextView currentView = textViews.get(i); 380 if (i == 0) { 381 chain.connect(currentView.getId(), ConstraintSet.TOP, ConstraintSet.PARENT_ID, 382 ConstraintSet.TOP); 383 } else { 384 chain.connect(currentView.getId(), ConstraintSet.TOP, textViews.get(i-1).getId(), 385 ConstraintSet.BOTTOM); 386 } 387 chain.connect(currentView.getId(), ConstraintSet.START, constraintLayout.getId(), 388 ConstraintSet.START); 389 chain.connect(currentView.getId(), ConstraintSet.END, constraintLayout.getId(), 390 ConstraintSet.END); 391 } 392 int[] viewIds = textViews.stream().mapToInt(TextView::getId).toArray(); 393 float[] weights = new float[textViews.size()]; 394 Arrays.fill(weights,1); // fill with 1 for equal weights 395 chain.createVerticalChain(constraintLayout.getId(), ConstraintSet.TOP, 396 constraintLayout.getId(), ConstraintSet.BOTTOM, viewIds, weights, 397 ConstraintSet.CHAIN_SPREAD); 398 chain.applyTo(constraintLayout); 399 } 400 401 @Override getLetterList()402 public ConstraintLayout getLetterList() { 403 return mLetterList; 404 } 405 logCumulativeVerticalScroll()406 private void logCumulativeVerticalScroll() { 407 ActivityContext context = ActivityContext.lookupContext(getContext()); 408 StatsLogManager mgr = context.getStatsLogManager(); 409 ActivityAllAppsContainerView<?> appsView = context.getAppsView(); 410 ExtendedEditText editText = appsView.getSearchUiManager().getEditText(); 411 ContainerInfo containerInfo = ContainerInfo.newBuilder().setSearchResultContainer( 412 SearchResultContainer 413 .newBuilder() 414 .setQueryLength((editText == null) ? -1 : editText.length())).build(); 415 if (mCumulativeVerticalScroll == 0) { 416 // mCumulativeVerticalScroll == 0 when user comes back to original position, we 417 // don't know the direction of scrolling. 418 mgr.logger().withContainerInfo(containerInfo).log( 419 LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION); 420 return; 421 } else if (appsView.isSearching()) { 422 // In search results page 423 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 424 ? LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN 425 : LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP); 426 return; 427 } else if (appsView.mViewPager != null) { 428 int currentPage = appsView.mViewPager.getCurrentPage(); 429 if (currentPage == ActivityAllAppsContainerView.AdapterHolder.WORK) { 430 // In work A-Z list 431 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 432 ? LAUNCHER_WORK_FAB_BUTTON_COLLAPSE 433 : LAUNCHER_WORK_FAB_BUTTON_EXTEND); 434 return; 435 } 436 } 437 // In personal A-Z list 438 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 439 ? LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN 440 : LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP); 441 } 442 } 443