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