1 /* 2 * Copyright (C) 2017 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 17 package com.android.launcher3.views; 18 19 import android.animation.ObjectAnimator; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.Paint; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.support.v7.widget.RecyclerView; 28 import android.util.AttributeSet; 29 import android.util.Property; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 import android.widget.TextView; 34 35 import com.android.launcher3.BaseRecyclerView; 36 import com.android.launcher3.R; 37 import com.android.launcher3.Utilities; 38 import com.android.launcher3.config.FeatureFlags; 39 import com.android.launcher3.graphics.FastScrollThumbDrawable; 40 import com.android.launcher3.util.Themes; 41 42 /** 43 * The track and scrollbar that shows when you scroll the list. 44 */ 45 public class RecyclerViewFastScroller extends View { 46 47 private static final int SCROLL_DELTA_THRESHOLD_DP = 4; 48 private static final Rect sTempRect = new Rect(); 49 50 private static final Property<RecyclerViewFastScroller, Integer> TRACK_WIDTH = 51 new Property<RecyclerViewFastScroller, Integer>(Integer.class, "width") { 52 53 @Override 54 public Integer get(RecyclerViewFastScroller scrollBar) { 55 return scrollBar.mWidth; 56 } 57 58 @Override 59 public void set(RecyclerViewFastScroller scrollBar, Integer value) { 60 scrollBar.setTrackWidth(value); 61 } 62 }; 63 64 private final static int MAX_TRACK_ALPHA = 30; 65 private final static int SCROLL_BAR_VIS_DURATION = 150; 66 private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f; 67 68 private final int mMinWidth; 69 private final int mMaxWidth; 70 private final int mThumbPadding; 71 72 /** Keeps the last known scrolling delta/velocity along y-axis. */ 73 private int mDy = 0; 74 private final float mDeltaThreshold; 75 76 private final ViewConfiguration mConfig; 77 78 // Current width of the track 79 private int mWidth; 80 private ObjectAnimator mWidthAnimator; 81 82 private final Paint mThumbPaint; 83 protected final int mThumbHeight; 84 85 private final Paint mTrackPaint; 86 87 private float mLastTouchY; 88 private boolean mIsDragging; 89 private boolean mIsThumbDetached; 90 private final boolean mCanThumbDetach; 91 private boolean mIgnoreDragGesture; 92 93 // This is the offset from the top of the scrollbar when the user first starts touching. To 94 // prevent jumping, this offset is applied as the user scrolls. 95 protected int mTouchOffsetY; 96 protected int mThumbOffsetY; 97 98 // Fast scroller popup 99 private TextView mPopupView; 100 private boolean mPopupVisible; 101 private String mPopupSectionName; 102 103 protected BaseRecyclerView mRv; 104 private RecyclerView.OnScrollListener mOnScrollListener; 105 106 private int mDownX; 107 private int mDownY; 108 private int mLastY; 109 110 public RecyclerViewFastScroller(Context context) { 111 this(context, null); 112 } 113 114 public RecyclerViewFastScroller(Context context, AttributeSet attrs) { 115 this(context, attrs, 0); 116 } 117 118 public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) { 119 super(context, attrs, defStyleAttr); 120 121 mTrackPaint = new Paint(); 122 mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); 123 mTrackPaint.setAlpha(MAX_TRACK_ALPHA); 124 125 mThumbPaint = new Paint(); 126 mThumbPaint.setAntiAlias(true); 127 mThumbPaint.setColor(Themes.getColorAccent(context)); 128 mThumbPaint.setStyle(Paint.Style.FILL); 129 130 Resources res = getResources(); 131 mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_min_width); 132 mMaxWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_max_width); 133 134 mThumbPadding = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_padding); 135 mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height); 136 137 mConfig = ViewConfiguration.get(context); 138 mDeltaThreshold = res.getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; 139 140 TypedArray ta = 141 context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewFastScroller, defStyleAttr, 0); 142 mCanThumbDetach = ta.getBoolean(R.styleable.RecyclerViewFastScroller_canThumbDetach, false); 143 ta.recycle(); 144 } 145 146 public void setRecyclerView(BaseRecyclerView rv, TextView popupView) { 147 if (mRv != null && mOnScrollListener != null) { 148 mRv.removeOnScrollListener(mOnScrollListener); 149 } 150 mRv = rv; 151 152 mRv.addOnScrollListener(mOnScrollListener = new RecyclerView.OnScrollListener() { 153 @Override 154 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 155 mDy = dy; 156 157 // TODO(winsonc): If we want to animate the section heads while scrolling, we can 158 // initiate that here if the recycler view scroll state is not 159 // RecyclerView.SCROLL_STATE_IDLE. 160 161 mRv.onUpdateScrollbar(dy); 162 } 163 }); 164 165 mPopupView = popupView; 166 mPopupView.setBackground( 167 new FastScrollThumbDrawable(mThumbPaint, Utilities.isRtl(getResources()))); 168 } 169 170 public void reattachThumbToScroll() { 171 mIsThumbDetached = false; 172 } 173 174 public void setThumbOffsetY(int y) { 175 if (mThumbOffsetY == y) { 176 return; 177 } 178 mThumbOffsetY = y; 179 invalidate(); 180 } 181 182 public int getThumbOffsetY() { 183 return mThumbOffsetY; 184 } 185 186 private void setTrackWidth(int width) { 187 if (mWidth == width) { 188 return; 189 } 190 mWidth = width; 191 invalidate(); 192 } 193 194 public int getThumbHeight() { 195 return mThumbHeight; 196 } 197 198 public boolean isDraggingThumb() { 199 return mIsDragging; 200 } 201 202 public boolean isThumbDetached() { 203 return mIsThumbDetached; 204 } 205 206 /** 207 * Handles the touch event and determines whether to show the fast scroller (or updates it if 208 * it is already showing). 209 */ 210 public boolean handleTouchEvent(MotionEvent ev, Point offset) { 211 int x = (int) ev.getX() - offset.x; 212 int y = (int) ev.getY() - offset.y; 213 switch (ev.getAction()) { 214 case MotionEvent.ACTION_DOWN: 215 // Keep track of the down positions 216 mDownX = x; 217 mDownY = mLastY = y; 218 219 if ((Math.abs(mDy) < mDeltaThreshold && 220 mRv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { 221 // now the touch events are being passed to the {@link WidgetCell} until the 222 // touch sequence goes over the touch slop. 223 mRv.stopScroll(); 224 } 225 if (isNearThumb(x, y)) { 226 mTouchOffsetY = mDownY - mThumbOffsetY; 227 } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL 228 && mRv.supportsFastScrolling() 229 && isNearScrollBar(mDownX)) { 230 calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY); 231 updateFastScrollSectionNameAndThumbOffset(mLastY, y); 232 } 233 break; 234 case MotionEvent.ACTION_MOVE: 235 mLastY = y; 236 237 // Check if we should start scrolling, but ignore this fastscroll gesture if we have 238 // exceeded some fixed movement 239 mIgnoreDragGesture |= Math.abs(y - mDownY) > mConfig.getScaledPagingTouchSlop(); 240 if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() && 241 isNearThumb(mDownX, mLastY) && 242 Math.abs(y - mDownY) > mConfig.getScaledTouchSlop()) { 243 calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY); 244 } 245 if (mIsDragging) { 246 updateFastScrollSectionNameAndThumbOffset(mLastY, y); 247 } 248 break; 249 case MotionEvent.ACTION_UP: 250 case MotionEvent.ACTION_CANCEL: 251 mRv.onFastScrollCompleted(); 252 mTouchOffsetY = 0; 253 mLastTouchY = 0; 254 mIgnoreDragGesture = false; 255 if (mIsDragging) { 256 mIsDragging = false; 257 animatePopupVisibility(false); 258 showActiveScrollbar(false); 259 } 260 break; 261 } 262 return mIsDragging; 263 } 264 265 private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) { 266 mIsDragging = true; 267 if (mCanThumbDetach) { 268 mIsThumbDetached = true; 269 } 270 mTouchOffsetY += (lastY - downY); 271 animatePopupVisibility(true); 272 showActiveScrollbar(true); 273 } 274 275 private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) { 276 // Update the fastscroller section name at this touch position 277 int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight; 278 float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY)); 279 String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom); 280 if (!sectionName.equals(mPopupSectionName)) { 281 mPopupSectionName = sectionName; 282 mPopupView.setText(sectionName); 283 } 284 animatePopupVisibility(!sectionName.isEmpty()); 285 updatePopupY(lastY); 286 mLastTouchY = boundedY; 287 setThumbOffsetY((int) mLastTouchY); 288 } 289 290 public void onDraw(Canvas canvas) { 291 if (mThumbOffsetY < 0) { 292 return; 293 } 294 int saveCount = canvas.save(); 295 canvas.translate(getWidth() / 2, mRv.getScrollBarTop()); 296 // Draw the track 297 float halfW = mWidth / 2; 298 canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(), 299 mWidth, mWidth, mTrackPaint); 300 301 canvas.translate(0, mThumbOffsetY); 302 halfW += mThumbPadding; 303 float r = mWidth + mThumbPadding + mThumbPadding; 304 canvas.drawRoundRect(-halfW, 0, halfW, mThumbHeight, r, r, mThumbPaint); 305 canvas.restoreToCount(saveCount); 306 } 307 308 309 /** 310 * Animates the width of the scrollbar. 311 */ 312 private void showActiveScrollbar(boolean isScrolling) { 313 if (mWidthAnimator != null) { 314 mWidthAnimator.cancel(); 315 } 316 317 mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH, 318 isScrolling ? mMaxWidth : mMinWidth); 319 mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION); 320 mWidthAnimator.start(); 321 } 322 323 /** 324 * Returns whether the specified point is inside the thumb bounds. 325 */ 326 private boolean isNearThumb(int x, int y) { 327 int offset = y - mThumbOffsetY; 328 329 return x >= 0 && x < getWidth() && offset >= 0 && offset <= mThumbHeight; 330 } 331 332 /** 333 * Returns true if AllAppsTransitionController can handle vertical motion 334 * beginning at this point. 335 */ 336 public boolean shouldBlockIntercept(int x, int y) { 337 return isNearThumb(x, y); 338 } 339 340 /** 341 * Returns whether the specified x position is near the scroll bar. 342 */ 343 public boolean isNearScrollBar(int x) { 344 return x >= (getWidth() - mMaxWidth) / 2 && x <= (getWidth() + mMaxWidth) / 2; 345 } 346 347 private void animatePopupVisibility(boolean visible) { 348 if (mPopupVisible != visible) { 349 mPopupVisible = visible; 350 mPopupView.animate().cancel(); 351 mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start(); 352 } 353 } 354 355 private void updatePopupY(int lastTouchY) { 356 int height = mPopupView.getHeight(); 357 float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height) 358 + mRv.getScrollBarTop(); 359 top = Utilities.boundToRange(top, 360 mMaxWidth, mRv.getScrollbarTrackHeight() - mMaxWidth - height); 361 mPopupView.setTranslationY(top); 362 } 363 364 public boolean isHitInParent(float x, float y, Point outOffset) { 365 if (mThumbOffsetY < 0) { 366 return false; 367 } 368 getHitRect(sTempRect); 369 sTempRect.top += mRv.getScrollBarTop(); 370 if (outOffset != null) { 371 outOffset.set(sTempRect.left, sTempRect.top); 372 } 373 return sTempRect.contains((int) x, (int) y); 374 } 375 376 @Override 377 public boolean hasOverlappingRendering() { 378 // There is actually some overlap between the track and the thumb. But since the track 379 // alpha is so low, it does not matter. 380 return false; 381 } 382 } 383