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 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 
     61     public AllAppsRecyclerView(Context context) {
     62         this(context, null);
     63     }
     64 
     65     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
     66         this(context, attrs, 0);
     67     }
     68 
     69     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
     70         this(context, attrs, defStyleAttr, 0);
     71     }
     72 
     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      */
     85     public void setApps(AlphabeticalAppsList apps, boolean usingTabs) {
     86         mApps = apps;
     87         mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     88     }
     89 
     90     public AlphabeticalAppsList getApps() {
     91         return mApps;
     92     }
     93 
     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      */
    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
    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
    129     protected boolean verifyDrawable(Drawable who) {
    130         return who == mEmptySearchBackground || super.verifyDrawable(who);
    131     }
    132 
    133     @Override
    134     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    135         updateEmptySearchBackgroundBounds();
    136         updatePoolSize();
    137     }
    138 
    139     @Override
    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 
    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
    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
    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
    211     public void onFastScrollCompleted() {
    212         super.onFastScrollCompleted();
    213         mFastScrollHelper.onFastScrollCompleted();
    214     }
    215 
    216     @Override
    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
    228     protected float getBottomFadingEdgeStrength() {
    229         // No bottom fading edge.
    230         return 0;
    231     }
    232 
    233     @Override
    234     protected boolean isPaddingOffsetRequired() {
    235         return true;
    236     }
    237 
    238     @Override
    239     protected int getTopPaddingOffset() {
    240         return -getPaddingTop();
    241     }
    242 
    243     /**
    244      * Updates the bounds for the scrollbar.
    245      */
    246     @Override
    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
    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
    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 
    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