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 package com.android.launcher3;
     17 
     18 import android.animation.AnimatorSet;
     19 import android.animation.ArgbEvaluator;
     20 import android.animation.ObjectAnimator;
     21 import android.animation.ValueAnimator;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Paint;
     26 import android.graphics.Path;
     27 import android.graphics.Point;
     28 import android.graphics.Rect;
     29 import android.view.MotionEvent;
     30 import android.view.VelocityTracker;
     31 import android.view.ViewConfiguration;
     32 
     33 import com.android.launcher3.util.Thunk;
     34 
     35 /**
     36  * The track and scrollbar that shows when you scroll the list.
     37  */
     38 public class BaseRecyclerViewFastScrollBar {
     39 
     40     public interface FastScrollFocusableView {
     41         void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
     42     }
     43 
     44     private final static int MAX_TRACK_ALPHA = 30;
     45     private final static int SCROLL_BAR_VIS_DURATION = 150;
     46 
     47     @Thunk BaseRecyclerView mRv;
     48     private BaseRecyclerViewFastScrollPopup mPopup;
     49 
     50     private AnimatorSet mScrollbarAnimator;
     51 
     52     private int mThumbInactiveColor;
     53     private int mThumbActiveColor;
     54     @Thunk Point mThumbOffset = new Point(-1, -1);
     55     @Thunk Paint mThumbPaint;
     56     private int mThumbMinWidth;
     57     private int mThumbMaxWidth;
     58     @Thunk int mThumbWidth;
     59     @Thunk int mThumbHeight;
     60     private int mThumbCurvature;
     61     private Path mThumbPath = new Path();
     62     private Paint mTrackPaint;
     63     private int mTrackWidth;
     64     private float mLastTouchY;
     65     // The inset is the buffer around which a point will still register as a click on the scrollbar
     66     private int mTouchInset;
     67     private boolean mIsDragging;
     68     private boolean mIsThumbDetached;
     69     private boolean mCanThumbDetach;
     70     private boolean mIgnoreDragGesture;
     71 
     72     // This is the offset from the top of the scrollbar when the user first starts touching.  To
     73     // prevent jumping, this offset is applied as the user scrolls.
     74     private int mTouchOffset;
     75 
     76     private Rect mInvalidateRect = new Rect();
     77     private Rect mTmpRect = new Rect();
     78 
     79     public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
     80         mRv = rv;
     81         mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
     82         mTrackPaint = new Paint();
     83         mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
     84         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
     85         mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor(
     86                 res.getColor(R.color.container_fastscroll_thumb_inactive_color));
     87         mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color);
     88         mThumbPaint = new Paint();
     89         mThumbPaint.setAntiAlias(true);
     90         mThumbPaint.setColor(mThumbInactiveColor);
     91         mThumbPaint.setStyle(Paint.Style.FILL);
     92         mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
     93         mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
     94         mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
     95         mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
     96         mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
     97     }
     98 
     99     public void setDetachThumbOnFastScroll() {
    100         mCanThumbDetach = true;
    101     }
    102 
    103     public void reattachThumbToScroll() {
    104         mIsThumbDetached = false;
    105     }
    106 
    107     public void setThumbOffset(int x, int y) {
    108         if (mThumbOffset.x == x && mThumbOffset.y == y) {
    109             return;
    110         }
    111         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
    112                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
    113         mThumbOffset.set(x, y);
    114         updateThumbPath();
    115         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
    116                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
    117         mRv.invalidate(mInvalidateRect);
    118     }
    119 
    120     public Point getThumbOffset() {
    121         return mThumbOffset;
    122     }
    123 
    124     // Setter/getter for the thumb bar width for animations
    125     public void setThumbWidth(int width) {
    126         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
    127                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
    128         mThumbWidth = width;
    129         updateThumbPath();
    130         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
    131                 mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
    132         mRv.invalidate(mInvalidateRect);
    133     }
    134 
    135     public int getThumbWidth() {
    136         return mThumbWidth;
    137     }
    138 
    139     // Setter/getter for the track bar width for animations
    140     public void setTrackWidth(int width) {
    141         mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
    142                 mRv.getHeight());
    143         mTrackWidth = width;
    144         updateThumbPath();
    145         mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
    146                 mRv.getHeight());
    147         mRv.invalidate(mInvalidateRect);
    148     }
    149 
    150     public int getTrackWidth() {
    151         return mTrackWidth;
    152     }
    153 
    154     public int getThumbHeight() {
    155         return mThumbHeight;
    156     }
    157 
    158     public int getThumbMaxWidth() {
    159         return mThumbMaxWidth;
    160     }
    161 
    162     public float getLastTouchY() {
    163         return mLastTouchY;
    164     }
    165 
    166     public boolean isDraggingThumb() {
    167         return mIsDragging;
    168     }
    169 
    170     public boolean isThumbDetached() {
    171         return mIsThumbDetached;
    172     }
    173 
    174     /**
    175      * Handles the touch event and determines whether to show the fast scroller (or updates it if
    176      * it is already showing).
    177      */
    178     public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
    179         ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
    180 
    181         int action = ev.getAction();
    182         int y = (int) ev.getY();
    183         switch (action) {
    184             case MotionEvent.ACTION_DOWN:
    185                 if (isNearThumb(downX, downY)) {
    186                     mTouchOffset = downY - mThumbOffset.y;
    187                 }
    188                 break;
    189             case MotionEvent.ACTION_MOVE:
    190                 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
    191                 // exceeded some fixed movement
    192                 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
    193                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
    194                         isNearThumb(downX, lastY) &&
    195                         Math.abs(y - downY) > config.getScaledTouchSlop()) {
    196                     mRv.getParent().requestDisallowInterceptTouchEvent(true);
    197                     mIsDragging = true;
    198                     if (mCanThumbDetach) {
    199                         mIsThumbDetached = true;
    200                     }
    201                     mTouchOffset += (lastY - downY);
    202                     mPopup.animateVisibility(true);
    203                     showActiveScrollbar(true);
    204                 }
    205                 if (mIsDragging) {
    206                     // Update the fastscroller section name at this touch position
    207                     int top = mRv.getBackgroundPadding().top;
    208                     int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight;
    209                     float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
    210                     String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
    211                             (bottom - top));
    212                     mPopup.setSectionName(sectionName);
    213                     mPopup.animateVisibility(!sectionName.isEmpty());
    214                     mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
    215                     mLastTouchY = boundedY;
    216                 }
    217                 break;
    218             case MotionEvent.ACTION_UP:
    219             case MotionEvent.ACTION_CANCEL:
    220                 mTouchOffset = 0;
    221                 mLastTouchY = 0;
    222                 mIgnoreDragGesture = false;
    223                 if (mIsDragging) {
    224                     mIsDragging = false;
    225                     mPopup.animateVisibility(false);
    226                     showActiveScrollbar(false);
    227                 }
    228                 break;
    229         }
    230     }
    231 
    232     public void draw(Canvas canvas) {
    233         if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
    234             return;
    235         }
    236 
    237         // Draw the scroll bar track and thumb
    238         if (mTrackPaint.getAlpha() > 0) {
    239             canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint);
    240         }
    241         canvas.drawPath(mThumbPath, mThumbPaint);
    242 
    243         // Draw the popup
    244         mPopup.draw(canvas);
    245     }
    246 
    247     /**
    248      * Animates the width and color of the scrollbar.
    249      */
    250     private void showActiveScrollbar(boolean isScrolling) {
    251         if (mScrollbarAnimator != null) {
    252             mScrollbarAnimator.cancel();
    253         }
    254 
    255         mScrollbarAnimator = new AnimatorSet();
    256         ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
    257                 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
    258         ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
    259                 isScrolling ? mThumbMaxWidth : mThumbMinWidth);
    260         mScrollbarAnimator.playTogether(trackWidthAnim, thumbWidthAnim);
    261         if (mThumbActiveColor != mThumbInactiveColor) {
    262             ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
    263                     mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
    264             colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    265                 @Override
    266                 public void onAnimationUpdate(ValueAnimator animator) {
    267                     mThumbPaint.setColor((Integer) animator.getAnimatedValue());
    268                     mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
    269                             mThumbOffset.y + mThumbHeight);
    270                 }
    271             });
    272             mScrollbarAnimator.play(colorAnimation);
    273         }
    274         mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
    275         mScrollbarAnimator.start();
    276     }
    277 
    278     /**
    279      * Updates the path for the thumb drawable.
    280      */
    281     private void updateThumbPath() {
    282         mThumbCurvature = mThumbMaxWidth - mThumbWidth;
    283         mThumbPath.reset();
    284         mThumbPath.moveTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y);                    // tr
    285         mThumbPath.lineTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);     // br
    286         mThumbPath.lineTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight);                   // bl
    287         mThumbPath.cubicTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight,
    288                 mThumbOffset.x - mThumbCurvature, mThumbOffset.y + mThumbHeight / 2,
    289                 mThumbOffset.x, mThumbOffset.y);                                            // bl2tl
    290         mThumbPath.close();
    291     }
    292 
    293     /**
    294      * Returns whether the specified points are near the scroll bar bounds.
    295      */
    296     private boolean isNearThumb(int x, int y) {
    297         mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
    298                 mThumbOffset.y + mThumbHeight);
    299         mTmpRect.inset(mTouchInset, mTouchInset);
    300         return mTmpRect.contains(x, y);
    301     }
    302 }
    303