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 17 package com.android.launcher3; 18 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.graphics.Rect; 22 import android.support.v7.widget.RecyclerView; 23 import android.util.AttributeSet; 24 import android.view.MotionEvent; 25 import com.android.launcher3.util.Thunk; 26 27 28 /** 29 * A base {@link RecyclerView}, which does the following: 30 * <ul> 31 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold. 32 * <li> Enable fast scroller. 33 * </ul> 34 */ 35 public abstract class BaseRecyclerView extends RecyclerView 36 implements RecyclerView.OnItemTouchListener { 37 38 private static final int SCROLL_DELTA_THRESHOLD_DP = 4; 39 40 /** Keeps the last known scrolling delta/velocity along y-axis. */ 41 @Thunk int mDy = 0; 42 private float mDeltaThreshold; 43 44 /** 45 * The current scroll state of the recycler view. We use this in onUpdateScrollbar() 46 * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so 47 * that we can calculate what the scroll bar looks like, and where to jump to from the fast 48 * scroller. 49 */ 50 public static class ScrollPositionState { 51 // The index of the first visible row 52 public int rowIndex; 53 // The offset of the first visible row 54 public int rowTopOffset; 55 // The adapter position of the first visible item 56 public int itemPos; 57 } 58 59 protected BaseRecyclerViewFastScrollBar mScrollbar; 60 61 private int mDownX; 62 private int mDownY; 63 private int mLastY; 64 protected Rect mBackgroundPadding = new Rect(); 65 BaseRecyclerView(Context context)66 public BaseRecyclerView(Context context) { 67 this(context, null); 68 } 69 BaseRecyclerView(Context context, AttributeSet attrs)70 public BaseRecyclerView(Context context, AttributeSet attrs) { 71 this(context, attrs, 0); 72 } 73 BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)74 public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 75 super(context, attrs, defStyleAttr); 76 mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; 77 mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); 78 79 ScrollListener listener = new ScrollListener(); 80 setOnScrollListener(listener); 81 } 82 83 private class ScrollListener extends OnScrollListener { ScrollListener()84 public ScrollListener() { 85 // Do nothing 86 } 87 88 @Override onScrolled(RecyclerView recyclerView, int dx, int dy)89 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 90 mDy = dy; 91 92 // TODO(winsonc): If we want to animate the section heads while scrolling, we can 93 // initiate that here if the recycler view scroll state is not 94 // RecyclerView.SCROLL_STATE_IDLE. 95 96 onUpdateScrollbar(dy); 97 } 98 } 99 reset()100 public void reset() { 101 mScrollbar.reattachThumbToScroll(); 102 } 103 104 @Override onFinishInflate()105 protected void onFinishInflate() { 106 super.onFinishInflate(); 107 addOnItemTouchListener(this); 108 } 109 110 /** 111 * We intercept the touch handling only to support fast scrolling when initiated from the 112 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. 113 */ 114 @Override onInterceptTouchEvent(RecyclerView rv, MotionEvent ev)115 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { 116 return handleTouchEvent(ev); 117 } 118 119 @Override onTouchEvent(RecyclerView rv, MotionEvent ev)120 public void onTouchEvent(RecyclerView rv, MotionEvent ev) { 121 handleTouchEvent(ev); 122 } 123 124 /** 125 * Handles the touch event and determines whether to show the fast scroller (or updates it if 126 * it is already showing). 127 */ handleTouchEvent(MotionEvent ev)128 private boolean handleTouchEvent(MotionEvent ev) { 129 int action = ev.getAction(); 130 int x = (int) ev.getX(); 131 int y = (int) ev.getY(); 132 switch (action) { 133 case MotionEvent.ACTION_DOWN: 134 // Keep track of the down positions 135 mDownX = x; 136 mDownY = mLastY = y; 137 if (shouldStopScroll(ev)) { 138 stopScroll(); 139 } 140 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 141 break; 142 case MotionEvent.ACTION_MOVE: 143 mLastY = y; 144 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 145 break; 146 case MotionEvent.ACTION_UP: 147 case MotionEvent.ACTION_CANCEL: 148 onFastScrollCompleted(); 149 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 150 break; 151 } 152 return mScrollbar.isDraggingThumb(); 153 } 154 onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)155 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 156 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS 157 } 158 159 /** 160 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. 161 */ shouldStopScroll(MotionEvent ev)162 protected boolean shouldStopScroll(MotionEvent ev) { 163 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 164 if ((Math.abs(mDy) < mDeltaThreshold && 165 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { 166 // now the touch events are being passed to the {@link WidgetCell} until the 167 // touch sequence goes over the touch slop. 168 return true; 169 } 170 } 171 return false; 172 } 173 updateBackgroundPadding(Rect padding)174 public void updateBackgroundPadding(Rect padding) { 175 mBackgroundPadding.set(padding); 176 } 177 getBackgroundPadding()178 public Rect getBackgroundPadding() { 179 return mBackgroundPadding; 180 } 181 182 /** 183 * Returns the scroll bar width when the user is scrolling. 184 */ getMaxScrollbarWidth()185 public int getMaxScrollbarWidth() { 186 return mScrollbar.getThumbMaxWidth(); 187 } 188 189 /** 190 * Returns the visible height of the recycler view: 191 * VisibleHeight = View height - top padding - bottom padding 192 */ getVisibleHeight()193 protected int getVisibleHeight() { 194 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; 195 return visibleHeight; 196 } 197 198 /** 199 * Returns the available scroll height: 200 * AvailableScrollHeight = Total height of the all items - last page height 201 */ getAvailableScrollHeight(int rowCount)202 protected int getAvailableScrollHeight(int rowCount) { 203 int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom(); 204 int availableScrollHeight = totalHeight - getVisibleHeight(); 205 return availableScrollHeight; 206 } 207 208 /** 209 * Returns the available scroll bar height: 210 * AvailableScrollBarHeight = Total height of the visible view - thumb height 211 */ getAvailableScrollBarHeight()212 protected int getAvailableScrollBarHeight() { 213 int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight(); 214 return availableScrollBarHeight; 215 } 216 217 /** 218 * Returns the track color (ignoring alpha), can be overridden by each subclass. 219 */ getFastScrollerTrackColor(int defaultTrackColor)220 public int getFastScrollerTrackColor(int defaultTrackColor) { 221 return defaultTrackColor; 222 } 223 224 /** 225 * Returns the inactive thumb color, can be overridden by each subclass. 226 */ getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor)227 public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { 228 return defaultInactiveThumbColor; 229 } 230 231 /** 232 * Returns the scrollbar for this recycler view. 233 */ getScrollBar()234 public BaseRecyclerViewFastScrollBar getScrollBar() { 235 return mScrollbar; 236 } 237 238 @Override dispatchDraw(Canvas canvas)239 protected void dispatchDraw(Canvas canvas) { 240 super.dispatchDraw(canvas); 241 onUpdateScrollbar(0); 242 mScrollbar.draw(canvas); 243 } 244 245 /** 246 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does 247 * this by mapping the available scroll area of the recycler view to the available space for the 248 * scroll bar. 249 * 250 * @param scrollPosState the current scroll position 251 * @param rowCount the number of rows, used to calculate the total scroll height (assumes that 252 * all rows are the same height) 253 */ synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount)254 protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, 255 int rowCount) { 256 // Only show the scrollbar if there is height to be scrolled 257 int availableScrollBarHeight = getAvailableScrollBarHeight(); 258 int availableScrollHeight = getAvailableScrollHeight(rowCount); 259 if (availableScrollHeight <= 0) { 260 mScrollbar.setThumbOffset(-1, -1); 261 return; 262 } 263 264 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 265 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 266 // padding) 267 int scrollY = getScrollTop(scrollPosState); 268 int scrollBarY = mBackgroundPadding.top + 269 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 270 271 // Calculate the position and size of the scroll bar 272 int scrollBarX; 273 if (Utilities.isRtl(getResources())) { 274 scrollBarX = mBackgroundPadding.left; 275 } else { 276 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); 277 } 278 mScrollbar.setThumbOffset(scrollBarX, scrollBarY); 279 } 280 281 /** 282 * @return whether fast scrolling is supported in the current state. 283 */ supportsFastScrolling()284 protected boolean supportsFastScrolling() { 285 return true; 286 } 287 288 /** 289 * Maps the touch (from 0..1) to the adapter position that should be visible. 290 * <p>Override in each subclass of this base class. 291 * 292 * @return the scroll top of this recycler view. 293 */ getScrollTop(ScrollPositionState scrollPosState)294 protected int getScrollTop(ScrollPositionState scrollPosState) { 295 return getPaddingTop() + getTop(scrollPosState.rowIndex) - 296 scrollPosState.rowTopOffset; 297 } 298 299 /** 300 * Returns information about the item that the recycler view is currently scrolled to. 301 */ getCurScrollState(ScrollPositionState stateOut, int viewTypeMask)302 protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask); 303 304 /** 305 * Returns the top (or y position) of the row at the specified index. 306 */ getTop(int rowIndex)307 protected abstract int getTop(int rowIndex); 308 309 /** 310 * Maps the touch (from 0..1) to the adapter position that should be visible. 311 * <p>Override in each subclass of this base class. 312 */ scrollToPositionAtProgress(float touchFraction)313 protected abstract String scrollToPositionAtProgress(float touchFraction); 314 315 /** 316 * Updates the bounds for the scrollbar. 317 * <p>Override in each subclass of this base class. 318 */ onUpdateScrollbar(int dy)319 protected abstract void onUpdateScrollbar(int dy); 320 321 /** 322 * <p>Override in each subclass of this base class. 323 */ onFastScrollCompleted()324 protected void onFastScrollCompleted() {} 325 }