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