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