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