Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2017 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.views;
     18 
     19 import android.animation.ObjectAnimator;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.Point;
     26 import android.graphics.Rect;
     27 import android.support.v7.widget.RecyclerView;
     28 import android.util.AttributeSet;
     29 import android.util.Property;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewConfiguration;
     33 import android.widget.TextView;
     34 
     35 import com.android.launcher3.BaseRecyclerView;
     36 import com.android.launcher3.R;
     37 import com.android.launcher3.Utilities;
     38 import com.android.launcher3.config.FeatureFlags;
     39 import com.android.launcher3.graphics.FastScrollThumbDrawable;
     40 import com.android.launcher3.util.Themes;
     41 
     42 /**
     43  * The track and scrollbar that shows when you scroll the list.
     44  */
     45 public class RecyclerViewFastScroller extends View {
     46 
     47     private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
     48     private static final Rect sTempRect = new Rect();
     49 
     50     private static final Property<RecyclerViewFastScroller, Integer> TRACK_WIDTH =
     51             new Property<RecyclerViewFastScroller, Integer>(Integer.class, "width") {
     52 
     53                 @Override
     54                 public Integer get(RecyclerViewFastScroller scrollBar) {
     55                     return scrollBar.mWidth;
     56                 }
     57 
     58                 @Override
     59                 public void set(RecyclerViewFastScroller scrollBar, Integer value) {
     60                     scrollBar.setTrackWidth(value);
     61                 }
     62             };
     63 
     64     private final static int MAX_TRACK_ALPHA = 30;
     65     private final static int SCROLL_BAR_VIS_DURATION = 150;
     66     private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f;
     67 
     68     private final int mMinWidth;
     69     private final int mMaxWidth;
     70     private final int mThumbPadding;
     71 
     72     /** Keeps the last known scrolling delta/velocity along y-axis. */
     73     private int mDy = 0;
     74     private final float mDeltaThreshold;
     75 
     76     private final ViewConfiguration mConfig;
     77 
     78     // Current width of the track
     79     private int mWidth;
     80     private ObjectAnimator mWidthAnimator;
     81 
     82     private final Paint mThumbPaint;
     83     protected final int mThumbHeight;
     84 
     85     private final Paint mTrackPaint;
     86 
     87     private float mLastTouchY;
     88     private boolean mIsDragging;
     89     private boolean mIsThumbDetached;
     90     private final boolean mCanThumbDetach;
     91     private boolean mIgnoreDragGesture;
     92 
     93     // This is the offset from the top of the scrollbar when the user first starts touching.  To
     94     // prevent jumping, this offset is applied as the user scrolls.
     95     protected int mTouchOffsetY;
     96     protected int mThumbOffsetY;
     97 
     98     // Fast scroller popup
     99     private TextView mPopupView;
    100     private boolean mPopupVisible;
    101     private String mPopupSectionName;
    102 
    103     protected BaseRecyclerView mRv;
    104     private RecyclerView.OnScrollListener mOnScrollListener;
    105 
    106     private int mDownX;
    107     private int mDownY;
    108     private int mLastY;
    109 
    110     public RecyclerViewFastScroller(Context context) {
    111         this(context, null);
    112     }
    113 
    114     public RecyclerViewFastScroller(Context context, AttributeSet attrs) {
    115         this(context, attrs, 0);
    116     }
    117 
    118     public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) {
    119         super(context, attrs, defStyleAttr);
    120 
    121         mTrackPaint = new Paint();
    122         mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
    123         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
    124 
    125         mThumbPaint = new Paint();
    126         mThumbPaint.setAntiAlias(true);
    127         mThumbPaint.setColor(Themes.getColorAccent(context));
    128         mThumbPaint.setStyle(Paint.Style.FILL);
    129 
    130         Resources res = getResources();
    131         mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_min_width);
    132         mMaxWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_max_width);
    133 
    134         mThumbPadding = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_padding);
    135         mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height);
    136 
    137         mConfig = ViewConfiguration.get(context);
    138         mDeltaThreshold = res.getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP;
    139 
    140         TypedArray ta =
    141                 context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewFastScroller, defStyleAttr, 0);
    142         mCanThumbDetach = ta.getBoolean(R.styleable.RecyclerViewFastScroller_canThumbDetach, false);
    143         ta.recycle();
    144     }
    145 
    146     public void setRecyclerView(BaseRecyclerView rv, TextView popupView) {
    147         if (mRv != null && mOnScrollListener != null) {
    148             mRv.removeOnScrollListener(mOnScrollListener);
    149         }
    150         mRv = rv;
    151 
    152         mRv.addOnScrollListener(mOnScrollListener = new RecyclerView.OnScrollListener() {
    153             @Override
    154             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    155                 mDy = dy;
    156 
    157                 // TODO(winsonc): If we want to animate the section heads while scrolling, we can
    158                 //                initiate that here if the recycler view scroll state is not
    159                 //                RecyclerView.SCROLL_STATE_IDLE.
    160 
    161                 mRv.onUpdateScrollbar(dy);
    162             }
    163         });
    164 
    165         mPopupView = popupView;
    166         mPopupView.setBackground(
    167                 new FastScrollThumbDrawable(mThumbPaint, Utilities.isRtl(getResources())));
    168     }
    169 
    170     public void reattachThumbToScroll() {
    171         mIsThumbDetached = false;
    172     }
    173 
    174     public void setThumbOffsetY(int y) {
    175         if (mThumbOffsetY == y) {
    176             return;
    177         }
    178         mThumbOffsetY = y;
    179         invalidate();
    180     }
    181 
    182     public int getThumbOffsetY() {
    183         return mThumbOffsetY;
    184     }
    185 
    186     private void setTrackWidth(int width) {
    187         if (mWidth == width) {
    188             return;
    189         }
    190         mWidth = width;
    191         invalidate();
    192     }
    193 
    194     public int getThumbHeight() {
    195         return mThumbHeight;
    196     }
    197 
    198     public boolean isDraggingThumb() {
    199         return mIsDragging;
    200     }
    201 
    202     public boolean isThumbDetached() {
    203         return mIsThumbDetached;
    204     }
    205 
    206     /**
    207      * Handles the touch event and determines whether to show the fast scroller (or updates it if
    208      * it is already showing).
    209      */
    210     public boolean handleTouchEvent(MotionEvent ev, Point offset) {
    211         int x = (int) ev.getX() - offset.x;
    212         int y = (int) ev.getY() - offset.y;
    213         switch (ev.getAction()) {
    214             case MotionEvent.ACTION_DOWN:
    215                 // Keep track of the down positions
    216                 mDownX = x;
    217                 mDownY = mLastY = y;
    218 
    219                 if ((Math.abs(mDy) < mDeltaThreshold &&
    220                         mRv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
    221                     // now the touch events are being passed to the {@link WidgetCell} until the
    222                     // touch sequence goes over the touch slop.
    223                     mRv.stopScroll();
    224                 }
    225                 if (isNearThumb(x, y)) {
    226                     mTouchOffsetY = mDownY - mThumbOffsetY;
    227                 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
    228                         && mRv.supportsFastScrolling()
    229                         && isNearScrollBar(mDownX)) {
    230                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
    231                     updateFastScrollSectionNameAndThumbOffset(mLastY, y);
    232                 }
    233                 break;
    234             case MotionEvent.ACTION_MOVE:
    235                 mLastY = y;
    236 
    237                 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
    238                 // exceeded some fixed movement
    239                 mIgnoreDragGesture |= Math.abs(y - mDownY) > mConfig.getScaledPagingTouchSlop();
    240                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
    241                         isNearThumb(mDownX, mLastY) &&
    242                         Math.abs(y - mDownY) > mConfig.getScaledTouchSlop()) {
    243                     calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
    244                 }
    245                 if (mIsDragging) {
    246                     updateFastScrollSectionNameAndThumbOffset(mLastY, y);
    247                 }
    248                 break;
    249             case MotionEvent.ACTION_UP:
    250             case MotionEvent.ACTION_CANCEL:
    251                 mRv.onFastScrollCompleted();
    252                 mTouchOffsetY = 0;
    253                 mLastTouchY = 0;
    254                 mIgnoreDragGesture = false;
    255                 if (mIsDragging) {
    256                     mIsDragging = false;
    257                     animatePopupVisibility(false);
    258                     showActiveScrollbar(false);
    259                 }
    260                 break;
    261         }
    262         return mIsDragging;
    263     }
    264 
    265     private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
    266         mIsDragging = true;
    267         if (mCanThumbDetach) {
    268             mIsThumbDetached = true;
    269         }
    270         mTouchOffsetY += (lastY - downY);
    271         animatePopupVisibility(true);
    272         showActiveScrollbar(true);
    273     }
    274 
    275     private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
    276         // Update the fastscroller section name at this touch position
    277         int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
    278         float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
    279         String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom);
    280         if (!sectionName.equals(mPopupSectionName)) {
    281             mPopupSectionName = sectionName;
    282             mPopupView.setText(sectionName);
    283         }
    284         animatePopupVisibility(!sectionName.isEmpty());
    285         updatePopupY(lastY);
    286         mLastTouchY = boundedY;
    287         setThumbOffsetY((int) mLastTouchY);
    288     }
    289 
    290     public void onDraw(Canvas canvas) {
    291         if (mThumbOffsetY < 0) {
    292             return;
    293         }
    294         int saveCount = canvas.save();
    295         canvas.translate(getWidth() / 2, mRv.getScrollBarTop());
    296         // Draw the track
    297         float halfW = mWidth / 2;
    298         canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(),
    299                 mWidth, mWidth, mTrackPaint);
    300 
    301         canvas.translate(0, mThumbOffsetY);
    302         halfW += mThumbPadding;
    303         float r = mWidth + mThumbPadding + mThumbPadding;
    304         canvas.drawRoundRect(-halfW, 0, halfW, mThumbHeight, r, r, mThumbPaint);
    305         canvas.restoreToCount(saveCount);
    306     }
    307 
    308 
    309     /**
    310      * Animates the width of the scrollbar.
    311      */
    312     private void showActiveScrollbar(boolean isScrolling) {
    313         if (mWidthAnimator != null) {
    314             mWidthAnimator.cancel();
    315         }
    316 
    317         mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
    318                 isScrolling ? mMaxWidth : mMinWidth);
    319         mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
    320         mWidthAnimator.start();
    321     }
    322 
    323     /**
    324      * Returns whether the specified point is inside the thumb bounds.
    325      */
    326     private boolean isNearThumb(int x, int y) {
    327         int offset = y - mThumbOffsetY;
    328 
    329         return x >= 0 && x < getWidth() && offset >= 0 && offset <= mThumbHeight;
    330     }
    331 
    332     /**
    333      * Returns true if AllAppsTransitionController can handle vertical motion
    334      * beginning at this point.
    335      */
    336     public boolean shouldBlockIntercept(int x, int y) {
    337         return isNearThumb(x, y);
    338     }
    339 
    340     /**
    341      * Returns whether the specified x position is near the scroll bar.
    342      */
    343     public boolean isNearScrollBar(int x) {
    344         return x >= (getWidth() - mMaxWidth) / 2 && x <= (getWidth() + mMaxWidth) / 2;
    345     }
    346 
    347     private void animatePopupVisibility(boolean visible) {
    348         if (mPopupVisible != visible) {
    349             mPopupVisible = visible;
    350             mPopupView.animate().cancel();
    351             mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
    352         }
    353     }
    354 
    355     private void updatePopupY(int lastTouchY) {
    356         int height = mPopupView.getHeight();
    357         float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height)
    358                 + mRv.getScrollBarTop();
    359         top = Utilities.boundToRange(top,
    360                 mMaxWidth, mRv.getScrollbarTrackHeight() - mMaxWidth - height);
    361         mPopupView.setTranslationY(top);
    362     }
    363 
    364     public boolean isHitInParent(float x, float y, Point outOffset) {
    365         if (mThumbOffsetY < 0) {
    366             return false;
    367         }
    368         getHitRect(sTempRect);
    369         sTempRect.top += mRv.getScrollBarTop();
    370         if (outOffset != null) {
    371             outOffset.set(sTempRect.left, sTempRect.top);
    372         }
    373         return sTempRect.contains((int) x, (int) y);
    374     }
    375 
    376     @Override
    377     public boolean hasOverlappingRendering() {
    378         // There is actually some overlap between the track and the thumb. But since the track
    379         // alpha is so low, it does not matter.
    380         return false;
    381     }
    382 }
    383