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