blob: 30397446487c5b501b86a6a08e90122286dc1607 [file] [log] [blame]
Winson Chungb1777442015-06-16 13:35:04 -07001/*
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
Winson Chungb1777442015-06-16 13:35:04 -070018import android.animation.ObjectAnimator;
Winson Chungb1777442015-06-16 13:35:04 -070019import android.content.res.Resources;
20import android.graphics.Canvas;
21import android.graphics.Color;
22import android.graphics.Paint;
Winson67795952015-08-20 12:23:52 -070023import android.graphics.Path;
Winson Chungb1777442015-06-16 13:35:04 -070024import android.graphics.Rect;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070025import android.util.Property;
Winson Chungb1777442015-06-16 13:35:04 -070026import android.view.MotionEvent;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070027import android.view.View;
Winson Chungb1777442015-06-16 13:35:04 -070028import android.view.ViewConfiguration;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070029import android.widget.TextView;
Sunny Goyalb713ad42015-06-24 11:45:32 -070030
Mario Bertschleree4ee422016-12-27 15:41:24 -080031import com.android.launcher3.config.FeatureFlags;
Sunny Goyal1f3f07d2017-02-10 16:52:16 -080032import com.android.launcher3.util.Themes;
Mario Bertschleree4ee422016-12-27 15:41:24 -080033
Winson Chungb1777442015-06-16 13:35:04 -070034/**
35 * The track and scrollbar that shows when you scroll the list.
36 */
37public class BaseRecyclerViewFastScrollBar {
38
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070039 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
Winson Chungb1777442015-06-16 13:35:04 -070053 private final static int MAX_TRACK_ALPHA = 30;
54 private final static int SCROLL_BAR_VIS_DURATION = 150;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070055 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
Winson Chungb1777442015-06-16 13:35:04 -070056
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070057 private final Rect mTmpRect = new Rect();
58 private final BaseRecyclerView mRv;
Winson Chungb1777442015-06-16 13:35:04 -070059
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070060 private final boolean mIsRtl;
Winson Chungb1777442015-06-16 13:35:04 -070061
Winson Chungb1777442015-06-16 13:35:04 -070062 // The inset is the buffer around which a point will still register as a click on the scrollbar
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070063 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;
Winson Chungb1777442015-06-16 13:35:04 -070079 private boolean mIsDragging;
Winsond2eb49e2015-08-18 17:43:02 -070080 private boolean mIsThumbDetached;
81 private boolean mCanThumbDetach;
Winsonec4845b2015-08-27 10:19:48 -070082 private boolean mIgnoreDragGesture;
Winson Chungb1777442015-06-16 13:35:04 -070083
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.
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070086 private int mTouchOffsetY;
87 private int mThumbOffsetY;
Winson Chungb1777442015-06-16 13:35:04 -070088
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070089 // Fast scroller popup
90 private TextView mPopupView;
91 private boolean mPopupVisible;
92 private String mPopupSectionName;
Winson Chungb1777442015-06-16 13:35:04 -070093
94 public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
95 mRv = rv;
Winson Chungb1777442015-06-16 13:35:04 -070096 mTrackPaint = new Paint();
97 mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
Winson67795952015-08-20 12:23:52 -070098 mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070099
Winson Chungb1777442015-06-16 13:35:04 -0700100 mThumbPaint = new Paint();
Winson67795952015-08-20 12:23:52 -0700101 mThumbPaint.setAntiAlias(true);
Sunny Goyal1f3f07d2017-02-10 16:52:16 -0800102 mThumbPaint.setColor(Themes.getColorAccent(rv.getContext()));
Winson67795952015-08-20 12:23:52 -0700103 mThumbPaint.setStyle(Paint.Style.FILL);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700104
105 mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
106 mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
Winson Chungb1777442015-06-16 13:35:04 -0700107 mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
108 mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700109 mIsRtl = Utilities.isRtl(res);
110 updateThumbPath();
111 }
112
113 public void setPopupView(View popup) {
114 mPopupView = (TextView) popup;
Winson Chungb1777442015-06-16 13:35:04 -0700115 }
116
Winsond2eb49e2015-08-18 17:43:02 -0700117 public void setDetachThumbOnFastScroll() {
118 mCanThumbDetach = true;
119 }
120
121 public void reattachThumbToScroll() {
122 mIsThumbDetached = false;
123 }
124
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700125 private int getDrawLeft() {
126 return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
127 }
128
129 public void setThumbOffsetY(int y) {
130 if (mThumbOffsetY == y) {
Winson Chungb1777442015-06-16 13:35:04 -0700131 return;
132 }
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700133
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);
Sunny Goyaldc19a072017-05-12 08:17:35 -0700139 mTmpRect.offset(0, mRv.getPaddingTop());
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700140 mRv.invalidate(mTmpRect);
141 }
142
143 public int getThumbOffsetY() {
144 return mThumbOffsetY;
145 }
146
147 private void setTrackWidth(int width) {
148 if (mWidth == width) {
149 return;
150 }
151 int left = getDrawLeft();
Sunny Goyaldc19a072017-05-12 08:17:35 -0700152 int top = mRv.getPaddingTop();
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700153 // Invalidate the whole scroll bar area.
Sunny Goyaldc19a072017-05-12 08:17:35 -0700154 mRv.invalidate(left, top, left + mMaxWidth, top + mRv.getScrollbarTrackHeight());
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700155
156 mWidth = width;
Winson67795952015-08-20 12:23:52 -0700157 updateThumbPath();
Winson Chungb1777442015-06-16 13:35:04 -0700158 }
159
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700160 /**
161 * Updates the path for the thumb drawable.
162 */
163 private void updateThumbPath() {
164 int smallWidth = mIsRtl ? mWidth : -mWidth;
165 int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth;
Winsond2eb49e2015-08-18 17:43:02 -0700166
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700167 mThumbPath.reset();
168 mThumbPath.moveTo(0, 0);
169 mThumbPath.lineTo(0, mThumbHeight); // Left edge
170 mThumbPath.lineTo(smallWidth, mThumbHeight); // bottom edge
171 mThumbPath.cubicTo(smallWidth, mThumbHeight, // right edge
172 largeWidth, mThumbHeight / 2,
173 smallWidth, 0);
174 mThumbPath.close();
Winson Chungb1777442015-06-16 13:35:04 -0700175 }
176
177 public int getThumbHeight() {
178 return mThumbHeight;
179 }
180
Winsond2eb49e2015-08-18 17:43:02 -0700181 public boolean isDraggingThumb() {
Winson Chungb1777442015-06-16 13:35:04 -0700182 return mIsDragging;
183 }
184
Winsond2eb49e2015-08-18 17:43:02 -0700185 public boolean isThumbDetached() {
186 return mIsThumbDetached;
187 }
188
Winson Chungb1777442015-06-16 13:35:04 -0700189 /**
190 * Handles the touch event and determines whether to show the fast scroller (or updates it if
191 * it is already showing).
192 */
193 public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
194 ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
195
196 int action = ev.getAction();
197 int y = (int) ev.getY();
198 switch (action) {
199 case MotionEvent.ACTION_DOWN:
Winsonec4845b2015-08-27 10:19:48 -0700200 if (isNearThumb(downX, downY)) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700201 mTouchOffsetY = downY - mThumbOffsetY;
Mario Bertschleree4ee422016-12-27 15:41:24 -0800202 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
203 && mRv.supportsFastScrolling()
204 && isNearScrollBar(downX)) {
205 calcTouchOffsetAndPrepToFastScroll(downY, lastY);
206 updateFastScrollSectionNameAndThumbOffset(lastY, y);
Winson Chungb1777442015-06-16 13:35:04 -0700207 }
208 break;
209 case MotionEvent.ACTION_MOVE:
Winsonec4845b2015-08-27 10:19:48 -0700210 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
211 // exceeded some fixed movement
212 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
Winson646c2362015-09-03 11:46:11 -0700213 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
214 isNearThumb(downX, lastY) &&
Winson Chungb1777442015-06-16 13:35:04 -0700215 Math.abs(y - downY) > config.getScaledTouchSlop()) {
Mario Bertschleree4ee422016-12-27 15:41:24 -0800216 calcTouchOffsetAndPrepToFastScroll(downY, lastY);
Winson Chungb1777442015-06-16 13:35:04 -0700217 }
218 if (mIsDragging) {
Mario Bertschleree4ee422016-12-27 15:41:24 -0800219 updateFastScrollSectionNameAndThumbOffset(lastY, y);
Winson Chungb1777442015-06-16 13:35:04 -0700220 }
221 break;
222 case MotionEvent.ACTION_UP:
223 case MotionEvent.ACTION_CANCEL:
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700224 mTouchOffsetY = 0;
Winsond2eb49e2015-08-18 17:43:02 -0700225 mLastTouchY = 0;
Winsonec4845b2015-08-27 10:19:48 -0700226 mIgnoreDragGesture = false;
Winson Chung4c7fc622015-06-24 16:59:31 -0700227 if (mIsDragging) {
228 mIsDragging = false;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700229 animatePopupVisibility(false);
Winsonc0880492015-08-21 11:16:27 -0700230 showActiveScrollbar(false);
Winson Chung4c7fc622015-06-24 16:59:31 -0700231 }
Winson Chungb1777442015-06-16 13:35:04 -0700232 break;
233 }
234 }
235
Mario Bertschleree4ee422016-12-27 15:41:24 -0800236 private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
237 mRv.getParent().requestDisallowInterceptTouchEvent(true);
238 mIsDragging = true;
239 if (mCanThumbDetach) {
240 mIsThumbDetached = true;
241 }
242 mTouchOffsetY += (lastY - downY);
243 animatePopupVisibility(true);
244 showActiveScrollbar(true);
245 }
246
247 private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
248 // Update the fastscroller section name at this touch position
249 int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
250 float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
251 String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom);
252 if (!sectionName.equals(mPopupSectionName)) {
253 mPopupSectionName = sectionName;
254 mPopupView.setText(sectionName);
255 }
256 animatePopupVisibility(!sectionName.isEmpty());
257 updatePopupY(lastY);
258 mLastTouchY = boundedY;
259 setThumbOffsetY((int) mLastTouchY);
260 }
261
Winson Chungb1777442015-06-16 13:35:04 -0700262 public void draw(Canvas canvas) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700263 if (mThumbOffsetY < 0) {
Winson Chungb1777442015-06-16 13:35:04 -0700264 return;
265 }
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700266 int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
267 if (!mIsRtl) {
268 canvas.translate(mRv.getWidth(), 0);
Winson Chungb1777442015-06-16 13:35:04 -0700269 }
Sunny Goyaldc19a072017-05-12 08:17:35 -0700270 canvas.translate(0, mRv.getPaddingTop());
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700271 // Draw the track
272 int thumbWidth = mIsRtl ? mWidth : -mWidth;
Sunny Goyal00e10682016-10-18 14:23:46 +0100273 canvas.drawRect(0, 0, thumbWidth, mRv.getScrollbarTrackHeight(), mTrackPaint);
Winson Chungb1777442015-06-16 13:35:04 -0700274
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700275 canvas.translate(0, mThumbOffsetY);
276 canvas.drawPath(mThumbPath, mThumbPaint);
277 canvas.restoreToCount(saveCount);
Winson Chungb1777442015-06-16 13:35:04 -0700278 }
279
280 /**
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700281 * Animates the width of the scrollbar.
Winson Chungb1777442015-06-16 13:35:04 -0700282 */
Winsonc0880492015-08-21 11:16:27 -0700283 private void showActiveScrollbar(boolean isScrolling) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700284 if (mWidthAnimator != null) {
285 mWidthAnimator.cancel();
Winson Chungb1777442015-06-16 13:35:04 -0700286 }
Winson67795952015-08-20 12:23:52 -0700287
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700288 mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
289 isScrolling ? mMaxWidth : mMinWidth);
290 mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
291 mWidthAnimator.start();
Winson67795952015-08-20 12:23:52 -0700292 }
293
294 /**
Mario Bertschleree4ee422016-12-27 15:41:24 -0800295 * Returns whether the specified point is inside the thumb bounds.
Winson Chungb1777442015-06-16 13:35:04 -0700296 */
Hyunyoung Songf4cbb142016-06-10 12:00:02 -0700297 public boolean isNearThumb(int x, int y) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700298 int left = getDrawLeft();
299 mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700300 mTmpRect.inset(mTouchInset, mTouchInset);
301 return mTmpRect.contains(x, y);
302 }
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700303
Mario Bertschleree4ee422016-12-27 15:41:24 -0800304 /**
305 * Returns whether the specified x position is near the scroll bar.
306 */
307 public boolean isNearScrollBar(int x) {
308 int left = getDrawLeft();
309 return x >= left && x <= left + mMaxWidth;
310 }
311
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700312 private void animatePopupVisibility(boolean visible) {
313 if (mPopupVisible != visible) {
314 mPopupVisible = visible;
315 mPopupView.animate().cancel();
316 mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
317 }
318 }
319
320 private void updatePopupY(int lastTouchY) {
321 int height = mPopupView.getHeight();
322 float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height);
Sunny Goyal00e10682016-10-18 14:23:46 +0100323 top = Math.max(mMaxWidth, Math.min(top, mRv.getScrollbarTrackHeight() - mMaxWidth - height));
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700324 mPopupView.setTranslationY(top);
325 }
Winson Chungb1777442015-06-16 13:35:04 -0700326}