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.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 
     55     public AllAppsRecyclerView(Context context) {
     56         this(context, null);
     57     }
     58 
     59     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
     60         this(context, attrs, 0);
     61     }
     62 
     63     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
     64         this(context, attrs, defStyleAttr, 0);
     65     }
     66 
     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      */
     80     public void setApps(AlphabeticalAppsList apps) {
     81         mApps = apps;
     82         mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     83     }
     84 
     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      */
     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      */
    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      */
    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
    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
    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
    197     protected boolean verifyDrawable(Drawable who) {
    198         return who == mEmptySearchBackground || super.verifyDrawable(who);
    199     }
    200 
    201     @Override
    202     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    203         updateEmptySearchBackgroundBounds();
    204     }
    205 
    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 
    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
    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
    277     public void onFastScrollCompleted() {
    278         super.onFastScrollCompleted();
    279         mFastScrollHelper.onFastScrollCompleted();
    280     }
    281 
    282     @Override
    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
    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
    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
    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 
    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