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 17package com.android.launcher3; 18 19import android.content.Context; 20import android.graphics.Canvas; 21import android.graphics.Rect; 22import android.support.v7.widget.RecyclerView; 23import android.util.AttributeSet; 24import android.view.MotionEvent; 25import 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 */ 35public 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 66 public BaseRecyclerView(Context context) { 67 this(context, null); 68 } 69 70 public BaseRecyclerView(Context context, AttributeSet attrs) { 71 this(context, attrs, 0); 72 } 73 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 { 84 public ScrollListener() { 85 // Do nothing 86 } 87 88 @Override 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 } 97 98 @Override 99 protected void onFinishInflate() { 100 super.onFinishInflate(); 101 addOnItemTouchListener(this); 102 } 103 104 /** 105 * We intercept the touch handling only to support fast scrolling when initiated from the 106 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. 107 */ 108 @Override 109 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { 110 return handleTouchEvent(ev); 111 } 112 113 @Override 114 public void onTouchEvent(RecyclerView rv, MotionEvent ev) { 115 handleTouchEvent(ev); 116 } 117 118 /** 119 * Handles the touch event and determines whether to show the fast scroller (or updates it if 120 * it is already showing). 121 */ 122 private boolean handleTouchEvent(MotionEvent ev) { 123 int action = ev.getAction(); 124 int x = (int) ev.getX(); 125 int y = (int) ev.getY(); 126 switch (action) { 127 case MotionEvent.ACTION_DOWN: 128 // Keep track of the down positions 129 mDownX = x; 130 mDownY = mLastY = y; 131 if (shouldStopScroll(ev)) { 132 stopScroll(); 133 } 134 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 135 break; 136 case MotionEvent.ACTION_MOVE: 137 mLastY = y; 138 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 139 break; 140 case MotionEvent.ACTION_UP: 141 case MotionEvent.ACTION_CANCEL: 142 onFastScrollCompleted(); 143 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 144 break; 145 } 146 return mScrollbar.isDragging(); 147 } 148 149 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 150 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS 151 } 152 153 /** 154 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. 155 */ 156 protected boolean shouldStopScroll(MotionEvent ev) { 157 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 158 if ((Math.abs(mDy) < mDeltaThreshold && 159 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { 160 // now the touch events are being passed to the {@link WidgetCell} until the 161 // touch sequence goes over the touch slop. 162 return true; 163 } 164 } 165 return false; 166 } 167 168 public void updateBackgroundPadding(Rect padding) { 169 mBackgroundPadding.set(padding); 170 } 171 172 public Rect getBackgroundPadding() { 173 return mBackgroundPadding; 174 } 175 176 /** 177 * Returns the scroll bar width when the user is scrolling. 178 */ 179 public int getMaxScrollbarWidth() { 180 return mScrollbar.getThumbMaxWidth(); 181 } 182 183 /** 184 * Returns the available scroll height: 185 * AvailableScrollHeight = Total height of the all items - last page height 186 * 187 * This assumes that all rows are the same height. 188 * 189 * @param yOffset the offset from the top of the recycler view to start tracking. 190 */ 191 protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) { 192 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; 193 int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom(); 194 int availableScrollHeight = scrollHeight - visibleHeight; 195 return availableScrollHeight; 196 } 197 198 /** 199 * Returns the available scroll bar height: 200 * AvailableScrollBarHeight = Total height of the visible view - thumb height 201 */ 202 protected int getAvailableScrollBarHeight() { 203 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; 204 int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); 205 return availableScrollBarHeight; 206 } 207 208 /** 209 * Returns the track color (ignoring alpha), can be overridden by each subclass. 210 */ 211 public int getFastScrollerTrackColor(int defaultTrackColor) { 212 return defaultTrackColor; 213 } 214 215 /** 216 * Returns the inactive thumb color, can be overridden by each subclass. 217 */ 218 public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { 219 return defaultInactiveThumbColor; 220 } 221 222 @Override 223 protected void dispatchDraw(Canvas canvas) { 224 super.dispatchDraw(canvas); 225 onUpdateScrollbar(); 226 mScrollbar.draw(canvas); 227 } 228 229 /** 230 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does 231 * this by mapping the available scroll area of the recycler view to the available space for the 232 * scroll bar. 233 * 234 * @param scrollPosState the current scroll position 235 * @param rowCount the number of rows, used to calculate the total scroll height (assumes that 236 * all rows are the same height) 237 * @param yOffset the offset to start tracking in the recycler view (only used for all apps) 238 */ 239 protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, 240 int rowCount, int yOffset) { 241 int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight, 242 yOffset); 243 int availableScrollBarHeight = getAvailableScrollBarHeight(); 244 245 // Only show the scrollbar if there is height to be scrolled 246 if (availableScrollHeight <= 0) { 247 mScrollbar.setScrollbarThumbOffset(-1, -1); 248 return; 249 } 250 251 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 252 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 253 // padding) 254 int scrollY = getPaddingTop() + yOffset + 255 (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; 256 int scrollBarY = mBackgroundPadding.top + 257 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 258 259 // Calculate the position and size of the scroll bar 260 int scrollBarX; 261 if (Utilities.isRtl(getResources())) { 262 scrollBarX = mBackgroundPadding.left; 263 } else { 264 scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth(); 265 } 266 mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY); 267 } 268 269 /** 270 * Maps the touch (from 0..1) to the adapter position that should be visible. 271 * <p>Override in each subclass of this base class. 272 */ 273 public abstract String scrollToPositionAtProgress(float touchFraction); 274 275 /** 276 * Updates the bounds for the scrollbar. 277 * <p>Override in each subclass of this base class. 278 */ 279 public abstract void onUpdateScrollbar(); 280 281 /** 282 * <p>Override in each subclass of this base class. 283 */ 284 public void onFastScrollCompleted() {} 285}