• 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.animation.ObjectAnimator;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.drawable.Drawable;
23 import android.os.Bundle;
24 import android.support.v7.widget.LinearLayoutManager;
25 import android.support.v7.widget.RecyclerView;
26 import android.util.AttributeSet;
27 import android.view.View;
28 
29 import com.android.launcher3.BaseRecyclerView;
30 import com.android.launcher3.BaseRecyclerViewFastScrollBar;
31 import com.android.launcher3.DeviceProfile;
32 import com.android.launcher3.R;
33 import com.android.launcher3.Stats;
34 import com.android.launcher3.Utilities;
35 import com.android.launcher3.util.Thunk;
36 
37 import java.util.List;
38 
39 /**
40  * A RecyclerView with custom fast scroll support for the all apps view.
41  */
42 public class AllAppsRecyclerView extends BaseRecyclerView
43         implements Stats.LaunchSourceProvider {
44 
45     private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
46     private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
47 
48     private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
49     private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
50 
51     private AlphabeticalAppsList mApps;
52     private int mNumAppsPerRow;
53 
54     @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
55     @Thunk int mPrevFastScrollFocusedPosition;
56     @Thunk int mFastScrollFrameIndex;
57     @Thunk final int[] mFastScrollFrames = new int[10];
58 
59     private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
60     private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
61 
62     private ScrollPositionState mScrollPosState = new ScrollPositionState();
63 
64     private AllAppsBackgroundDrawable mEmptySearchBackground;
65     private int mEmptySearchBackgroundTopOffset;
66 
AllAppsRecyclerView(Context context)67     public AllAppsRecyclerView(Context context) {
68         this(context, null);
69     }
70 
AllAppsRecyclerView(Context context, AttributeSet attrs)71     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
72         this(context, attrs, 0);
73     }
74 
AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)75     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
76         this(context, attrs, defStyleAttr, 0);
77     }
78 
AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)79     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
80             int defStyleRes) {
81         super(context, attrs, defStyleAttr);
82 
83         Resources res = getResources();
84         mScrollbar.setDetachThumbOnFastScroll();
85         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
86                 R.dimen.all_apps_empty_search_bg_top_offset);
87     }
88 
89     /**
90      * Sets the list of apps in this view, used to determine the fastscroll position.
91      */
setApps(AlphabeticalAppsList apps)92     public void setApps(AlphabeticalAppsList apps) {
93         mApps = apps;
94     }
95 
96     /**
97      * Sets the number of apps per row in this recycler view.
98      */
setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow)99     public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
100         mNumAppsPerRow = numAppsPerRow;
101 
102         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
103         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
104         pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
105         pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1);
106         pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1);
107         pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
108         pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow);
109         pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
110     }
111 
112     /**
113      * Scrolls this recycler view to the top.
114      */
scrollToTop()115     public void scrollToTop() {
116         // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
117         if (mScrollbar.isThumbDetached()) {
118             mScrollbar.reattachThumbToScroll();
119         }
120         scrollToPosition(0);
121     }
122 
123     /**
124      * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
125      * background bounds.
126      */
127     @Override
dispatchDraw(Canvas canvas)128     protected void dispatchDraw(Canvas canvas) {
129         canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
130                 getWidth() - mBackgroundPadding.right,
131                 getHeight() - mBackgroundPadding.bottom);
132         super.dispatchDraw(canvas);
133     }
134 
135     @Override
onDraw(Canvas c)136     public void onDraw(Canvas c) {
137         // Draw the background
138         if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
139             c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
140                     getWidth() - mBackgroundPadding.right,
141                     getHeight() - mBackgroundPadding.bottom);
142 
143             mEmptySearchBackground.draw(c);
144         }
145 
146         super.onDraw(c);
147     }
148 
149     @Override
verifyDrawable(Drawable who)150     protected boolean verifyDrawable(Drawable who) {
151         return who == mEmptySearchBackground || super.verifyDrawable(who);
152     }
153 
154     @Override
onSizeChanged(int w, int h, int oldw, int oldh)155     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
156         updateEmptySearchBackgroundBounds();
157     }
158 
159     @Override
onFinishInflate()160     protected void onFinishInflate() {
161         super.onFinishInflate();
162 
163         // Bind event handlers
164         addOnItemTouchListener(this);
165     }
166 
167     @Override
fillInLaunchSourceData(Bundle sourceData)168     public void fillInLaunchSourceData(Bundle sourceData) {
169         sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
170         if (mApps.hasFilter()) {
171             sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
172                     Stats.SUB_CONTAINER_ALL_APPS_SEARCH);
173         } else {
174             sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
175                     Stats.SUB_CONTAINER_ALL_APPS_A_Z);
176         }
177     }
178 
onSearchResultsChanged()179     public void onSearchResultsChanged() {
180         // Always scroll the view to the top so the user can see the changed results
181         scrollToTop();
182 
183         if (mApps.hasNoFilteredResults()) {
184             if (mEmptySearchBackground == null) {
185                 mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext());
186                 mEmptySearchBackground.setAlpha(0);
187                 mEmptySearchBackground.setCallback(this);
188                 updateEmptySearchBackgroundBounds();
189             }
190             mEmptySearchBackground.animateBgAlpha(1f, 150);
191         } else if (mEmptySearchBackground != null) {
192             // For the time being, we just immediately hide the background to ensure that it does
193             // not overlap with the results
194             mEmptySearchBackground.setBgAlpha(0f);
195         }
196     }
197 
198     /**
199      * Maps the touch (from 0..1) to the adapter position that should be visible.
200      */
201     @Override
scrollToPositionAtProgress(float touchFraction)202     public String scrollToPositionAtProgress(float touchFraction) {
203         int rowCount = mApps.getNumAppRows();
204         if (rowCount == 0) {
205             return "";
206         }
207 
208         // Stop the scroller if it is scrolling
209         stopScroll();
210 
211         // Find the fastscroll section that maps to this touch fraction
212         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
213                 mApps.getFastScrollerSections();
214         AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
215         if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
216             for (int i = 1; i < fastScrollSections.size(); i++) {
217                 AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
218                 if (info.touchFraction > touchFraction) {
219                     break;
220                 }
221                 lastInfo = info;
222             }
223         } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
224             lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
225         } else {
226             throw new RuntimeException("Unexpected scroll bar mode");
227         }
228 
229         // Map the touch position back to the scroll of the recycler view
230         getCurScrollState(mScrollPosState);
231         int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
232         LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
233         if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
234             layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
235         }
236 
237         if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
238             mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
239 
240             // Reset the last focused view
241             if (mLastFastScrollFocusedView != null) {
242                 mLastFastScrollFocusedView.setFastScrollFocused(false, true);
243                 mLastFastScrollFocusedView = null;
244             }
245 
246             if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
247                 smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
248             } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
249                 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
250                 if (vh != null &&
251                         vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
252                     mLastFastScrollFocusedView =
253                             (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
254                     mLastFastScrollFocusedView.setFastScrollFocused(true, true);
255                 }
256             } else {
257                 throw new RuntimeException("Unexpected fast scroll mode");
258             }
259         }
260         return lastInfo.sectionName;
261     }
262 
263     @Override
onFastScrollCompleted()264     public void onFastScrollCompleted() {
265         super.onFastScrollCompleted();
266         // Reset and clean up the last focused view
267         if (mLastFastScrollFocusedView != null) {
268             mLastFastScrollFocusedView.setFastScrollFocused(false, true);
269             mLastFastScrollFocusedView = null;
270         }
271         mPrevFastScrollFocusedPosition = -1;
272     }
273 
274     /**
275      * Updates the bounds for the scrollbar.
276      */
277     @Override
onUpdateScrollbar(int dy)278     public void onUpdateScrollbar(int dy) {
279         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
280 
281         // Skip early if there are no items or we haven't been measured
282         if (items.isEmpty() || mNumAppsPerRow == 0) {
283             mScrollbar.setThumbOffset(-1, -1);
284             return;
285         }
286 
287         // Find the index and height of the first visible row (all rows have the same height)
288         int rowCount = mApps.getNumAppRows();
289         getCurScrollState(mScrollPosState);
290         if (mScrollPosState.rowIndex < 0) {
291             mScrollbar.setThumbOffset(-1, -1);
292             return;
293         }
294 
295         // Only show the scrollbar if there is height to be scrolled
296         int availableScrollBarHeight = getAvailableScrollBarHeight();
297         int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
298         if (availableScrollHeight <= 0) {
299             mScrollbar.setThumbOffset(-1, -1);
300             return;
301         }
302 
303         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
304         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
305         // padding)
306         int scrollY = getPaddingTop() +
307                 (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
308         int scrollBarY = mBackgroundPadding.top +
309                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
310 
311         if (mScrollbar.isThumbDetached()) {
312             int scrollBarX;
313             if (Utilities.isRtl(getResources())) {
314                 scrollBarX = mBackgroundPadding.left;
315             } else {
316                 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
317             }
318 
319             if (mScrollbar.isDraggingThumb()) {
320                 // If the thumb is detached, then just update the thumb to the current
321                 // touch position
322                 mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY());
323             } else {
324                 int thumbScrollY = mScrollbar.getThumbOffset().y;
325                 int diffScrollY = scrollBarY - thumbScrollY;
326                 if (diffScrollY * dy > 0f) {
327                     // User is scrolling in the same direction the thumb needs to catch up to the
328                     // current scroll position.  We do this by mapping the difference in movement
329                     // from the original scroll bar position to the difference in movement necessary
330                     // in the detached thumb position to ensure that both speed towards the same
331                     // position at either end of the list.
332                     if (dy < 0) {
333                         int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY);
334                         thumbScrollY += Math.max(offset, diffScrollY);
335                     } else {
336                         int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) /
337                                 (float) (availableScrollBarHeight - scrollBarY));
338                         thumbScrollY += Math.min(offset, diffScrollY);
339                     }
340                     thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
341                     mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
342                     if (scrollBarY == thumbScrollY) {
343                         mScrollbar.reattachThumbToScroll();
344                     }
345                 } else {
346                     // User is scrolling in an opposite direction to the direction that the thumb
347                     // needs to catch up to the scroll position.  Do nothing except for updating
348                     // the scroll bar x to match the thumb width.
349                     mScrollbar.setThumbOffset(scrollBarX, thumbScrollY);
350                 }
351             }
352         } else {
353             synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
354         }
355     }
356 
357     /**
358      * This runnable runs a single frame of the smooth scroll animation and posts the next frame
359      * if necessary.
360      */
361     @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
362         @Override
363         public void run() {
364             if (mFastScrollFrameIndex < mFastScrollFrames.length) {
365                 scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
366                 mFastScrollFrameIndex++;
367                 postOnAnimation(mSmoothSnapNextFrameRunnable);
368             } else {
369                 // Animation completed, set the fast scroll state on the target view
370                 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
371                 if (vh != null &&
372                         vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
373                         mLastFastScrollFocusedView != vh.itemView) {
374                     mLastFastScrollFocusedView =
375                             (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
376                     mLastFastScrollFocusedView.setFastScrollFocused(true, true);
377                 }
378             }
379         }
380     };
381 
382     /**
383      * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
384      * ourselves and animating the scroll on the recycler view.
385      */
smoothSnapToPosition(final int position, ScrollPositionState scrollPosState)386     private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
387         removeCallbacks(mSmoothSnapNextFrameRunnable);
388 
389         // Calculate the full animation from the current scroll position to the final scroll
390         // position, and then run the animation for the duration.
391         int curScrollY = getPaddingTop() +
392                 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
393         int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
394         int numFrames = mFastScrollFrames.length;
395         for (int i = 0; i < numFrames; i++) {
396             // TODO(winsonc): We can interpolate this as well.
397             mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
398         }
399         mFastScrollFrameIndex = 0;
400         postOnAnimation(mSmoothSnapNextFrameRunnable);
401     }
402 
403     /**
404      * Returns the current scroll state of the apps rows.
405      */
getCurScrollState(ScrollPositionState stateOut)406     protected void getCurScrollState(ScrollPositionState stateOut) {
407         stateOut.rowIndex = -1;
408         stateOut.rowTopOffset = -1;
409         stateOut.rowHeight = -1;
410 
411         // Return early if there are no items or we haven't been measured
412         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
413         if (items.isEmpty() || mNumAppsPerRow == 0) {
414             return;
415         }
416 
417         int childCount = getChildCount();
418         for (int i = 0; i < childCount; i++) {
419             View child = getChildAt(i);
420             int position = getChildPosition(child);
421             if (position != NO_POSITION) {
422                 AlphabeticalAppsList.AdapterItem item = items.get(position);
423                 if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
424                         item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
425                     stateOut.rowIndex = item.rowIndex;
426                     stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
427                     stateOut.rowHeight = child.getHeight();
428                     break;
429                 }
430             }
431         }
432     }
433 
434     /**
435      * Returns the scrollY for the given position in the adapter.
436      */
getScrollAtPosition(int position, int rowHeight)437     private int getScrollAtPosition(int position, int rowHeight) {
438         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
439         if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
440                 item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
441             int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
442             return offset + item.rowIndex * rowHeight;
443         } else {
444             return 0;
445         }
446     }
447 
448     /**
449      * Updates the bounds of the empty search background.
450      */
updateEmptySearchBackgroundBounds()451     private void updateEmptySearchBackgroundBounds() {
452         if (mEmptySearchBackground == null) {
453             return;
454         }
455 
456         // Center the empty search background on this new view bounds
457         int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2;
458         int y = mEmptySearchBackgroundTopOffset;
459         mEmptySearchBackground.setBounds(x, y,
460                 x + mEmptySearchBackground.getIntrinsicWidth(),
461                 y + mEmptySearchBackground.getIntrinsicHeight());
462     }
463 }
464