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.ObjectAnimator;
     19 import android.content.res.Resources;
     20 import android.graphics.Canvas;
     21 import android.graphics.Color;
     22 import android.graphics.Paint;
     23 import android.graphics.Path;
     24 import android.graphics.Rect;
     25 import android.util.Property;
     26 import android.view.MotionEvent;
     27 import android.view.View;
     28 import android.view.ViewConfiguration;
     29 import android.widget.TextView;
     30 
     31 import com.android.launcher3.config.FeatureFlags;
     32 import com.android.launcher3.util.Themes;
     33 
     34 /**
     35  * The track and scrollbar that shows when you scroll the list.
     36  */
     37 public class BaseRecyclerViewFastScrollBar {
     38 
     39     private static final Property<BaseRecyclerViewFastScrollBar, Integer> TRACK_WIDTH =
     40             new Property<BaseRecyclerViewFastScrollBar, Integer>(Integer.class, "width") {
     41 
     42                 @Override
     43                 public Integer get(BaseRecyclerViewFastScrollBar scrollBar) {
     44                     return scrollBar.mWidth;
     45                 }
     46 
     47                 @Override
     48                 public void set(BaseRecyclerViewFastScrollBar scrollBar, Integer value) {
     49                     scrollBar.setTrackWidth(value);
     50                 }
     51             };
     52 
     53     private final static int MAX_TRACK_ALPHA = 30;
     54     private final static int SCROLL_BAR_VIS_DURATION = 150;
     55     private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
     56 
     57     private final Rect mTmpRect = new Rect();
     58     private final BaseRecyclerView mRv;
     59 
     60     private final boolean mIsRtl;
     61 
     62     // The inset is the buffer around which a point will still register as a click on the scrollbar
     63     private final int mTouchInset;
     64 
     65     private final int mMinWidth;
     66     private final int mMaxWidth;
     67 
     68     // Current width of the track
     69     private int mWidth;
     70     private ObjectAnimator mWidthAnimator;
     71 
     72     private final Path mThumbPath = new Path();
     73     private final Paint mThumbPaint;
     74     private final int mThumbHeight;
     75 
     76     private final Paint mTrackPaint;
     77 
     78     private float mLastTouchY;
     79     private boolean mIsDragging;
     80     private boolean mIsThumbDetached;
     81     private boolean mCanThumbDetach;
     82     private boolean mIgnoreDragGesture;
     83 
     84     // This is the offset from the top of the scrollbar when the user first starts touching.  To
     85     // prevent jumping, this offset is applied as the user scrolls.
     86     private int mTouchOffsetY;
     87     private int mThumbOffsetY;
     88 
     89     // Fast scroller popup
     90     private TextView mPopupView;
     91     private boolean mPopupVisible;
     92     private String mPopupSectionName;
     93 
     94     public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
     95         mRv = rv;
     96         mTrackPaint = new Paint();
     97         mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
     98         mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
     99 
    100         mThumbPaint = new Paint();
    101         mThumbPaint.setAntiAlias(true);
    102         mThumbPaint.setColor(Themes.getColorAccent(rv.getContext()));
    103         mThumbPaint.setStyle(Paint.Style.FILL);
    104 
    105         mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
    106         mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
    107         mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
    108         mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
    109         mIsRtl = Utilities.isRtl(res);
    110         updateThumbPath();
    111     }
    112 
    113     public void setPopupView(View popup) {
    114         mPopupView = (TextView) popup;
    115     }
    116 
    117     public void setDetachThumbOnFastScroll() {
    118         mCanThumbDetach = true;
    119     }
    120 
    121     public void reattachThumbToScroll() {
    122         mIsThumbDetached = false;
    123     }
    124 
    125     private int getDrawLeft() {
    126         return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
    127     }
    128 
    129     public void setThumbOffsetY(int y) {
    130         if (mThumbOffsetY == y) {
    131             return;
    132         }
    133 
    134         // Invalidate the previous and new thumb area
    135         int drawLeft = getDrawLeft();
    136         mTmpRect.set(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
    137         mThumbOffsetY = y;
    138         mTmpRect.union(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
    139         mRv.invalidate(mTmpRect);
    140     }
    141 
    142     public int getThumbOffsetY() {
    143         return mThumbOffsetY;
    144     }
    145 
    146     private void setTrackWidth(int width) {
    147         if (mWidth == width) {
    148             return;
    149         }
    150         int left = getDrawLeft();
    151         // Invalidate the whole scroll bar area.
    152         mRv.invalidate(left, 0, left + mMaxWidth, mRv.getScrollbarTrackHeight());
    153 
    154         mWidth = width;
    155         updateThumbPath();
    156     }
    157 
    158     /**
    159      * Updates the path for the thumb drawable.
    160      */
    161     private void updateThumbPath() {
    162         int smallWidth = mIsRtl ? mWidth : -mWidth;
    163         int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth;
    164 
    165         mThumbPath.reset();
    166         mThumbPath.moveTo(0, 0);
    167         mThumbPath.lineTo(0, mThumbHeight);             // Left edge
    168         mThumbPath.lineTo(smallWidth, mThumbHeight);    // bottom edge
    169         mThumbPath.cubicTo(smallWidth, mThumbHeight,    // right edge
    170                 largeWidth, mThumbHeight / 2,
    171                 smallWidth, 0);
    172         mThumbPath.close();
    173     }
    174 
    175     public int getThumbHeight() {
    176         return mThumbHeight;
    177     }
    178 
    179     public boolean isDraggingThumb() {
    180         return mIsDragging;
    181     }
    182 
    183     public boolean isThumbDetached() {
    184         return mIsThumbDetached;
    185     }
    186 
    187     /**
    188      * Handles the touch event and determines whether to show the fast scroller (or updates it if
    189      * it is already showing).
    190      */
    191     public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
    192         ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
    193 
    194         int action = ev.getAction();
    195         int y = (int) ev.getY();
    196         switch (action) {
    197             case MotionEvent.ACTION_DOWN:
    198                 if (isNearThumb(downX, downY)) {
    199                     mTouchOffsetY = downY - mThumbOffsetY;
    200                 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
    201                         && mRv.supportsFastScrolling()
    202                         && isNearScrollBar(downX)) {
    203                     calcTouchOffsetAndPrepToFastScroll(downY, lastY);
    204                     updateFastScrollSectionNameAndThumbOffset(lastY, y);
    205                 }
    206                 break;
    207             case MotionEvent.ACTION_MOVE:
    208                 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
    209                 // exceeded some fixed movement
    210                 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
    211                 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
    212                         isNearThumb(downX, lastY) &&
    213                         Math.abs(y - downY) > config.getScaledTouchSlop()) {
    214                     calcTouchOffsetAndPrepToFastScroll(downY, lastY);
    215                 }
    216                 if (mIsDragging) {
    217                     updateFastScrollSectionNameAndThumbOffset(lastY, y);
    218                 }
    219                 break;
    220             case MotionEvent.ACTION_UP:
    221             case MotionEvent.ACTION_CANCEL:
    222                 mTouchOffsetY = 0;
    223                 mLastTouchY = 0;
    224                 mIgnoreDragGesture = false;
    225                 if (mIsDragging) {
    226                     mIsDragging = false;
    227                     animatePopupVisibility(false);
    228                     showActiveScrollbar(false);
    229                 }
    230                 break;
    231         }
    232     }
    233 
    234     private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
    235         mRv.getParent().requestDisallowInterceptTouchEvent(true);
    236         mIsDragging = true;
    237         if (mCanThumbDetach) {
    238             mIsThumbDetached = true;
    239         }
    240         mTouchOffsetY += (lastY - downY);
    241         animatePopupVisibility(true);
    242         showActiveScrollbar(true);
    243     }
    244 
    245     private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
    246         // Update the fastscroller section name at this touch position
    247         int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
    248         float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
    249         String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom);
    250         if (!sectionName.equals(mPopupSectionName)) {
    251             mPopupSectionName = sectionName;
    252             mPopupView.setText(sectionName);
    253         }
    254         animatePopupVisibility(!sectionName.isEmpty());
    255         updatePopupY(lastY);
    256         mLastTouchY = boundedY;
    257         setThumbOffsetY((int) mLastTouchY);
    258     }
    259 
    260     public void draw(Canvas canvas) {
    261         if (mThumbOffsetY < 0) {
    262             return;
    263         }
    264         int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
    265         if (!mIsRtl) {
    266             canvas.translate(mRv.getWidth(), 0);
    267         }
    268         // Draw the track
    269         int thumbWidth = mIsRtl ? mWidth : -mWidth;
    270         canvas.drawRect(0, 0, thumbWidth, mRv.getScrollbarTrackHeight(), mTrackPaint);
    271 
    272         canvas.translate(0, mThumbOffsetY);
    273         canvas.drawPath(mThumbPath, mThumbPaint);
    274         canvas.restoreToCount(saveCount);
    275     }
    276 
    277     /**
    278      * Animates the width of the scrollbar.
    279      */
    280     private void showActiveScrollbar(boolean isScrolling) {
    281         if (mWidthAnimator != null) {
    282             mWidthAnimator.cancel();
    283         }
    284 
    285         mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
    286                 isScrolling ? mMaxWidth : mMinWidth);
    287         mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
    288         mWidthAnimator.start();
    289     }
    290 
    291     /**
    292      * Returns whether the specified point is inside the thumb bounds.
    293      */
    294     public boolean isNearThumb(int x, int y) {
    295         int left = getDrawLeft();
    296         mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
    297         mTmpRect.inset(mTouchInset, mTouchInset);
    298         return mTmpRect.contains(x, y);
    299     }
    300 
    301     /**
    302      * Returns whether the specified x position is near the scroll bar.
    303      */
    304     public boolean isNearScrollBar(int x) {
    305         int left = getDrawLeft();
    306         return x >= left && x <= left + mMaxWidth;
    307     }
    308 
    309     private void animatePopupVisibility(boolean visible) {
    310         if (mPopupVisible != visible) {
    311             mPopupVisible = visible;
    312             mPopupView.animate().cancel();
    313             mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
    314         }
    315     }
    316 
    317     private void updatePopupY(int lastTouchY) {
    318         int height = mPopupView.getHeight();
    319         float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height);
    320         top = Math.max(mMaxWidth, Math.min(top, mRv.getScrollbarTrackHeight() - mMaxWidth - height));
    321         mPopupView.setTranslationY(top);
    322     }
    323 }
    324