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