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