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