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.graphics.Canvas;
     20 import android.os.Bundle;
     21 import android.support.v7.widget.LinearLayoutManager;
     22 import android.support.v7.widget.RecyclerView;
     23 import android.util.AttributeSet;
     24 import android.view.View;
     25 
     26 import com.android.launcher3.BaseRecyclerView;
     27 import com.android.launcher3.BaseRecyclerViewFastScrollBar;
     28 import com.android.launcher3.DeviceProfile;
     29 import com.android.launcher3.Stats;
     30 import com.android.launcher3.Utilities;
     31 import com.android.launcher3.util.Thunk;
     32 
     33 import java.util.List;
     34 
     35 /**
     36  * A RecyclerView with custom fast scroll support for the all apps view.
     37  */
     38 public class AllAppsRecyclerView extends BaseRecyclerView
     39         implements Stats.LaunchSourceProvider {
     40 
     41     private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
     42     private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
     43 
     44     private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
     45     private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
     46 
     47     private AlphabeticalAppsList mApps;
     48     private int mNumAppsPerRow;
     49 
     50     @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
     51     @Thunk int mPrevFastScrollFocusedPosition;
     52     @Thunk int mFastScrollFrameIndex;
     53     @Thunk final int[] mFastScrollFrames = new int[10];
     54 
     55     private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
     56     private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
     57 
     58     private ScrollPositionState mScrollPosState = new ScrollPositionState();
     59 
     60     public AllAppsRecyclerView(Context context) {
     61         this(context, null);
     62     }
     63 
     64     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
     65         this(context, attrs, 0);
     66     }
     67 
     68     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
     69         this(context, attrs, defStyleAttr, 0);
     70     }
     71 
     72     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
     73             int defStyleRes) {
     74         super(context, attrs, defStyleAttr);
     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     }
     83 
     84     /**
     85      * Sets the number of apps per row in this recycler view.
     86      */
     87     public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
     88         mNumAppsPerRow = numAppsPerRow;
     89 
     90         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
     91         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
     92         pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
     93         pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
     94         pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow);
     95         pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
     96     }
     97 
     98     /**
     99      * Scrolls this recycler view to the top.
    100      */
    101     public void scrollToTop() {
    102         scrollToPosition(0);
    103     }
    104 
    105     /**
    106      * We need to override the draw to ensure that we don't draw the overscroll effect beyond the
    107      * background bounds.
    108      */
    109     @Override
    110     protected void dispatchDraw(Canvas canvas) {
    111         canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
    112                 getWidth() - mBackgroundPadding.right,
    113                 getHeight() - mBackgroundPadding.bottom);
    114         super.dispatchDraw(canvas);
    115     }
    116 
    117     @Override
    118     protected void onFinishInflate() {
    119         super.onFinishInflate();
    120 
    121         // Bind event handlers
    122         addOnItemTouchListener(this);
    123     }
    124 
    125     @Override
    126     public void fillInLaunchSourceData(Bundle sourceData) {
    127         sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
    128         if (mApps.hasFilter()) {
    129             sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
    130                     Stats.SUB_CONTAINER_ALL_APPS_SEARCH);
    131         } else {
    132             sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
    133                     Stats.SUB_CONTAINER_ALL_APPS_A_Z);
    134         }
    135     }
    136 
    137     /**
    138      * Maps the touch (from 0..1) to the adapter position that should be visible.
    139      */
    140     @Override
    141     public String scrollToPositionAtProgress(float touchFraction) {
    142         int rowCount = mApps.getNumAppRows();
    143         if (rowCount == 0) {
    144             return "";
    145         }
    146 
    147         // Stop the scroller if it is scrolling
    148         stopScroll();
    149 
    150         // Find the fastscroll section that maps to this touch fraction
    151         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
    152                 mApps.getFastScrollerSections();
    153         AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
    154         if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
    155             for (int i = 1; i < fastScrollSections.size(); i++) {
    156                 AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
    157                 if (info.touchFraction > touchFraction) {
    158                     break;
    159                 }
    160                 lastInfo = info;
    161             }
    162         } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
    163             lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
    164         } else {
    165             throw new RuntimeException("Unexpected scroll bar mode");
    166         }
    167 
    168         // Map the touch position back to the scroll of the recycler view
    169         getCurScrollState(mScrollPosState, mApps.getAdapterItems());
    170         int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0);
    171         LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
    172         if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
    173             layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
    174         }
    175 
    176         if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
    177             mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
    178 
    179             // Reset the last focused view
    180             if (mLastFastScrollFocusedView != null) {
    181                 mLastFastScrollFocusedView.setFastScrollFocused(false, true);
    182                 mLastFastScrollFocusedView = null;
    183             }
    184 
    185             if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
    186                 smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
    187             } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
    188                 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
    189                 if (vh != null &&
    190                         vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
    191                     mLastFastScrollFocusedView =
    192                             (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
    193                     mLastFastScrollFocusedView.setFastScrollFocused(true, true);
    194                 }
    195             } else {
    196                 throw new RuntimeException("Unexpected fast scroll mode");
    197             }
    198         }
    199         return lastInfo.sectionName;
    200     }
    201 
    202     @Override
    203     public void onFastScrollCompleted() {
    204         super.onFastScrollCompleted();
    205         // Reset and clean up the last focused view
    206         if (mLastFastScrollFocusedView != null) {
    207             mLastFastScrollFocusedView.setFastScrollFocused(false, true);
    208             mLastFastScrollFocusedView = null;
    209         }
    210         mPrevFastScrollFocusedPosition = -1;
    211     }
    212 
    213     /**
    214      * Updates the bounds for the scrollbar.
    215      */
    216     @Override
    217     public void onUpdateScrollbar() {
    218         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
    219 
    220         // Skip early if there are no items or we haven't been measured
    221         if (items.isEmpty() || mNumAppsPerRow == 0) {
    222             mScrollbar.setScrollbarThumbOffset(-1, -1);
    223             return;
    224         }
    225 
    226         // Find the index and height of the first visible row (all rows have the same height)
    227         int rowCount = mApps.getNumAppRows();
    228         getCurScrollState(mScrollPosState, items);
    229         if (mScrollPosState.rowIndex < 0) {
    230             mScrollbar.setScrollbarThumbOffset(-1, -1);
    231             return;
    232         }
    233 
    234         synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0);
    235     }
    236 
    237     /**
    238      * This runnable runs a single frame of the smooth scroll animation and posts the next frame
    239      * if necessary.
    240      */
    241     @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
    242         @Override
    243         public void run() {
    244             if (mFastScrollFrameIndex < mFastScrollFrames.length) {
    245                 scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
    246                 mFastScrollFrameIndex++;
    247                 postOnAnimation(mSmoothSnapNextFrameRunnable);
    248             } else {
    249                 // Animation completed, set the fast scroll state on the target view
    250                 final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
    251                 if (vh != null &&
    252                         vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
    253                         mLastFastScrollFocusedView != vh.itemView) {
    254                     mLastFastScrollFocusedView =
    255                             (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
    256                     mLastFastScrollFocusedView.setFastScrollFocused(true, true);
    257                 }
    258             }
    259         }
    260     };
    261 
    262     /**
    263      * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
    264      * ourselves and animating the scroll on the recycler view.
    265      */
    266     private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
    267         removeCallbacks(mSmoothSnapNextFrameRunnable);
    268 
    269         // Calculate the full animation from the current scroll position to the final scroll
    270         // position, and then run the animation for the duration.
    271         int curScrollY = getPaddingTop() +
    272                 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
    273         int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
    274         int numFrames = mFastScrollFrames.length;
    275         for (int i = 0; i < numFrames; i++) {
    276             // TODO(winsonc): We can interpolate this as well.
    277             mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
    278         }
    279         mFastScrollFrameIndex = 0;
    280         postOnAnimation(mSmoothSnapNextFrameRunnable);
    281     }
    282 
    283     /**
    284      * Returns the current scroll state of the apps rows.
    285      */
    286     private void getCurScrollState(ScrollPositionState stateOut,
    287             List<AlphabeticalAppsList.AdapterItem> items) {
    288         stateOut.rowIndex = -1;
    289         stateOut.rowTopOffset = -1;
    290         stateOut.rowHeight = -1;
    291 
    292         // Return early if there are no items or we haven't been measured
    293         if (items.isEmpty() || mNumAppsPerRow == 0) {
    294             return;
    295         }
    296 
    297         int childCount = getChildCount();
    298         for (int i = 0; i < childCount; i++) {
    299             View child = getChildAt(i);
    300             int position = getChildPosition(child);
    301             if (position != NO_POSITION) {
    302                 AlphabeticalAppsList.AdapterItem item = items.get(position);
    303                 if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
    304                         item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
    305                     stateOut.rowIndex = item.rowIndex;
    306                     stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
    307                     stateOut.rowHeight = child.getHeight();
    308                     break;
    309                 }
    310             }
    311         }
    312     }
    313 
    314     /**
    315      * Returns the scrollY for the given position in the adapter.
    316      */
    317     private int getScrollAtPosition(int position, int rowHeight) {
    318         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
    319         if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
    320                 item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
    321             int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
    322             return offset + item.rowIndex * rowHeight;
    323         } else {
    324             return 0;
    325         }
    326     }
    327 }
    328