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.Point; 27 import android.graphics.Rect; 28 import android.view.MotionEvent; 29 import android.view.ViewConfiguration; 30 31 import com.android.launcher3.util.Thunk; 32 33 /** 34 * The track and scrollbar that shows when you scroll the list. 35 */ 36 public class BaseRecyclerViewFastScrollBar { 37 38 public interface FastScrollFocusableView { 39 void setFastScrollFocused(boolean focused, boolean animated); 40 } 41 42 private final static int MAX_TRACK_ALPHA = 30; 43 private final static int SCROLL_BAR_VIS_DURATION = 150; 44 45 @Thunk BaseRecyclerView mRv; 46 private BaseRecyclerViewFastScrollPopup mPopup; 47 48 private AnimatorSet mScrollbarAnimator; 49 50 private int mThumbInactiveColor; 51 private int mThumbActiveColor; 52 @Thunk Point mThumbOffset = new Point(-1, -1); 53 @Thunk Paint mThumbPaint; 54 private Paint mTrackPaint; 55 private int mThumbMinWidth; 56 private int mThumbMaxWidth; 57 @Thunk int mThumbWidth; 58 @Thunk int mThumbHeight; 59 // The inset is the buffer around which a point will still register as a click on the scrollbar 60 private int mTouchInset; 61 private boolean mIsDragging; 62 63 // This is the offset from the top of the scrollbar when the user first starts touching. To 64 // prevent jumping, this offset is applied as the user scrolls. 65 private int mTouchOffset; 66 67 private Rect mInvalidateRect = new Rect(); 68 private Rect mTmpRect = new Rect(); 69 70 public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) { 71 mRv = rv; 72 mPopup = new BaseRecyclerViewFastScrollPopup(rv, res); 73 mTrackPaint = new Paint(); 74 mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK)); 75 mTrackPaint.setAlpha(0); 76 mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor( 77 res.getColor(R.color.container_fastscroll_thumb_inactive_color)); 78 mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color); 79 mThumbPaint = new Paint(); 80 mThumbPaint.setColor(mThumbInactiveColor); 81 mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width); 82 mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width); 83 mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height); 84 mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset); 85 } 86 87 public void setScrollbarThumbOffset(int x, int y) { 88 if (mThumbOffset.x == x && mThumbOffset.y == y) { 89 return; 90 } 91 mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); 92 mThumbOffset.set(x, y); 93 mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, 94 mRv.getHeight())); 95 mRv.invalidate(mInvalidateRect); 96 } 97 98 // Setter/getter for the search bar width for animations 99 public void setWidth(int width) { 100 mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); 101 mThumbWidth = width; 102 mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, 103 mRv.getHeight())); 104 mRv.invalidate(mInvalidateRect); 105 } 106 107 public int getWidth() { 108 return mThumbWidth; 109 } 110 111 // Setter/getter for the track background alpha for animations 112 public void setTrackAlpha(int alpha) { 113 mTrackPaint.setAlpha(alpha); 114 mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); 115 mRv.invalidate(mInvalidateRect); 116 } 117 118 public int getTrackAlpha() { 119 return mTrackPaint.getAlpha(); 120 } 121 122 public int getThumbHeight() { 123 return mThumbHeight; 124 } 125 126 public int getThumbMaxWidth() { 127 return mThumbMaxWidth; 128 } 129 130 public boolean isDragging() { 131 return mIsDragging; 132 } 133 134 /** 135 * Handles the touch event and determines whether to show the fast scroller (or updates it if 136 * it is already showing). 137 */ 138 public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) { 139 ViewConfiguration config = ViewConfiguration.get(mRv.getContext()); 140 141 int action = ev.getAction(); 142 int y = (int) ev.getY(); 143 switch (action) { 144 case MotionEvent.ACTION_DOWN: 145 if (isNearPoint(downX, downY)) { 146 mTouchOffset = downY - mThumbOffset.y; 147 } 148 break; 149 case MotionEvent.ACTION_MOVE: 150 // Check if we should start scrolling 151 if (!mIsDragging && isNearPoint(downX, downY) && 152 Math.abs(y - downY) > config.getScaledTouchSlop()) { 153 mRv.getParent().requestDisallowInterceptTouchEvent(true); 154 mIsDragging = true; 155 mTouchOffset += (lastY - downY); 156 mPopup.animateVisibility(true); 157 animateScrollbar(true); 158 } 159 if (mIsDragging) { 160 // Update the fastscroller section name at this touch position 161 int top = mRv.getBackgroundPadding().top; 162 int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight; 163 float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset)); 164 String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) / 165 (bottom - top)); 166 mPopup.setSectionName(sectionName); 167 mPopup.animateVisibility(!sectionName.isEmpty()); 168 mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); 169 } 170 break; 171 case MotionEvent.ACTION_UP: 172 case MotionEvent.ACTION_CANCEL: 173 mTouchOffset = 0; 174 if (mIsDragging) { 175 mIsDragging = false; 176 mPopup.animateVisibility(false); 177 animateScrollbar(false); 178 } 179 break; 180 } 181 } 182 183 public void draw(Canvas canvas) { 184 if (mThumbOffset.x < 0 || mThumbOffset.y < 0) { 185 return; 186 } 187 188 // Draw the scroll bar track and thumb 189 if (mTrackPaint.getAlpha() > 0) { 190 canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint); 191 } 192 canvas.drawRect(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, 193 mThumbOffset.y + mThumbHeight, mThumbPaint); 194 195 // Draw the popup 196 mPopup.draw(canvas); 197 } 198 199 /** 200 * Animates the width and color of the scrollbar. 201 */ 202 private void animateScrollbar(boolean isScrolling) { 203 if (mScrollbarAnimator != null) { 204 mScrollbarAnimator.cancel(); 205 } 206 ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha", 207 isScrolling ? MAX_TRACK_ALPHA : 0); 208 ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width", 209 isScrolling ? mThumbMaxWidth : mThumbMinWidth); 210 ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), 211 mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor); 212 colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 213 @Override 214 public void onAnimationUpdate(ValueAnimator animator) { 215 mThumbPaint.setColor((Integer) animator.getAnimatedValue()); 216 mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, 217 mThumbOffset.y + mThumbHeight); 218 } 219 }); 220 mScrollbarAnimator = new AnimatorSet(); 221 mScrollbarAnimator.playTogether(trackAlphaAnim, thumbWidthAnim, colorAnimation); 222 mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION); 223 mScrollbarAnimator.start(); 224 } 225 226 /** 227 * Returns whether the specified points are near the scroll bar bounds. 228 */ 229 private boolean isNearPoint(int x, int y) { 230 mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, 231 mThumbOffset.y + mThumbHeight); 232 mTmpRect.inset(mTouchInset, mTouchInset); 233 return mTmpRect.contains(x, y); 234 } 235 } 236