• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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