blob: 9e8d300ae13e933acadb285ed1589071b5769952 [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 Goyal1a8f6fb2017-06-14 15:35:16 -070032import com.android.launcher3.graphics.FastScrollThumbDrawable;
Sunny Goyal1f3f07d2017-02-10 16:52:16 -080033import com.android.launcher3.util.Themes;
Mario Bertschleree4ee422016-12-27 15:41:24 -080034
Winson Chungb1777442015-06-16 13:35:04 -070035/**
36 * The track and scrollbar that shows when you scroll the list.
37 */
38public class BaseRecyclerViewFastScrollBar {
39
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070040 private static final Property<BaseRecyclerViewFastScrollBar, Integer> TRACK_WIDTH =
41 new Property<BaseRecyclerViewFastScrollBar, Integer>(Integer.class, "width") {
42
43 @Override
44 public Integer get(BaseRecyclerViewFastScrollBar scrollBar) {
45 return scrollBar.mWidth;
46 }
47
48 @Override
49 public void set(BaseRecyclerViewFastScrollBar scrollBar, Integer value) {
50 scrollBar.setTrackWidth(value);
51 }
52 };
53
Winson Chungb1777442015-06-16 13:35:04 -070054 private final static int MAX_TRACK_ALPHA = 30;
55 private final static int SCROLL_BAR_VIS_DURATION = 150;
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -070056 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f;
Winson Chungb1777442015-06-16 13:35:04 -070057
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070058 private final Rect mTmpRect = new Rect();
59 private final BaseRecyclerView mRv;
Winson Chungb1777442015-06-16 13:35:04 -070060
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070061 private final boolean mIsRtl;
Winson Chungb1777442015-06-16 13:35:04 -070062
Winson Chungb1777442015-06-16 13:35:04 -070063 // 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 -070064 private final int mTouchInset;
65
66 private final int mMinWidth;
67 private final int mMaxWidth;
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -070068 private final int mThumbPadding;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070069
70 // Current width of the track
71 private int mWidth;
72 private ObjectAnimator mWidthAnimator;
73
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070074 private final Paint mThumbPaint;
75 private final int mThumbHeight;
76
77 private final Paint mTrackPaint;
78
79 private float mLastTouchY;
Winson Chungb1777442015-06-16 13:35:04 -070080 private boolean mIsDragging;
Winsond2eb49e2015-08-18 17:43:02 -070081 private boolean mIsThumbDetached;
82 private boolean mCanThumbDetach;
Winsonec4845b2015-08-27 10:19:48 -070083 private boolean mIgnoreDragGesture;
Winson Chungb1777442015-06-16 13:35:04 -070084
85 // This is the offset from the top of the scrollbar when the user first starts touching. To
86 // prevent jumping, this offset is applied as the user scrolls.
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070087 private int mTouchOffsetY;
88 private int mThumbOffsetY;
Winson Chungb1777442015-06-16 13:35:04 -070089
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -070090 // Fast scroller popup
91 private TextView mPopupView;
92 private boolean mPopupVisible;
93 private String mPopupSectionName;
Winson Chungb1777442015-06-16 13:35:04 -070094
95 public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
96 mRv = rv;
Winson Chungb1777442015-06-16 13:35:04 -070097 mTrackPaint = new Paint();
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -070098 mTrackPaint.setColor(Themes.getAttrColor(rv.getContext(), android.R.attr.textColorPrimary));
Winson67795952015-08-20 12:23:52 -070099 mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700100
Winson Chungb1777442015-06-16 13:35:04 -0700101 mThumbPaint = new Paint();
Winson67795952015-08-20 12:23:52 -0700102 mThumbPaint.setAntiAlias(true);
Sunny Goyal1f3f07d2017-02-10 16:52:16 -0800103 mThumbPaint.setColor(Themes.getColorAccent(rv.getContext()));
Winson67795952015-08-20 12:23:52 -0700104 mThumbPaint.setStyle(Paint.Style.FILL);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700105
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -0700106 mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_min_width);
107 mMaxWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_max_width);
108
109 mThumbPadding = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_padding);
110 mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height);
111
112 mTouchInset = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_touch_inset);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700113 mIsRtl = Utilities.isRtl(res);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700114 }
115
116 public void setPopupView(View popup) {
117 mPopupView = (TextView) popup;
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -0700118 mPopupView.setBackground(new FastScrollThumbDrawable(mThumbPaint, mIsRtl));
Winson Chungb1777442015-06-16 13:35:04 -0700119 }
120
Winsond2eb49e2015-08-18 17:43:02 -0700121 public void setDetachThumbOnFastScroll() {
122 mCanThumbDetach = true;
123 }
124
125 public void reattachThumbToScroll() {
126 mIsThumbDetached = false;
127 }
128
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700129 private int getDrawLeft() {
130 return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
131 }
132
133 public void setThumbOffsetY(int y) {
134 if (mThumbOffsetY == y) {
Winson Chungb1777442015-06-16 13:35:04 -0700135 return;
136 }
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700137
138 // Invalidate the previous and new thumb area
139 int drawLeft = getDrawLeft();
140 mTmpRect.set(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
141 mThumbOffsetY = y;
142 mTmpRect.union(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight);
Sunny Goyaldc19a072017-05-12 08:17:35 -0700143 mTmpRect.offset(0, mRv.getPaddingTop());
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700144 mRv.invalidate(mTmpRect);
145 }
146
147 public int getThumbOffsetY() {
148 return mThumbOffsetY;
149 }
150
151 private void setTrackWidth(int width) {
152 if (mWidth == width) {
153 return;
154 }
155 int left = getDrawLeft();
Sunny Goyaldc19a072017-05-12 08:17:35 -0700156 int top = mRv.getPaddingTop();
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700157 // Invalidate the whole scroll bar area.
Sunny Goyaldc19a072017-05-12 08:17:35 -0700158 mRv.invalidate(left, top, left + mMaxWidth, top + mRv.getScrollbarTrackHeight());
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700159
160 mWidth = width;
Winson Chungb1777442015-06-16 13:35:04 -0700161 }
162
163 public int getThumbHeight() {
164 return mThumbHeight;
165 }
166
Winsond2eb49e2015-08-18 17:43:02 -0700167 public boolean isDraggingThumb() {
Winson Chungb1777442015-06-16 13:35:04 -0700168 return mIsDragging;
169 }
170
Winsond2eb49e2015-08-18 17:43:02 -0700171 public boolean isThumbDetached() {
172 return mIsThumbDetached;
173 }
174
Winson Chungb1777442015-06-16 13:35:04 -0700175 /**
176 * Handles the touch event and determines whether to show the fast scroller (or updates it if
177 * it is already showing).
178 */
179 public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) {
180 ViewConfiguration config = ViewConfiguration.get(mRv.getContext());
181
182 int action = ev.getAction();
183 int y = (int) ev.getY();
184 switch (action) {
185 case MotionEvent.ACTION_DOWN:
Winsonec4845b2015-08-27 10:19:48 -0700186 if (isNearThumb(downX, downY)) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700187 mTouchOffsetY = downY - mThumbOffsetY;
Mario Bertschleree4ee422016-12-27 15:41:24 -0800188 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
189 && mRv.supportsFastScrolling()
190 && isNearScrollBar(downX)) {
191 calcTouchOffsetAndPrepToFastScroll(downY, lastY);
192 updateFastScrollSectionNameAndThumbOffset(lastY, y);
Winson Chungb1777442015-06-16 13:35:04 -0700193 }
194 break;
195 case MotionEvent.ACTION_MOVE:
Winsonec4845b2015-08-27 10:19:48 -0700196 // Check if we should start scrolling, but ignore this fastscroll gesture if we have
197 // exceeded some fixed movement
198 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop();
Winson646c2362015-09-03 11:46:11 -0700199 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
200 isNearThumb(downX, lastY) &&
Winson Chungb1777442015-06-16 13:35:04 -0700201 Math.abs(y - downY) > config.getScaledTouchSlop()) {
Mario Bertschleree4ee422016-12-27 15:41:24 -0800202 calcTouchOffsetAndPrepToFastScroll(downY, lastY);
Winson Chungb1777442015-06-16 13:35:04 -0700203 }
204 if (mIsDragging) {
Mario Bertschleree4ee422016-12-27 15:41:24 -0800205 updateFastScrollSectionNameAndThumbOffset(lastY, y);
Winson Chungb1777442015-06-16 13:35:04 -0700206 }
207 break;
208 case MotionEvent.ACTION_UP:
209 case MotionEvent.ACTION_CANCEL:
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700210 mTouchOffsetY = 0;
Winsond2eb49e2015-08-18 17:43:02 -0700211 mLastTouchY = 0;
Winsonec4845b2015-08-27 10:19:48 -0700212 mIgnoreDragGesture = false;
Winson Chung4c7fc622015-06-24 16:59:31 -0700213 if (mIsDragging) {
214 mIsDragging = false;
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700215 animatePopupVisibility(false);
Winsonc0880492015-08-21 11:16:27 -0700216 showActiveScrollbar(false);
Winson Chung4c7fc622015-06-24 16:59:31 -0700217 }
Winson Chungb1777442015-06-16 13:35:04 -0700218 break;
219 }
220 }
221
Mario Bertschleree4ee422016-12-27 15:41:24 -0800222 private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
223 mRv.getParent().requestDisallowInterceptTouchEvent(true);
224 mIsDragging = true;
225 if (mCanThumbDetach) {
226 mIsThumbDetached = true;
227 }
228 mTouchOffsetY += (lastY - downY);
229 animatePopupVisibility(true);
230 showActiveScrollbar(true);
231 }
232
233 private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
234 // Update the fastscroller section name at this touch position
235 int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
236 float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));
237 String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom);
238 if (!sectionName.equals(mPopupSectionName)) {
239 mPopupSectionName = sectionName;
240 mPopupView.setText(sectionName);
241 }
242 animatePopupVisibility(!sectionName.isEmpty());
243 updatePopupY(lastY);
244 mLastTouchY = boundedY;
245 setThumbOffsetY((int) mLastTouchY);
246 }
247
Winson Chungb1777442015-06-16 13:35:04 -0700248 public void draw(Canvas canvas) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700249 if (mThumbOffsetY < 0) {
Winson Chungb1777442015-06-16 13:35:04 -0700250 return;
251 }
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700252 int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
253 if (!mIsRtl) {
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -0700254 canvas.translate(mRv.getWidth() - mWidth, 0);
Winson Chungb1777442015-06-16 13:35:04 -0700255 }
Sunny Goyaldc19a072017-05-12 08:17:35 -0700256 canvas.translate(0, mRv.getPaddingTop());
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700257 // Draw the track
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -0700258 canvas.drawRoundRect(0, 0, mWidth, mRv.getScrollbarTrackHeight(),
259 mWidth, mWidth, mTrackPaint);
Winson Chungb1777442015-06-16 13:35:04 -0700260
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -0700261 canvas.translate(-mThumbPadding, mThumbOffsetY);
262 float r = mWidth + mThumbPadding + mThumbPadding;
263 canvas.drawRoundRect(0, 0, r, mThumbHeight, r, r, mThumbPaint);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700264 canvas.restoreToCount(saveCount);
Winson Chungb1777442015-06-16 13:35:04 -0700265 }
266
267 /**
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700268 * Animates the width of the scrollbar.
Winson Chungb1777442015-06-16 13:35:04 -0700269 */
Winsonc0880492015-08-21 11:16:27 -0700270 private void showActiveScrollbar(boolean isScrolling) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700271 if (mWidthAnimator != null) {
272 mWidthAnimator.cancel();
Winson Chungb1777442015-06-16 13:35:04 -0700273 }
Winson67795952015-08-20 12:23:52 -0700274
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700275 mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
276 isScrolling ? mMaxWidth : mMinWidth);
277 mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
278 mWidthAnimator.start();
Winson67795952015-08-20 12:23:52 -0700279 }
280
281 /**
Mario Bertschleree4ee422016-12-27 15:41:24 -0800282 * Returns whether the specified point is inside the thumb bounds.
Winson Chungb1777442015-06-16 13:35:04 -0700283 */
Hyunyoung Songf4cbb142016-06-10 12:00:02 -0700284 public boolean isNearThumb(int x, int y) {
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700285 int left = getDrawLeft();
286 mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
Winson Chungb1777442015-06-16 13:35:04 -0700287 mTmpRect.inset(mTouchInset, mTouchInset);
288 return mTmpRect.contains(x, y);
289 }
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700290
Mario Bertschleree4ee422016-12-27 15:41:24 -0800291 /**
292 * Returns whether the specified x position is near the scroll bar.
293 */
294 public boolean isNearScrollBar(int x) {
295 int left = getDrawLeft();
296 return x >= left && x <= left + mMaxWidth;
297 }
298
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700299 private void animatePopupVisibility(boolean visible) {
300 if (mPopupVisible != visible) {
301 mPopupVisible = visible;
302 mPopupView.animate().cancel();
303 mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start();
304 }
305 }
306
307 private void updatePopupY(int lastTouchY) {
308 int height = mPopupView.getHeight();
309 float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height);
Sunny Goyal1a8f6fb2017-06-14 15:35:16 -0700310 top = Utilities.boundToRange(top,
311 mMaxWidth, mRv.getScrollbarTrackHeight() - mMaxWidth - height);
Sunny Goyal5d9fb0e2016-10-08 17:43:48 -0700312 mPopupView.setTranslationY(top);
313 }
Winson Chungb1777442015-06-16 13:35:04 -0700314}