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