• 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 android.content.Context;
19 import android.content.res.Resources;
20 import android.graphics.Canvas;
21 import android.graphics.drawable.Drawable;
22 import android.support.v7.widget.RecyclerView;
23 import android.util.AttributeSet;
24 import android.util.SparseIntArray;
25 import android.view.View;
26 
27 import com.android.launcher3.BaseRecyclerView;
28 import com.android.launcher3.BubbleTextView;
29 import com.android.launcher3.DeviceProfile;
30 import com.android.launcher3.Launcher;
31 import com.android.launcher3.R;
32 import com.android.launcher3.userevent.nano.LauncherLogProto;
33 
34 import java.util.List;
35 
36 /**
37  * A RecyclerView with custom fast scroll support for the all apps view.
38  */
39 public class AllAppsRecyclerView extends BaseRecyclerView {
40 
41     private AlphabeticalAppsList mApps;
42     private AllAppsFastScrollHelper mFastScrollHelper;
43     private int mNumAppsPerRow;
44 
45     // The specific view heights that we use to calculate scroll
46     private SparseIntArray mViewHeights = new SparseIntArray();
47     private SparseIntArray mCachedScrollPositions = new SparseIntArray();
48 
49     // The empty-search result background
50     private AllAppsBackgroundDrawable mEmptySearchBackground;
51     private int mEmptySearchBackgroundTopOffset;
52 
53     private HeaderElevationController mElevationController;
54 
AllAppsRecyclerView(Context context)55     public AllAppsRecyclerView(Context context) {
56         this(context, null);
57     }
58 
AllAppsRecyclerView(Context context, AttributeSet attrs)59     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
60         this(context, attrs, 0);
61     }
62 
AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)63     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
64         this(context, attrs, defStyleAttr, 0);
65     }
66 
AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)67     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
68             int defStyleRes) {
69         super(context, attrs, defStyleAttr);
70         Resources res = getResources();
71         addOnItemTouchListener(this);
72         mScrollbar.setDetachThumbOnFastScroll();
73         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
74                 R.dimen.all_apps_empty_search_bg_top_offset);
75     }
76 
77     /**
78      * Sets the list of apps in this view, used to determine the fastscroll position.
79      */
setApps(AlphabeticalAppsList apps)80     public void setApps(AlphabeticalAppsList apps) {
81         mApps = apps;
82         mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
83     }
84 
setElevationController(HeaderElevationController elevationController)85     public void setElevationController(HeaderElevationController elevationController) {
86         mElevationController = elevationController;
87     }
88 
89     /**
90      * Sets the number of apps per row in this recycler view.
91      */
setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow)92     public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
93         mNumAppsPerRow = numAppsPerRow;
94 
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_SEARCH_DIVIDER, 1);
99         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, 1);
100         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
101         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
102         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, mNumAppsPerRow);
103         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, 1);
104         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SECTION_BREAK, approxRows);
105     }
106 
107     /**
108      * Ensures that we can present a stable scrollbar for views of varying types by pre-measuring
109      * all the different view types.
110      */
preMeasureViews(AllAppsGridAdapter adapter)111     public void preMeasureViews(AllAppsGridAdapter adapter) {
112         final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
113                 getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST);
114         final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
115                 getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST);
116 
117         // Icons
118         BubbleTextView icon = (BubbleTextView) adapter.onCreateViewHolder(this,
119                 AllAppsGridAdapter.VIEW_TYPE_ICON).mContent;
120         int iconHeight = icon.getLayoutParams().height;
121         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight);
122         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight);
123 
124         // Search divider
125         View searchDivider = adapter.onCreateViewHolder(this,
126                 AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER).mContent;
127         searchDivider.measure(widthMeasureSpec, heightMeasureSpec);
128         int searchDividerHeight = searchDivider.getMeasuredHeight();
129         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER, searchDividerHeight);
130 
131         // Generic dividers
132         View divider = adapter.onCreateViewHolder(this,
133                 AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER).mContent;
134         divider.measure(widthMeasureSpec, heightMeasureSpec);
135         int dividerHeight = divider.getMeasuredHeight();
136         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, dividerHeight);
137         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, dividerHeight);
138 
139         // Search views
140         View emptySearch = adapter.onCreateViewHolder(this,
141                 AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH).mContent;
142         emptySearch.measure(widthMeasureSpec, heightMeasureSpec);
143         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH,
144                 emptySearch.getMeasuredHeight());
145         View searchMarket = adapter.onCreateViewHolder(this,
146                 AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET).mContent;
147         searchMarket.measure(widthMeasureSpec, heightMeasureSpec);
148         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET,
149                 searchMarket.getMeasuredHeight());
150 
151         // Section breaks
152         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SECTION_BREAK, 0);
153     }
154 
155     /**
156      * Scrolls this recycler view to the top.
157      */
scrollToTop()158     public void scrollToTop() {
159         // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
160         if (mScrollbar.isThumbDetached()) {
161             mScrollbar.reattachThumbToScroll();
162         }
163         scrollToPosition(0);
164         if (mElevationController != null) {
165             mElevationController.reset();
166         }
167     }
168 
169     /**
170      * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
171      * background bounds.
172      */
173     @Override
dispatchDraw(Canvas canvas)174     protected void dispatchDraw(Canvas canvas) {
175         // Clip to ensure that we don't draw the overscroll effect beyond the background bounds
176         canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
177                 getWidth() - mBackgroundPadding.right,
178                 getHeight() - mBackgroundPadding.bottom);
179         super.dispatchDraw(canvas);
180     }
181 
182     @Override
onDraw(Canvas c)183     public void onDraw(Canvas c) {
184         // Draw the background
185         if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
186             c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
187                     getWidth() - mBackgroundPadding.right,
188                     getHeight() - mBackgroundPadding.bottom);
189 
190             mEmptySearchBackground.draw(c);
191         }
192 
193         super.onDraw(c);
194     }
195 
196     @Override
verifyDrawable(Drawable who)197     protected boolean verifyDrawable(Drawable who) {
198         return who == mEmptySearchBackground || super.verifyDrawable(who);
199     }
200 
201     @Override
onSizeChanged(int w, int h, int oldw, int oldh)202     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
203         updateEmptySearchBackgroundBounds();
204     }
205 
getContainerType(View v)206     public int getContainerType(View v) {
207         if (mApps.hasFilter()) {
208             return LauncherLogProto.SEARCHRESULT;
209         } else {
210             if (v instanceof BubbleTextView) {
211                 BubbleTextView icon = (BubbleTextView) v;
212                 int position = getChildPosition(icon);
213                 if (position != NO_POSITION) {
214                     List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
215                     AlphabeticalAppsList.AdapterItem item = items.get(position);
216                     if (item.viewType == AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON) {
217                         return LauncherLogProto.PREDICTION;
218                     }
219                 }
220             }
221             return LauncherLogProto.ALLAPPS;
222         }
223     }
224 
onSearchResultsChanged()225     public void onSearchResultsChanged() {
226         // Always scroll the view to the top so the user can see the changed results
227         scrollToTop();
228 
229         if (mApps.hasNoFilteredResults()) {
230             if (mEmptySearchBackground == null) {
231                 mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext());
232                 mEmptySearchBackground.setAlpha(0);
233                 mEmptySearchBackground.setCallback(this);
234                 updateEmptySearchBackgroundBounds();
235             }
236             mEmptySearchBackground.animateBgAlpha(1f, 150);
237         } else if (mEmptySearchBackground != null) {
238             // For the time being, we just immediately hide the background to ensure that it does
239             // not overlap with the results
240             mEmptySearchBackground.setBgAlpha(0f);
241         }
242     }
243 
244     /**
245      * Maps the touch (from 0..1) to the adapter position that should be visible.
246      */
247     @Override
scrollToPositionAtProgress(float touchFraction)248     public String scrollToPositionAtProgress(float touchFraction) {
249         int rowCount = mApps.getNumAppRows();
250         if (rowCount == 0) {
251             return "";
252         }
253 
254         // Stop the scroller if it is scrolling
255         stopScroll();
256 
257         // Find the fastscroll section that maps to this touch fraction
258         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
259                 mApps.getFastScrollerSections();
260         AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
261         for (int i = 1; i < fastScrollSections.size(); i++) {
262             AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
263             if (info.touchFraction > touchFraction) {
264                 break;
265             }
266             lastInfo = info;
267         }
268 
269         // Update the fast scroll
270         int scrollY = getCurrentScrollY();
271         int availableScrollHeight = getAvailableScrollHeight();
272         mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
273         return lastInfo.sectionName;
274     }
275 
276     @Override
onFastScrollCompleted()277     public void onFastScrollCompleted() {
278         super.onFastScrollCompleted();
279         mFastScrollHelper.onFastScrollCompleted();
280     }
281 
282     @Override
setAdapter(Adapter adapter)283     public void setAdapter(Adapter adapter) {
284         super.setAdapter(adapter);
285         adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
286             public void onChanged() {
287                 mCachedScrollPositions.clear();
288             }
289         });
290         mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
291     }
292 
293     /**
294      * Updates the bounds for the scrollbar.
295      */
296     @Override
onUpdateScrollbar(int dy)297     public void onUpdateScrollbar(int dy) {
298         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
299 
300         // Skip early if there are no items or we haven't been measured
301         if (items.isEmpty() || mNumAppsPerRow == 0) {
302             mScrollbar.setThumbOffset(-1, -1);
303             return;
304         }
305 
306         // Skip early if, there no child laid out in the container.
307         int scrollY = getCurrentScrollY();
308         if (scrollY < 0) {
309             mScrollbar.setThumbOffset(-1, -1);
310             return;
311         }
312 
313         // Only show the scrollbar if there is height to be scrolled
314         int availableScrollBarHeight = getAvailableScrollBarHeight();
315         int availableScrollHeight = getAvailableScrollHeight();
316         if (availableScrollHeight <= 0) {
317             mScrollbar.setThumbOffset(-1, -1);
318             return;
319         }
320 
321         if (mScrollbar.isThumbDetached()) {
322             if (!mScrollbar.isDraggingThumb()) {
323                 // Calculate the current scroll position, the scrollY of the recycler view accounts
324                 // for the view padding, while the scrollBarY is drawn right up to the background
325                 // padding (ignoring padding)
326                 int scrollBarX = getScrollBarX();
327                 int scrollBarY = mBackgroundPadding.top +
328                         (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
329 
330                 int thumbScrollY = mScrollbar.getThumbOffset().y;
331                 int diffScrollY = scrollBarY - thumbScrollY;
332                 if (diffScrollY * dy > 0f) {
333                     // User is scrolling in the same direction the thumb needs to catch up to the
334                     // current scroll position.  We do this by mapping the difference in movement
335                     // from the original scroll bar position to the difference in movement necessary
336                     // in the detached thumb position to ensure that both speed towards the same
337                     // position at either end of the list.
338                     if (dy < 0) {
339                         int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY);
340                         thumbScrollY += Math.max(offset, diffScrollY);
341                     } else {
342                         int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) /
343                                 (float) (availableScrollBarHeight - scrollBarY));
344                         thumbScrollY += Math.min(offset, diffScrollY);
345                     }
346                     thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
347                     mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
348                     if (scrollBarY == thumbScrollY) {
349                         mScrollbar.reattachThumbToScroll();
350                     }
351                 } else {
352                     // User is scrolling in an opposite direction to the direction that the thumb
353                     // needs to catch up to the scroll position.  Do nothing except for updating
354                     // the scroll bar x to match the thumb width.
355                     mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
356                 }
357             }
358         } else {
359             synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight);
360         }
361     }
362 
363     @Override
supportsFastScrolling()364     protected boolean supportsFastScrolling() {
365         // Only allow fast scrolling when the user is not searching, since the results are not
366         // grouped in a meaningful order
367         return !mApps.hasFilter();
368     }
369 
370     @Override
getCurrentScrollY()371     public int getCurrentScrollY() {
372         // Return early if there are no items or we haven't been measured
373         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
374         if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
375             return -1;
376         }
377 
378         // Calculate the y and offset for the item
379         View child = getChildAt(0);
380         int position = getChildPosition(child);
381         if (position == NO_POSITION) {
382             return -1;
383         }
384         return getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child));
385     }
386 
getCurrentScrollY(int position, int offset)387     public int getCurrentScrollY(int position, int offset) {
388         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
389         AlphabeticalAppsList.AdapterItem posItem = position < items.size() ?
390                 items.get(position) : null;
391         int y = mCachedScrollPositions.get(position, -1);
392         if (y < 0) {
393             y = 0;
394             for (int i = 0; i < position; i++) {
395                 AlphabeticalAppsList.AdapterItem item = items.get(i);
396                 if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
397                     // Break once we reach the desired row
398                     if (posItem != null && posItem.viewType == item.viewType &&
399                             posItem.rowIndex == item.rowIndex) {
400                         break;
401                     }
402                     // Otherwise, only account for the first icon in the row since they are the same
403                     // size within a row
404                     if (item.rowAppIndex == 0) {
405                         y += mViewHeights.get(item.viewType, 0);
406                     }
407                 } else {
408                     // Rest of the views span the full width
409                     y += mViewHeights.get(item.viewType, 0);
410                 }
411             }
412             mCachedScrollPositions.put(position, y);
413         }
414 
415         return getPaddingTop() + y - offset;
416     }
417 
418     @Override
419     protected int getVisibleHeight() {
420         return super.getVisibleHeight()
421                 - Launcher.getLauncher(getContext()).getDragLayer().getInsets().bottom;
422     }
423 
424     /**
425      * Returns the available scroll height:
426      *   AvailableScrollHeight = Total height of the all items - last page height
427      */
428     @Override
429     protected int getAvailableScrollHeight() {
430         int paddedHeight = getCurrentScrollY(mApps.getAdapterItems().size(), 0);
431         int totalHeight = paddedHeight + getPaddingBottom();
432         return totalHeight - getVisibleHeight();
433     }
434 
435     /**
436      * Updates the bounds of the empty search background.
437      */
438     private void updateEmptySearchBackgroundBounds() {
439         if (mEmptySearchBackground == null) {
440             return;
441         }
442 
443         // Center the empty search background on this new view bounds
444         int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2;
445         int y = mEmptySearchBackgroundTopOffset;
446         mEmptySearchBackground.setBounds(x, y,
447                 x + mEmptySearchBackground.getIntrinsicWidth(),
448                 y + mEmptySearchBackground.getIntrinsicHeight());
449     }
450 }
451