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.logger.LauncherAtom.ContainerInfo; 19 import static com.android.launcher3.logger.LauncherAtom.SearchResultContainer; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN; 26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END; 27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_COLLAPSE; 28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORK_FAB_BUTTON_EXTEND; 29 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING; 30 31 import android.content.Context; 32 import android.graphics.Canvas; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 36 import androidx.recyclerview.widget.RecyclerView; 37 38 import com.android.launcher3.DeviceProfile; 39 import com.android.launcher3.ExtendedEditText; 40 import com.android.launcher3.FastScrollRecyclerView; 41 import com.android.launcher3.LauncherAppState; 42 import com.android.launcher3.R; 43 import com.android.launcher3.Utilities; 44 import com.android.launcher3.logging.StatsLogManager; 45 import com.android.launcher3.views.ActivityContext; 46 47 import java.util.List; 48 49 /** 50 * A RecyclerView with custom fast scroll support for the all apps view. 51 */ 52 public class AllAppsRecyclerView extends FastScrollRecyclerView { 53 protected static final String TAG = "AllAppsRecyclerView"; 54 private static final boolean DEBUG = false; 55 private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING); 56 57 protected final int mNumAppsPerRow; 58 private final AllAppsFastScrollHelper mFastScrollHelper; 59 private int mCumulativeVerticalScroll; 60 61 protected AlphabeticalAppsList<?> mApps; 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 mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns; 79 mFastScrollHelper = new AllAppsFastScrollHelper(this); 80 } 81 82 /** 83 * Sets the list of apps in this view, used to determine the fastscroll position. 84 */ setApps(AlphabeticalAppsList<?> apps)85 public void setApps(AlphabeticalAppsList<?> apps) { 86 mApps = apps; 87 } 88 getApps()89 public AlphabeticalAppsList<?> getApps() { 90 return mApps; 91 } 92 updatePoolSize()93 protected void updatePoolSize() { 94 DeviceProfile grid = ActivityContext.lookupContext(getContext()).getDeviceProfile(); 95 RecyclerView.RecycledViewPool pool = getRecycledViewPool(); 96 int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); 97 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1); 98 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1); 99 pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows 100 * (mNumAppsPerRow + 1)); 101 } 102 103 @Override onDraw(Canvas c)104 public void onDraw(Canvas c) { 105 if (DEBUG) { 106 Log.d(TAG, "onDraw at = " + System.currentTimeMillis()); 107 } 108 if (DEBUG_LATENCY) { 109 Log.d(SEARCH_LOGGING, getClass().getSimpleName() + " onDraw; time stamp = " 110 + System.currentTimeMillis()); 111 } 112 super.onDraw(c); 113 } 114 115 @Override onSizeChanged(int w, int h, int oldw, int oldh)116 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 117 updatePoolSize(); 118 } 119 onSearchResultsChanged()120 public void onSearchResultsChanged() { 121 // Always scroll the view to the top so the user can see the changed results 122 scrollToTop(); 123 } 124 125 @Override onScrollStateChanged(int state)126 public void onScrollStateChanged(int state) { 127 super.onScrollStateChanged(state); 128 129 StatsLogManager mgr = ActivityContext.lookupContext(getContext()).getStatsLogManager(); 130 switch (state) { 131 case SCROLL_STATE_DRAGGING: 132 mCumulativeVerticalScroll = 0; 133 requestFocus(); 134 mgr.logger().sendToInteractionJankMonitor( 135 LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this); 136 ActivityContext.lookupContext(getContext()).hideKeyboard(); 137 break; 138 case SCROLL_STATE_IDLE: 139 mgr.logger().sendToInteractionJankMonitor( 140 LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END, this); 141 logCumulativeVerticalScroll(); 142 break; 143 } 144 } 145 146 @Override onScrolled(int dx, int dy)147 public void onScrolled(int dx, int dy) { 148 super.onScrolled(dx, dy); 149 mCumulativeVerticalScroll += dy; 150 } 151 152 /** 153 * Maps the touch (from 0..1) to the adapter position that should be visible. 154 */ 155 @Override scrollToPositionAtProgress(float touchFraction)156 public String scrollToPositionAtProgress(float touchFraction) { 157 int rowCount = mApps.getNumAppRows(); 158 if (rowCount == 0) { 159 return ""; 160 } 161 162 // Find the fastscroll section that maps to this touch fraction 163 List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections = 164 mApps.getFastScrollerSections(); 165 int count = fastScrollSections.size(); 166 if (count == 0) { 167 return ""; 168 } 169 int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1); 170 AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index); 171 mFastScrollHelper.smoothScrollToSection(section); 172 return section.sectionName; 173 } 174 175 @Override onFastScrollCompleted()176 public void onFastScrollCompleted() { 177 super.onFastScrollCompleted(); 178 mFastScrollHelper.onFastScrollCompleted(); 179 } 180 181 @Override isPaddingOffsetRequired()182 protected boolean isPaddingOffsetRequired() { 183 return true; 184 } 185 186 @Override getTopPaddingOffset()187 protected int getTopPaddingOffset() { 188 return -getPaddingTop(); 189 } 190 191 /** 192 * Updates the bounds for the scrollbar. 193 */ 194 @Override onUpdateScrollbar(int dy)195 public void onUpdateScrollbar(int dy) { 196 if (mApps == null) { 197 return; 198 } 199 List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems(); 200 201 // Skip early if there are no items or we haven't been measured 202 if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) { 203 mScrollbar.setThumbOffsetY(-1); 204 return; 205 } 206 207 // Skip early if, there no child laid out in the container. 208 int scrollY = computeVerticalScrollOffset(); 209 if (scrollY < 0) { 210 mScrollbar.setThumbOffsetY(-1); 211 return; 212 } 213 214 // Only show the scrollbar if there is height to be scrolled 215 int availableScrollBarHeight = getAvailableScrollBarHeight(); 216 int availableScrollHeight = getAvailableScrollHeight(); 217 if (availableScrollHeight <= 0) { 218 mScrollbar.setThumbOffsetY(-1); 219 return; 220 } 221 222 if (mScrollbar.isThumbDetached()) { 223 if (!mScrollbar.isDraggingThumb()) { 224 // Calculate the current scroll position, the scrollY of the recycler view accounts 225 // for the view padding, while the scrollBarY is drawn right up to the background 226 // padding (ignoring padding) 227 int scrollBarY = (int) 228 (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 229 230 int thumbScrollY = mScrollbar.getThumbOffsetY(); 231 int diffScrollY = scrollBarY - thumbScrollY; 232 if (diffScrollY * dy > 0f) { 233 // User is scrolling in the same direction the thumb needs to catch up to the 234 // current scroll position. We do this by mapping the difference in movement 235 // from the original scroll bar position to the difference in movement necessary 236 // in the detached thumb position to ensure that both speed towards the same 237 // position at either end of the list. 238 if (dy < 0) { 239 int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); 240 thumbScrollY += Math.max(offset, diffScrollY); 241 } else { 242 int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / 243 (float) (availableScrollBarHeight - scrollBarY)); 244 thumbScrollY += Math.min(offset, diffScrollY); 245 } 246 thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); 247 mScrollbar.setThumbOffsetY(thumbScrollY); 248 if (scrollBarY == thumbScrollY) { 249 mScrollbar.reattachThumbToScroll(); 250 } 251 } else { 252 // User is scrolling in an opposite direction to the direction that the thumb 253 // needs to catch up to the scroll position. Do nothing except for updating 254 // the scroll bar x to match the thumb width. 255 mScrollbar.setThumbOffsetY(thumbScrollY); 256 } 257 } 258 } else { 259 synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight); 260 } 261 } 262 263 @Override getScrollBarTop()264 public int getScrollBarTop() { 265 return ActivityContext.lookupContext(getContext()).getAppsView().isSearchSupported() 266 ? getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding) 267 : 0; 268 } 269 270 @Override getScrollBarMarginBottom()271 public int getScrollBarMarginBottom() { 272 return getRootWindowInsets() == null ? 0 273 : getRootWindowInsets().getSystemWindowInsetBottom(); 274 } 275 276 @Override hasOverlappingRendering()277 public boolean hasOverlappingRendering() { 278 return false; 279 } 280 logCumulativeVerticalScroll()281 private void logCumulativeVerticalScroll() { 282 ActivityContext context = ActivityContext.lookupContext(getContext()); 283 StatsLogManager mgr = context.getStatsLogManager(); 284 ActivityAllAppsContainerView<?> appsView = context.getAppsView(); 285 ExtendedEditText editText = appsView.getSearchUiManager().getEditText(); 286 ContainerInfo containerInfo = ContainerInfo.newBuilder().setSearchResultContainer( 287 SearchResultContainer 288 .newBuilder() 289 .setQueryLength((editText == null) ? -1 : editText.length())).build(); 290 if (mCumulativeVerticalScroll == 0) { 291 // mCumulativeVerticalScroll == 0 when user comes back to original position, we 292 // don't know the direction of scrolling. 293 mgr.logger().withContainerInfo(containerInfo).log( 294 LAUNCHER_ALLAPPS_SCROLLED_UNKNOWN_DIRECTION); 295 return; 296 } else if (appsView.isSearching()) { 297 // In search results page 298 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 299 ? LAUNCHER_ALLAPPS_SEARCH_SCROLLED_DOWN 300 : LAUNCHER_ALLAPPS_SEARCH_SCROLLED_UP); 301 return; 302 } else if (appsView.mViewPager != null) { 303 int currentPage = appsView.mViewPager.getCurrentPage(); 304 if (currentPage == ActivityAllAppsContainerView.AdapterHolder.WORK) { 305 // In work A-Z list 306 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 307 ? LAUNCHER_WORK_FAB_BUTTON_COLLAPSE 308 : LAUNCHER_WORK_FAB_BUTTON_EXTEND); 309 return; 310 } 311 } 312 // In personal A-Z list 313 mgr.logger().withContainerInfo(containerInfo).log((mCumulativeVerticalScroll > 0) 314 ? LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_DOWN 315 : LAUNCHER_ALLAPPS_PERSONAL_SCROLLED_UP); 316 } 317 } 318