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 height of a given row (they are currently all the same height)
     56         public int rowHeight;
     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     }
     97 
     98     @Override
     99     protected void onFinishInflate() {
    100         super.onFinishInflate();
    101         addOnItemTouchListener(this);
    102     }
    103 
    104     /**
    105      * We intercept the touch handling only to support fast scrolling when initiated from the
    106      * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
    107      */
    108     @Override
    109     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
    110         return handleTouchEvent(ev);
    111     }
    112 
    113     @Override
    114     public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
    115         handleTouchEvent(ev);
    116     }
    117 
    118     /**
    119      * Handles the touch event and determines whether to show the fast scroller (or updates it if
    120      * it is already showing).
    121      */
    122     private boolean handleTouchEvent(MotionEvent ev) {
    123         int action = ev.getAction();
    124         int x = (int) ev.getX();
    125         int y = (int) ev.getY();
    126         switch (action) {
    127             case MotionEvent.ACTION_DOWN:
    128                 // Keep track of the down positions
    129                 mDownX = x;
    130                 mDownY = mLastY = y;
    131                 if (shouldStopScroll(ev)) {
    132                     stopScroll();
    133                 }
    134                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    135                 break;
    136             case MotionEvent.ACTION_MOVE:
    137                 mLastY = y;
    138                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    139                 break;
    140             case MotionEvent.ACTION_UP:
    141             case MotionEvent.ACTION_CANCEL:
    142                 onFastScrollCompleted();
    143                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    144                 break;
    145         }
    146         return mScrollbar.isDragging();
    147     }
    148 
    149     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    150         // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
    151     }
    152 
    153     /**
    154      * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
    155      */
    156     protected boolean shouldStopScroll(MotionEvent ev) {
    157         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    158             if ((Math.abs(mDy) < mDeltaThreshold &&
    159                     getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
    160                 // now the touch events are being passed to the {@link WidgetCell} until the
    161                 // touch sequence goes over the touch slop.
    162                 return true;
    163             }
    164         }
    165         return false;
    166     }
    167 
    168     public void updateBackgroundPadding(Rect padding) {
    169         mBackgroundPadding.set(padding);
    170     }
    171 
    172     public Rect getBackgroundPadding() {
    173         return mBackgroundPadding;
    174     }
    175 
    176     /**
    177      * Returns the scroll bar width when the user is scrolling.
    178      */
    179     public int getMaxScrollbarWidth() {
    180         return mScrollbar.getThumbMaxWidth();
    181     }
    182 
    183     /**
    184      * Returns the available scroll height:
    185      *   AvailableScrollHeight = Total height of the all items - last page height
    186      *
    187      * This assumes that all rows are the same height.
    188      *
    189      * @param yOffset the offset from the top of the recycler view to start tracking.
    190      */
    191     protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) {
    192         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
    193         int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom();
    194         int availableScrollHeight = scrollHeight - visibleHeight;
    195         return availableScrollHeight;
    196     }
    197 
    198     /**
    199      * Returns the available scroll bar height:
    200      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
    201      */
    202     protected int getAvailableScrollBarHeight() {
    203         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
    204         int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
    205         return availableScrollBarHeight;
    206     }
    207 
    208     /**
    209      * Returns the track color (ignoring alpha), can be overridden by each subclass.
    210      */
    211     public int getFastScrollerTrackColor(int defaultTrackColor) {
    212         return defaultTrackColor;
    213     }
    214 
    215     /**
    216      * Returns the inactive thumb color, can be overridden by each subclass.
    217      */
    218     public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
    219         return defaultInactiveThumbColor;
    220     }
    221 
    222     @Override
    223     protected void dispatchDraw(Canvas canvas) {
    224         super.dispatchDraw(canvas);
    225         onUpdateScrollbar();
    226         mScrollbar.draw(canvas);
    227     }
    228 
    229     /**
    230      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
    231      * this by mapping the available scroll area of the recycler view to the available space for the
    232      * scroll bar.
    233      *
    234      * @param scrollPosState the current scroll position
    235      * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
    236      *                 all rows are the same height)
    237      * @param yOffset the offset to start tracking in the recycler view (only used for all apps)
    238      */
    239     protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
    240             int rowCount, int yOffset) {
    241         int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight,
    242                 yOffset);
    243         int availableScrollBarHeight = getAvailableScrollBarHeight();
    244 
    245         // Only show the scrollbar if there is height to be scrolled
    246         if (availableScrollHeight <= 0) {
    247             mScrollbar.setScrollbarThumbOffset(-1, -1);
    248             return;
    249         }
    250 
    251         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
    252         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
    253         // padding)
    254         int scrollY = getPaddingTop() + yOffset +
    255                 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
    256         int scrollBarY = mBackgroundPadding.top +
    257                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
    258 
    259         // Calculate the position and size of the scroll bar
    260         int scrollBarX;
    261         if (Utilities.isRtl(getResources())) {
    262             scrollBarX = mBackgroundPadding.left;
    263         } else {
    264             scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth();
    265         }
    266         mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY);
    267     }
    268 
    269     /**
    270      * Maps the touch (from 0..1) to the adapter position that should be visible.
    271      * <p>Override in each subclass of this base class.
    272      */
    273     public abstract String scrollToPositionAtProgress(float touchFraction);
    274 
    275     /**
    276      * Updates the bounds for the scrollbar.
    277      * <p>Override in each subclass of this base class.
    278      */
    279     public abstract void onUpdateScrollbar();
    280 
    281     /**
    282      * <p>Override in each subclass of this base class.
    283      */
    284     public void onFastScrollCompleted() {}
    285 }