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     protected BaseRecyclerViewFastScrollBar mScrollbar;
     45 
     46     private int mDownX;
     47     private int mDownY;
     48     private int mLastY;
     49     protected Rect mBackgroundPadding = new Rect();
     50 
     51     public BaseRecyclerView(Context context) {
     52         this(context, null);
     53     }
     54 
     55     public BaseRecyclerView(Context context, AttributeSet attrs) {
     56         this(context, attrs, 0);
     57     }
     58 
     59     public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
     60         super(context, attrs, defStyleAttr);
     61         mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
     62         mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources());
     63 
     64         ScrollListener listener = new ScrollListener();
     65         setOnScrollListener(listener);
     66     }
     67 
     68     private class ScrollListener extends OnScrollListener {
     69         public ScrollListener() {
     70             // Do nothing
     71         }
     72 
     73         @Override
     74         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
     75             mDy = dy;
     76 
     77             // TODO(winsonc): If we want to animate the section heads while scrolling, we can
     78             //                initiate that here if the recycler view scroll state is not
     79             //                RecyclerView.SCROLL_STATE_IDLE.
     80 
     81             onUpdateScrollbar(dy);
     82         }
     83     }
     84 
     85     public void reset() {
     86         mScrollbar.reattachThumbToScroll();
     87     }
     88 
     89     @Override
     90     protected void onFinishInflate() {
     91         super.onFinishInflate();
     92         addOnItemTouchListener(this);
     93     }
     94 
     95     /**
     96      * We intercept the touch handling only to support fast scrolling when initiated from the
     97      * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
     98      */
     99     @Override
    100     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
    101         return handleTouchEvent(ev);
    102     }
    103 
    104     @Override
    105     public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
    106         handleTouchEvent(ev);
    107     }
    108 
    109     /**
    110      * Handles the touch event and determines whether to show the fast scroller (or updates it if
    111      * it is already showing).
    112      */
    113     private boolean handleTouchEvent(MotionEvent ev) {
    114         int action = ev.getAction();
    115         int x = (int) ev.getX();
    116         int y = (int) ev.getY();
    117         switch (action) {
    118             case MotionEvent.ACTION_DOWN:
    119                 // Keep track of the down positions
    120                 mDownX = x;
    121                 mDownY = mLastY = y;
    122                 if (shouldStopScroll(ev)) {
    123                     stopScroll();
    124                 }
    125                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    126                 break;
    127             case MotionEvent.ACTION_MOVE:
    128                 mLastY = y;
    129                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    130                 break;
    131             case MotionEvent.ACTION_UP:
    132             case MotionEvent.ACTION_CANCEL:
    133                 onFastScrollCompleted();
    134                 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY);
    135                 break;
    136         }
    137         return mScrollbar.isDraggingThumb();
    138     }
    139 
    140     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    141         // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS
    142     }
    143 
    144     /**
    145      * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped.
    146      */
    147     protected boolean shouldStopScroll(MotionEvent ev) {
    148         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    149             if ((Math.abs(mDy) < mDeltaThreshold &&
    150                     getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
    151                 // now the touch events are being passed to the {@link WidgetCell} until the
    152                 // touch sequence goes over the touch slop.
    153                 return true;
    154             }
    155         }
    156         return false;
    157     }
    158 
    159     public void updateBackgroundPadding(Rect padding) {
    160         mBackgroundPadding.set(padding);
    161     }
    162 
    163     public Rect getBackgroundPadding() {
    164         return mBackgroundPadding;
    165     }
    166 
    167     /**
    168      * Returns the scroll bar width when the user is scrolling.
    169      */
    170     public int getMaxScrollbarWidth() {
    171         return mScrollbar.getThumbMaxWidth();
    172     }
    173 
    174     /**
    175      * Returns the visible height of the recycler view:
    176      *   VisibleHeight = View height - top padding - bottom padding
    177      */
    178     protected int getVisibleHeight() {
    179         int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
    180         return visibleHeight;
    181     }
    182 
    183     /**
    184      * Returns the available scroll height:
    185      *   AvailableScrollHeight = Total height of the all items - last page height
    186      */
    187     protected abstract int getAvailableScrollHeight();
    188 
    189     /**
    190      * Returns the available scroll bar height:
    191      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
    192      */
    193     protected int getAvailableScrollBarHeight() {
    194         int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
    195         return availableScrollBarHeight;
    196     }
    197 
    198     /**
    199      * Returns the track color (ignoring alpha), can be overridden by each subclass.
    200      */
    201     public int getFastScrollerTrackColor(int defaultTrackColor) {
    202         return defaultTrackColor;
    203     }
    204 
    205     /**
    206      * Returns the inactive thumb color, can be overridden by each subclass.
    207      */
    208     public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) {
    209         return defaultInactiveThumbColor;
    210     }
    211 
    212     /**
    213      * Returns the scrollbar for this recycler view.
    214      */
    215     public BaseRecyclerViewFastScrollBar getScrollBar() {
    216         return mScrollbar;
    217     }
    218 
    219     @Override
    220     protected void dispatchDraw(Canvas canvas) {
    221         super.dispatchDraw(canvas);
    222         onUpdateScrollbar(0);
    223         mScrollbar.draw(canvas);
    224     }
    225 
    226     /**
    227      * Updates the scrollbar thumb offset to match the visible scroll of the recycler view.  It does
    228      * this by mapping the available scroll area of the recycler view to the available space for the
    229      * scroll bar.
    230      *
    231      * @param scrollY the current scroll y
    232      */
    233     protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY,
    234             int availableScrollHeight) {
    235         // Only show the scrollbar if there is height to be scrolled
    236         int availableScrollBarHeight = getAvailableScrollBarHeight();
    237         if (availableScrollHeight <= 0) {
    238             mScrollbar.setThumbOffset(-1, -1);
    239             return;
    240         }
    241 
    242         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
    243         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
    244         // padding)
    245         int scrollBarY = mBackgroundPadding.top +
    246                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
    247 
    248         // Calculate the position and size of the scroll bar
    249         mScrollbar.setThumbOffset(getScrollBarX(), scrollBarY);
    250     }
    251 
    252     /**
    253      * @return the x position for the scrollbar thumb
    254      */
    255     protected int getScrollBarX() {
    256         if (Utilities.isRtl(getResources())) {
    257             return mBackgroundPadding.left;
    258         } else {
    259             return getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth();
    260         }
    261     }
    262 
    263     /**
    264      * @return whether fast scrolling is supported in the current state.
    265      */
    266     protected boolean supportsFastScrolling() {
    267         return true;
    268     }
    269 
    270     /**
    271      * Maps the touch (from 0..1) to the adapter position that should be visible.
    272      * <p>Override in each subclass of this base class.
    273      *
    274      * @return the scroll top of this recycler view.
    275      */
    276     public abstract int getCurrentScrollY();
    277 
    278     /**
    279      * Maps the touch (from 0..1) to the adapter position that should be visible.
    280      * <p>Override in each subclass of this base class.
    281      */
    282     protected abstract String scrollToPositionAtProgress(float touchFraction);
    283 
    284     /**
    285      * Updates the bounds for the scrollbar.
    286      * <p>Override in each subclass of this base class.
    287      */
    288     protected abstract void onUpdateScrollbar(int dy);
    289 
    290     /**
    291      * <p>Override in each subclass of this base class.
    292      */
    293     protected void onFastScrollCompleted() {}
    294 }