Home | History | Annotate | Download | only in launcher3
      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 
     17 package com.android.launcher3;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.Rect;
     22 import android.support.v7.widget.RecyclerView;
     23 import android.util.AttributeSet;
     24 import android.view.MotionEvent;
     25 import com.android.launcher3.util.Thunk;
     26 
     27 
     28 /**
     29  * A base {@link RecyclerView}, which does the following:
     30  * <ul>
     31  *   <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
     32  *   <li> Enable fast scroller.
     33  * </ul>
     34  */
     35 public abstract class BaseRecyclerView extends RecyclerView
     36         implements RecyclerView.OnItemTouchListener {
     37 
     38     private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
     39 
     40     /** Keeps the last known scrolling delta/velocity along y-axis. */
     41     @Thunk int mDy = 0;
     42     private float mDeltaThreshold;
     43 
     44     /**
     45      * The current scroll state of the recycler view.  We use this in onUpdateScrollbar()
     46      * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
     47      * that we can calculate what the scroll bar looks like, and where to jump to from the fast
     48      * scroller.
     49      */
     50     public static class ScrollPositionState {
     51         // The index of the first visible row
     52         public int rowIndex;
     53         // The offset of the first visible row
     54         public int rowTopOffset;
     55         // The adapter position of the first visible item
     56         public int itemPos;
     57     }
     58 
     59     protected BaseRecyclerViewFastScrollBar mScrollbar;
     60 
     61     private int mDownX;
     62     private int mDownY;
     63     private int mLastY;
     64     protected Rect mBackgroundPadding = new Rect();
     65 
     66     public BaseRecyclerView(Context context) {
     67         this(context, null);
     68     }
     69 
     70     public BaseRecyclerView(Context context, AttributeSet attrs) {
     71         this(context, attrs, 0);
     72     }
     73 
     74     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
     75         super(context, attrs, defStyleAttr);
     76         mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
     77         mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
     78 
     79         ScrollListener listener = new ScrollListener();
     80         setOnScrollListener(listener);
     81     }
     82 
     83     private class ScrollListener extends OnScrollListener {
     84         public ScrollListener() {
     85             // Do nothing
     86         }
     87 
     88         @Override
     89         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
     90             mDy = dy;
     91 
     92             // TODO(winsonc): If we want to animate the section heads while scrolling, we can
     93             //                initiate that here if the recycler view scroll state is not
     94             //                RecyclerView.SCROLL_STATE_IDLE.
     95 
     96             onUpdateScrollbar(dy);
     97         }
     98     }
     99 
    100     public void reset() {
    101         mScrollbar.reattachThumbToScroll();
    102     }
    103 
    104     @Override
    105     protected void onFinishInflate() {
    106         super.onFinishInflate();
    107         addOnItemTouchListener(this);
    108     }
    109 
    110     /**
    111      * We intercept the touch handling only to support fast scrolling when initiated from the
    112      * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
    113      */
    114     @Override
    115     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
    116         return handleTouchEvent(ev);
    117     }
    118 
    119     @Override
    120     public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
    121         handleTouchEvent(ev);
    122     }
    123 
    124     /**
    125      * Handles the touch event and determines whether to show the fast scroller (or updates it if
    126      * it is already showing).
    127      */
    128     private boolean handleTouchEvent(MotionEvent ev) {
    129         int action = ev.getAction();
    130         int x = (int) ev.getX();
    131         int y = (int) ev.getY();
    132         switch (action) {
    133             case MotionEvent.ACTION_DOWN:
    134                 // Keep track of the down positions
    135                 mDownX = x;
    136                 mDownY = mLastY = y;
    137                 if (shouldStopScroll(ev)) {
    138                     stopScroll();
    139                 }
    140                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    141                 break;
    142             case MotionEvent.ACTION_MOVE:
    143                 mLastY = y;
    144                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    145                 break;
    146             case MotionEvent.ACTION_UP:
    147             case MotionEvent.ACTION_CANCEL:
    148                 onFastScrollCompleted();
    149                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    150                 break;
    151         }
    152         return mScrollbar.isDraggingThumb();
    153     }
    154 
    155     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    156         // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
    157     }
    158 
    159     /**
    160      * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
    161      */
    162     protected boolean shouldStopScroll(MotionEvent ev) {
    163         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    164             if ((Math.abs(mDy) < mDeltaThreshold &&
    165                     getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
    166                 // now the touch events are being passed to the {@link WidgetCell} until the
    167                 // touch sequence goes over the touch slop.
    168                 return true;
    169             }
    170         }
    171         return false;
    172     }
    173 
    174     public void updateBackgroundPadding(Rect padding) {
    175         mBackgroundPadding.set(padding);
    176     }
    177 
    178     public Rect getBackgroundPadding() {
    179         return mBackgroundPadding;
    180     }
    181 
    182     /**
    183      * Returns the scroll bar width when the user is scrolling.
    184      */
    185     public int getMaxScrollbarWidth() {
    186         return mScrollbar.getThumbMaxWidth();
    187     }
    188 
    189     /**
    190      * Returns the visible height of the recycler view:
    191      *   VisibleHeight = View height - top padding - bottom padding
    192      */
    193     protected int getVisibleHeight() {
    194         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
    195         return visibleHeight;
    196     }
    197 
    198     /**
    199      * Returns the available scroll height:
    200      *   AvailableScrollHeight = Total height of the all items - last page height
    201      */
    202     protected int getAvailableScrollHeight(int rowCount) {
    203         int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
    204         int availableScrollHeight = totalHeight - getVisibleHeight();
    205         return availableScrollHeight;
    206     }
    207 
    208     /**
    209      * Returns the available scroll bar height:
    210      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
    211      */
    212     protected int getAvailableScrollBarHeight() {
    213         int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
    214         return availableScrollBarHeight;
    215     }
    216 
    217     /**
    218      * Returns the track color (ignoring alpha), can be overridden by each subclass.
    219      */
    220     public int getFastScrollerTrackColor(int defaultTrackColor) {
    221         return defaultTrackColor;
    222     }
    223 
    224     /**
    225      * Returns the inactive thumb color, can be overridden by each subclass.
    226      */
    227     public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
    228         return defaultInactiveThumbColor;
    229     }
    230 
    231     /**
    232      * Returns the scrollbar for this recycler view.
    233      */
    234     public BaseRecyclerViewFastScrollBar getScrollBar() {
    235         return mScrollbar;
    236     }
    237 
    238     @Override
    239     protected void dispatchDraw(Canvas canvas) {
    240         super.dispatchDraw(canvas);
    241         onUpdateScrollbar(0);
    242         mScrollbar.draw(canvas);
    243     }
    244 
    245     /**
    246      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
    247      * this by mapping the available scroll area of the recycler view to the available space for the
    248      * scroll bar.
    249      *
    250      * @param scrollPosState the current scroll position
    251      * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
    252      *                 all rows are the same height)
    253      */
    254     protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
    255             int rowCount) {
    256         // Only show the scrollbar if there is height to be scrolled
    257         int availableScrollBarHeight = getAvailableScrollBarHeight();
    258         int availableScrollHeight = getAvailableScrollHeight(rowCount);
    259         if (availableScrollHeight <= 0) {
    260             mScrollbar.setThumbOffset(-1, -1);
    261             return;
    262         }
    263 
    264         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
    265         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
    266         // padding)
    267         int scrollY = getScrollTop(scrollPosState);
    268         int scrollBarY = mBackgroundPadding.top +
    269                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
    270 
    271         // Calculate the position and size of the scroll bar
    272         int scrollBarX;
    273         if (Utilities.isRtl(getResources())) {
    274             scrollBarX = mBackgroundPadding.left;
    275         } else {
    276             scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
    277         }
    278         mScrollbar.setThumbOffset(scrollBarX, scrollBarY);
    279     }
    280 
    281     /**
    282      * @return whether fast scrolling is supported in the current state.
    283      */
    284     protected boolean supportsFastScrolling() {
    285         return true;
    286     }
    287 
    288     /**
    289      * Maps the touch (from 0..1) to the adapter position that should be visible.
    290      * <p>Override in each subclass of this base class.
    291      *
    292      * @return the scroll top of this recycler view.
    293      */
    294     protected int getScrollTop(ScrollPositionState scrollPosState) {
    295         return getPaddingTop() + getTop(scrollPosState.rowIndex) -
    296                 scrollPosState.rowTopOffset;
    297     }
    298 
    299     /**
    300      * Returns information about the item that the recycler view is currently scrolled to.
    301      */
    302     protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask);
    303 
    304     /**
    305      * Returns the top (or y position) of the row at the specified index.
    306      */
    307     protected abstract int getTop(int rowIndex);
    308 
    309     /**
    310      * Maps the touch (from 0..1) to the adapter position that should be visible.
    311      * <p>Override in each subclass of this base class.
    312      */
    313     protected abstract String scrollToPositionAtProgress(float touchFraction);
    314 
    315     /**
    316      * Updates the bounds for the scrollbar.
    317      * <p>Override in each subclass of this base class.
    318      */
    319     protected abstract void onUpdateScrollbar(int dy);
    320 
    321     /**
    322      * <p>Override in each subclass of this base class.
    323      */
    324     protected void onFastScrollCompleted() {}
    325 }