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.ObjectAnimator; 19 import android.content.res.Resources; 20 import android.graphics.Canvas; 21 import android.graphics.Color; 22 import android.graphics.Paint; 23 import android.graphics.Path; 24 import android.graphics.Rect; 25 import android.util.Property; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewConfiguration; 29 import android.widget.TextView; 30 31 import com.android.launcher3.config.FeatureFlags; 32 import com.android.launcher3.util.Themes; 33 34 /** 35 * The track and scrollbar that shows when you scroll the list. 36 */ 37 public class BaseRecyclerViewFastScrollBar { 38 39 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 53 private final static int MAX_TRACK_ALPHA = 30; 54 private final static int SCROLL_BAR_VIS_DURATION = 150; 55 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f; 56 57 private final Rect mTmpRect = new Rect(); 58 private final BaseRecyclerView mRv; 59 60 private final boolean mIsRtl; 61 62 // The inset is the buffer around which a point will still register as a click on the scrollbar 63 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; 79 private boolean mIsDragging; 80 private boolean mIsThumbDetached; 81 private boolean mCanThumbDetach; 82 private boolean mIgnoreDragGesture; 83 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. 86 private int mTouchOffsetY; 87 private int mThumbOffsetY; 88 89 // Fast scroller popup 90 private TextView mPopupView; 91 private boolean mPopupVisible; 92 private String mPopupSectionName; 93 94 public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) { 95 mRv = rv; 96 mTrackPaint = new Paint(); 97 mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK)); 98 mTrackPaint.setAlpha(MAX_TRACK_ALPHA); 99 100 mThumbPaint = new Paint(); 101 mThumbPaint.setAntiAlias(true); 102 mThumbPaint.setColor(Themes.getColorAccent(rv.getContext())); 103 mThumbPaint.setStyle(Paint.Style.FILL); 104 105 mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width); 106 mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width); 107 mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height); 108 mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset); 109 mIsRtl = Utilities.isRtl(res); 110 updateThumbPath(); 111 } 112 113 public void setPopupView(View popup) { 114 mPopupView = (TextView) popup; 115 } 116 117 public void setDetachThumbOnFastScroll() { 118 mCanThumbDetach = true; 119 } 120 121 public void reattachThumbToScroll() { 122 mIsThumbDetached = false; 123 } 124 125 private int getDrawLeft() { 126 return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth); 127 } 128 129 public void setThumbOffsetY(int y) { 130 if (mThumbOffsetY == y) { 131 return; 132 } 133 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); 139 mRv.invalidate(mTmpRect); 140 } 141 142 public int getThumbOffsetY() { 143 return mThumbOffsetY; 144 } 145 146 private void setTrackWidth(int width) { 147 if (mWidth == width) { 148 return; 149 } 150 int left = getDrawLeft(); 151 // Invalidate the whole scroll bar area. 152 mRv.invalidate(left, 0, left + mMaxWidth, mRv.getScrollbarTrackHeight()); 153 154 mWidth = width; 155 updateThumbPath(); 156 } 157 158 /** 159 * Updates the path for the thumb drawable. 160 */ 161 private void updateThumbPath() { 162 int smallWidth = mIsRtl ? mWidth : -mWidth; 163 int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth; 164 165 mThumbPath.reset(); 166 mThumbPath.moveTo(0, 0); 167 mThumbPath.lineTo(0, mThumbHeight); // Left edge 168 mThumbPath.lineTo(smallWidth, mThumbHeight); // bottom edge 169 mThumbPath.cubicTo(smallWidth, mThumbHeight, // right edge 170 largeWidth, mThumbHeight / 2, 171 smallWidth, 0); 172 mThumbPath.close(); 173 } 174 175 public int getThumbHeight() { 176 return mThumbHeight; 177 } 178 179 public boolean isDraggingThumb() { 180 return mIsDragging; 181 } 182 183 public boolean isThumbDetached() { 184 return mIsThumbDetached; 185 } 186 187 /** 188 * Handles the touch event and determines whether to show the fast scroller (or updates it if 189 * it is already showing). 190 */ 191 public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) { 192 ViewConfiguration config = ViewConfiguration.get(mRv.getContext()); 193 194 int action = ev.getAction(); 195 int y = (int) ev.getY(); 196 switch (action) { 197 case MotionEvent.ACTION_DOWN: 198 if (isNearThumb(downX, downY)) { 199 mTouchOffsetY = downY - mThumbOffsetY; 200 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL 201 && mRv.supportsFastScrolling() 202 && isNearScrollBar(downX)) { 203 calcTouchOffsetAndPrepToFastScroll(downY, lastY); 204 updateFastScrollSectionNameAndThumbOffset(lastY, y); 205 } 206 break; 207 case MotionEvent.ACTION_MOVE: 208 // Check if we should start scrolling, but ignore this fastscroll gesture if we have 209 // exceeded some fixed movement 210 mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop(); 211 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() && 212 isNearThumb(downX, lastY) && 213 Math.abs(y - downY) > config.getScaledTouchSlop()) { 214 calcTouchOffsetAndPrepToFastScroll(downY, lastY); 215 } 216 if (mIsDragging) { 217 updateFastScrollSectionNameAndThumbOffset(lastY, y); 218 } 219 break; 220 case MotionEvent.ACTION_UP: 221 case MotionEvent.ACTION_CANCEL: 222 mTouchOffsetY = 0; 223 mLastTouchY = 0; 224 mIgnoreDragGesture = false; 225 if (mIsDragging) { 226 mIsDragging = false; 227 animatePopupVisibility(false); 228 showActiveScrollbar(false); 229 } 230 break; 231 } 232 } 233 234 private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) { 235 mRv.getParent().requestDisallowInterceptTouchEvent(true); 236 mIsDragging = true; 237 if (mCanThumbDetach) { 238 mIsThumbDetached = true; 239 } 240 mTouchOffsetY += (lastY - downY); 241 animatePopupVisibility(true); 242 showActiveScrollbar(true); 243 } 244 245 private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) { 246 // Update the fastscroller section name at this touch position 247 int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight; 248 float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY)); 249 String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom); 250 if (!sectionName.equals(mPopupSectionName)) { 251 mPopupSectionName = sectionName; 252 mPopupView.setText(sectionName); 253 } 254 animatePopupVisibility(!sectionName.isEmpty()); 255 updatePopupY(lastY); 256 mLastTouchY = boundedY; 257 setThumbOffsetY((int) mLastTouchY); 258 } 259 260 public void draw(Canvas canvas) { 261 if (mThumbOffsetY < 0) { 262 return; 263 } 264 int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); 265 if (!mIsRtl) { 266 canvas.translate(mRv.getWidth(), 0); 267 } 268 // Draw the track 269 int thumbWidth = mIsRtl ? mWidth : -mWidth; 270 canvas.drawRect(0, 0, thumbWidth, mRv.getScrollbarTrackHeight(), mTrackPaint); 271 272 canvas.translate(0, mThumbOffsetY); 273 canvas.drawPath(mThumbPath, mThumbPaint); 274 canvas.restoreToCount(saveCount); 275 } 276 277 /** 278 * Animates the width of the scrollbar. 279 */ 280 private void showActiveScrollbar(boolean isScrolling) { 281 if (mWidthAnimator != null) { 282 mWidthAnimator.cancel(); 283 } 284 285 mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH, 286 isScrolling ? mMaxWidth : mMinWidth); 287 mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION); 288 mWidthAnimator.start(); 289 } 290 291 /** 292 * Returns whether the specified point is inside the thumb bounds. 293 */ 294 public boolean isNearThumb(int x, int y) { 295 int left = getDrawLeft(); 296 mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight); 297 mTmpRect.inset(mTouchInset, mTouchInset); 298 return mTmpRect.contains(x, y); 299 } 300 301 /** 302 * Returns whether the specified x position is near the scroll bar. 303 */ 304 public boolean isNearScrollBar(int x) { 305 int left = getDrawLeft(); 306 return x >= left && x <= left + mMaxWidth; 307 } 308 309 private void animatePopupVisibility(boolean visible) { 310 if (mPopupVisible != visible) { 311 mPopupVisible = visible; 312 mPopupView.animate().cancel(); 313 mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start(); 314 } 315 } 316 317 private void updatePopupY(int lastTouchY) { 318 int height = mPopupView.getHeight(); 319 float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height); 320 top = Math.max(mMaxWidth, Math.min(top, mRv.getScrollbarTrackHeight() - mMaxWidth - height)); 321 mPopupView.setTranslationY(top); 322 } 323 } 324