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 com.android.launcher3.config.FeatureFlags.ALL_APPS_GONE_VISIBILITY; 19 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_RV_PREINFLATION; 20 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo; 21 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN; 26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP; 27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN; 28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END; 29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE; 30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND; 31 import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.EXTRA_ICONS_COUNT; 32 import static com.android.launcher3.recyclerview.AllAppsRecyclerViewPoolKt.PREINFLATE_ICONS_ROW_COUNT; 33 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING; 34 35 import android.content.Context; 36 import android.graphics.Canvas; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.view.View; 40 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.core.util.Consumer; 44 import androidx.recyclerview.widget.RecyclerView; 45 46 import com.android.launcher3.DeviceProfile; 47 import com.android.launcher3.ExtendedEditText; 48 import com.android.launcher3.FastScrollRecyclerView; 49 import com.android.launcher3.LauncherAppState; 50 import com.android.launcher3.R; 51 import com.android.launcher3.Utilities; 52 import com.android.launcher3.logging.StatsLogManager; 53 import com.android.launcher3.views.ActivityContext; 54 55 import java.util.List; 56 57 /** 58 * A RecyclerView with custom fast scroll support for the all apps view. 59 */ 60 public class AllAppsRecyclerView extends FastScrollRecyclerView { 61 protected static final String TAG = "AllAppsRecyclerView"; 62 private static final boolean DEBUG = false; 63 private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING); 64 private Consumer<View> mChildAttachedConsumer; 65 66 protected final int mNumAppsPerRow; 67 private final AllAppsFastScrollHelper mFastScrollHelper; 68 private int mCumulativeVerticalScroll; 69 70 protected AlphabeticalAppsList<?> mApps; 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 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 protected void updatePoolSize() { 103 updatePoolSize(false); 104 } 105 updatePoolSize(boolean hasWorkProfile)106 void updatePoolSize(boolean hasWorkProfile) { 107 DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile(); 108 RecyclerView.RecycledViewPool pool = getRecycledViewPool(); 109 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); 110 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); 111 112 // By default the max num of pool size for app icons is num of app icons in one page of 113 // all apps. 114 int maxPoolSizeForAppIcons = grid.getMaxAllAppsRowCount() 115 * grid.numShownAllAppsColumns; 116 if (ALL_APPS_GONE_VISIBILITY.get() && ENABLE_ALL_APPS_RV_PREINFLATION.get()) { 117 // If we set all apps' hidden visibility to GONE and enable pre-inflation, we want to 118 // preinflate one page of all apps icons plus [PREINFLATE_ICONS_ROW_COUNT] rows + 119 // [EXTRA_ICONS_COUNT]. Thus we need to bump the max pool size of app icons accordingly. 120 maxPoolSizeForAppIcons += 121 PREINFLATE_ICONS_ROW_COUNT * grid.numShownAllAppsColumns + EXTRA_ICONS_COUNT; 122 } 123 if (hasWorkProfile) { 124 maxPoolSizeForAppIcons *= 2; 125 } 126 pool.setMaxRecycledViews( 127 AllAppsGridAdapter.VIEW_TYPE_ICON, maxPoolSizeForAppIcons); 128 } 129 130 @Override onDraw(Canvas c)131 public void onDraw(Canvas c) { 132 if (DEBUG) { 133 Log.d(TAG, "onDraw at = " + System.currentTimeMillis()); 134 } 135 if (DEBUG_LATENCY) { 136 Log.d(SEARCH_LOGGING, getClass().getSimpleName() + " onDraw; time stamp = " 137 + System.currentTimeMillis()); 138 } 139 super.onDraw(c); 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 updatePoolSize(); 145 } 146 onSearchResultsChanged()147 public void onSearchResultsChanged() { 148 // Always scroll the view to the top so the user can see the changed results 149 scrollToTop(); 150 } 151 152 @Override onScrollStateChanged(int state)153 public void onScrollStateChanged(int state) { 154 super.onScrollStateChanged(state); 155 156 StatsLogManager mgr = ActivityContext.lookupContext(getContext()).getStatsLogManager(); 157 switch (state) { 158 case SCROLL_STATE_DRAGGING: 159 mCumulativeVerticalScroll = 0; 160 requestFocus(); 161 mgr.logger().sendToInteractionJankMonitor( 162 LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this); 163 ActivityContext.lookupContext(getContext()).hideKeyboard(); 164 break; 165 case SCROLL_STATE_IDLE: 166 mgr.logger().sendToInteractionJankMonitor( 167 LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END, this); 168 logCumulativeVerticalScroll(); 169 break; 170 } 171 } 172 173 @Override onScrolled(int dx, int dy)174 public void onScrolled(int dx, int dy) { 175 super.onScrolled(dx, dy); 176 mCumulativeVerticalScroll += dy; 177 } 178 179 /** 180 * Maps the touch (from 0..1) to the adapter position that should be visible. 181 */ 182 @Override scrollToPositionAtProgress(float touchFraction)183 public CharSequence scrollToPositionAtProgress(float touchFraction) { 184 int rowCount = mApps.getNumAppRows(); 185 if (rowCount == 0) { 186 return ""; 187 } 188 189 // Find the fastscroll section that maps to this touch fraction 190 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 191 mApps.getFastScrollerSections(); 192 int count = fastScrollSections.size(); 193 if (count == 0) { 194 return ""; 195 } 196 int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1); 197 AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index); 198 mFastScrollHelper.smoothScrollToSection(section); 199 return section.sectionName; 200 } 201 202 @Override onFastScrollCompleted()203 public void onFastScrollCompleted() { 204 super.onFastScrollCompleted(); 205 mFastScrollHelper.onFastScrollCompleted(); 206 } 207 208 @Override isPaddingOffsetRequired()209 protected boolean isPaddingOffsetRequired() { 210 return true; 211 } 212 213 @Override getTopPaddingOffset()214 protected int getTopPaddingOffset() { 215 return -getPaddingTop(); 216 } 217 218 /** 219 * Updates the bounds for the scrollbar. 220 */ 221 @Override onUpdateScrollbar(int dy)222 public void onUpdateScrollbar(int dy) { 223 if (mApps == null) { 224 return; 225 } 226 List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems(); 227 228 // Skip early if there are no items or we haven't been measured 229 if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) { 230 mScrollbar.setThumbOffsetY(-1); 231 return; 232 } 233 234 // Skip early if, there no child laid out in the container. 235 int scrollY = computeVerticalScrollOffset(); 236 if (scrollY < 0) { 237 mScrollbar.setThumbOffsetY(-1); 238 return; 239 } 240 241 // Only show the scrollbar if there is height to be scrolled 242 int availableScrollBarHeight = getAvailableScrollBarHeight(); 243 int availableScrollHeight = getAvailableScrollHeight(); 244 if (availableScrollHeight <= 0) { 245 mScrollbar.setThumbOffsetY(-1); 246 return; 247 } 248 249 if (mScrollbar.isThumbDetached()) { 250 if (!mScrollbar.isDraggingThumb()) { 251 // Calculate the current scroll position, the scrollY of the recycler view accounts 252 // for the view padding, while the scrollBarY is drawn right up to the background 253 // padding (ignoring padding) 254 int scrollBarY = (int) 255 (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 256 257 int thumbScrollY = mScrollbar.getThumbOffsetY(); 258 int diffScrollY = scrollBarY - thumbScrollY; 259 if (diffScrollY * dy > 0f) { 260 // User is scrolling in the same direction the thumb needs to catch up to the 261 // current scroll position. We do this by mapping the difference in movement 262 // from the original scroll bar position to the difference in movement necessary 263 // in the detached thumb position to ensure that both speed towards the same 264 // position at either end of the list. 265 if (dy < 0) { 266 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 267 thumbScrollY += Math.max(offset, diffScrollY); 268 } else { 269 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 270 (float) (availableScrollBarHeight - scrollBarY)); 271 thumbScrollY += Math.min(offset, diffScrollY); 272 } 273 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 274 mScrollbar.setThumbOffsetY(thumbScrollY); 275 if (scrollBarY == thumbScrollY) { 276 mScrollbar.reattachThumbToScroll(); 277 } 278 } else { 279 // User is scrolling in an opposite direction to the direction that the thumb 280 // needs to catch up to the scroll position. Do nothing except for updating 281 // the scroll bar x to match the thumb width. 282 mScrollbar.setThumbOffsetY(thumbScrollY); 283 } 284 } 285 } else { 286 synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight); 287 } 288 } 289 290 /** 291 * This will be called just before a new child is attached to the window. Passing in null will 292 * remove the consumer. 293 */ setChildAttachedConsumer(@ullable Consumer<View> childAttachedConsumer)294 protected void setChildAttachedConsumer(@Nullable Consumer<View> childAttachedConsumer) { 295 mChildAttachedConsumer = childAttachedConsumer; 296 } 297 298 @Override onChildAttachedToWindow(@onNull View child)299 public void onChildAttachedToWindow(@NonNull View child) { 300 if (mChildAttachedConsumer != null) { 301 mChildAttachedConsumer.accept(child); 302 } 303 super.onChildAttachedToWindow(child); 304 } 305 306 @Override getScrollBarTop()307 public int getScrollBarTop() { 308 return getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding); 309 } 310 311 @Override getScrollBarMarginBottom()312 public int getScrollBarMarginBottom() { 313 return getRootWindowInsets() == null ? 0 314 : getRootWindowInsets().getSystemWindowInsetBottom(); 315 } 316 317 @Override hasOverlappingRendering()318 public boolean hasOverlappingRendering() { 319 return false; 320 } 321 logCumulativeVerticalScroll()322 private void logCumulativeVerticalScroll() { 323 ActivityContext context = ActivityContext.lookupContext(getContext()); 324 StatsLogManager mgr = context.getStatsLogManager(); 325 ActivityAllAppsContainerView<?> appsView = context.getAppsView(); 326 ExtendedEditText editText = appsView.getSearchUiManager().getEditText(); 327 ContainerInfo containerInfo = ContainerInfo.newBuilder().setSearchResultContainer( 328 SearchResultContainer 329 .newBuilder() 330 .setQueryLength((editText == null) ? -1 : editText.length())).build(); 331 if (mCumulativeVerticalScroll == 0) { 332 // mCumulativeVerticalScroll == 0 when user comes back to original position, we 333 // don't know the direction of scrolling. 334 mgr.logger().withContainerInfo(containerInfo).log( 335 LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION); 336 return; 337 } else if (appsView.isSearching()) { 338 // In search results page 339 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 340 ? LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN 341 : LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP); 342 return; 343 } else if (appsView.mViewPager != null) { 344 int currentPage = appsView.mViewPager.getCurrentPage(); 345 if (currentPage == ActivityAllAppsContainerView.AdapterHolder.WORK) { 346 // In work A-Z list 347 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 348 ? LAUNCHER_WORK_FAB_BUTTON_COLLAPSE 349 : LAUNCHER_WORK_FAB_BUTTON_EXTEND); 350 return; 351 } 352 } 353 // In personal A-Z list 354 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 355 ? LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN 356 : LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP); 357 } 358 } 359