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 height of a given row (they are currently all the same height) 56 public int rowHeight; 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 available scroll height: 191 * AvailableScrollHeight = Total height of the all items - last page height 192 * 193 * This assumes that all rows are the same height. 194 */ getAvailableScrollHeight(int rowCount, int rowHeight)195 protected int getAvailableScrollHeight(int rowCount, int rowHeight) { 196 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; 197 int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom(); 198 int availableScrollHeight = scrollHeight - visibleHeight; 199 return availableScrollHeight; 200 } 201 202 /** 203 * Returns the available scroll bar height: 204 * AvailableScrollBarHeight = Total height of the visible view - thumb height 205 */ getAvailableScrollBarHeight()206 protected int getAvailableScrollBarHeight() { 207 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; 208 int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); 209 return availableScrollBarHeight; 210 } 211 212 /** 213 * Returns the track color (ignoring alpha), can be overridden by each subclass. 214 */ getFastScrollerTrackColor(int defaultTrackColor)215 public int getFastScrollerTrackColor(int defaultTrackColor) { 216 return defaultTrackColor; 217 } 218 219 /** 220 * Returns the inactive thumb color, can be overridden by each subclass. 221 */ getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor)222 public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { 223 return defaultInactiveThumbColor; 224 } 225 226 @Override dispatchDraw(Canvas canvas)227 protected void dispatchDraw(Canvas canvas) { 228 super.dispatchDraw(canvas); 229 onUpdateScrollbar(0); 230 mScrollbar.draw(canvas); 231 } 232 233 /** 234 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does 235 * this by mapping the available scroll area of the recycler view to the available space for the 236 * scroll bar. 237 * 238 * @param scrollPosState the current scroll position 239 * @param rowCount the number of rows, used to calculate the total scroll height (assumes that 240 * all rows are the same height) 241 */ synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, int rowCount)242 protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, 243 int rowCount) { 244 // Only show the scrollbar if there is height to be scrolled 245 int availableScrollBarHeight = getAvailableScrollBarHeight(); 246 int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); 247 if (availableScrollHeight <= 0) { 248 mScrollbar.setThumbOffset(-1, -1); 249 return; 250 } 251 252 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 253 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 254 // padding) 255 int scrollY = getPaddingTop() + 256 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; 257 int scrollBarY = mBackgroundPadding.top + 258 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 259 260 // Calculate the position and size of the scroll bar 261 int scrollBarX; 262 if (Utilities.isRtl(getResources())) { 263 scrollBarX = mBackgroundPadding.left; 264 } else { 265 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); 266 } 267 mScrollbar.setThumbOffset(scrollBarX, scrollBarY); 268 } 269 270 /** 271 * Maps the touch (from 0..1) to the adapter position that should be visible. 272 * <p>Override in each subclass of this base class. 273 */ scrollToPositionAtProgress(float touchFraction)274 public abstract String scrollToPositionAtProgress(float touchFraction); 275 276 /** 277 * Updates the bounds for the scrollbar. 278 * <p>Override in each subclass of this base class. 279 */ onUpdateScrollbar(int dy)280 public abstract void onUpdateScrollbar(int dy); 281 282 /** 283 * <p>Override in each subclass of this base class. 284 */ onFastScrollCompleted()285 public void onFastScrollCompleted() {} 286 287 /** 288 * Returns information about the item that the recycler view is currently scrolled to. 289 */ getCurScrollState(ScrollPositionState stateOut)290 protected abstract void getCurScrollState(ScrollPositionState stateOut); 291 }