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