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 protected BaseRecyclerViewFastScrollBar mScrollbar; 45 46 private int mDownX; 47 private int mDownY; 48 private int mLastY; 49 protected Rect mBackgroundPadding = new Rect(); 50 51 public BaseRecyclerView(Context context) { 52 this(context, null); 53 } 54 55 public BaseRecyclerView(Context context, AttributeSet attrs) { 56 this(context, attrs, 0); 57 } 58 59 public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 60 super(context, attrs, defStyleAttr); 61 mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; 62 mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); 63 64 ScrollListener listener = new ScrollListener(); 65 setOnScrollListener(listener); 66 } 67 68 private class ScrollListener extends OnScrollListener { 69 public ScrollListener() { 70 // Do nothing 71 } 72 73 @Override 74 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 75 mDy = dy; 76 77 // TODO(winsonc): If we want to animate the section heads while scrolling, we can 78 // initiate that here if the recycler view scroll state is not 79 // RecyclerView.SCROLL_STATE_IDLE. 80 81 onUpdateScrollbar(dy); 82 } 83 } 84 85 public void reset() { 86 mScrollbar.reattachThumbToScroll(); 87 } 88 89 @Override 90 protected void onFinishInflate() { 91 super.onFinishInflate(); 92 addOnItemTouchListener(this); 93 } 94 95 /** 96 * We intercept the touch handling only to support fast scrolling when initiated from the 97 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. 98 */ 99 @Override 100 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { 101 return handleTouchEvent(ev); 102 } 103 104 @Override 105 public void onTouchEvent(RecyclerView rv, MotionEvent ev) { 106 handleTouchEvent(ev); 107 } 108 109 /** 110 * Handles the touch event and determines whether to show the fast scroller (or updates it if 111 * it is already showing). 112 */ 113 private boolean handleTouchEvent(MotionEvent ev) { 114 int action = ev.getAction(); 115 int x = (int) ev.getX(); 116 int y = (int) ev.getY(); 117 switch (action) { 118 case MotionEvent.ACTION_DOWN: 119 // Keep track of the down positions 120 mDownX = x; 121 mDownY = mLastY = y; 122 if (shouldStopScroll(ev)) { 123 stopScroll(); 124 } 125 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 126 break; 127 case MotionEvent.ACTION_MOVE: 128 mLastY = y; 129 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 130 break; 131 case MotionEvent.ACTION_UP: 132 case MotionEvent.ACTION_CANCEL: 133 onFastScrollCompleted(); 134 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 135 break; 136 } 137 return mScrollbar.isDraggingThumb(); 138 } 139 140 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 141 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS 142 } 143 144 /** 145 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. 146 */ 147 protected boolean shouldStopScroll(MotionEvent ev) { 148 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 149 if ((Math.abs(mDy) < mDeltaThreshold && 150 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { 151 // now the touch events are being passed to the {@link WidgetCell} until the 152 // touch sequence goes over the touch slop. 153 return true; 154 } 155 } 156 return false; 157 } 158 159 public void updateBackgroundPadding(Rect padding) { 160 mBackgroundPadding.set(padding); 161 } 162 163 public Rect getBackgroundPadding() { 164 return mBackgroundPadding; 165 } 166 167 /** 168 * Returns the scroll bar width when the user is scrolling. 169 */ 170 public int getMaxScrollbarWidth() { 171 return mScrollbar.getThumbMaxWidth(); 172 } 173 174 /** 175 * Returns the visible height of the recycler view: 176 * VisibleHeight = View height - top padding - bottom padding 177 */ 178 protected int getVisibleHeight() { 179 int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; 180 return visibleHeight; 181 } 182 183 /** 184 * Returns the available scroll height: 185 * AvailableScrollHeight = Total height of the all items - last page height 186 */ 187 protected abstract int getAvailableScrollHeight(); 188 189 /** 190 * Returns the available scroll bar height: 191 * AvailableScrollBarHeight = Total height of the visible view - thumb height 192 */ 193 protected int getAvailableScrollBarHeight() { 194 int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight(); 195 return availableScrollBarHeight; 196 } 197 198 /** 199 * Returns the track color (ignoring alpha), can be overridden by each subclass. 200 */ 201 public int getFastScrollerTrackColor(int defaultTrackColor) { 202 return defaultTrackColor; 203 } 204 205 /** 206 * Returns the inactive thumb color, can be overridden by each subclass. 207 */ 208 public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { 209 return defaultInactiveThumbColor; 210 } 211 212 /** 213 * Returns the scrollbar for this recycler view. 214 */ 215 public BaseRecyclerViewFastScrollBar getScrollBar() { 216 return mScrollbar; 217 } 218 219 @Override 220 protected void dispatchDraw(Canvas canvas) { 221 super.dispatchDraw(canvas); 222 onUpdateScrollbar(0); 223 mScrollbar.draw(canvas); 224 } 225 226 /** 227 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does 228 * this by mapping the available scroll area of the recycler view to the available space for the 229 * scroll bar. 230 * 231 * @param scrollY the current scroll y 232 */ 233 protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, 234 int availableScrollHeight) { 235 // Only show the scrollbar if there is height to be scrolled 236 int availableScrollBarHeight = getAvailableScrollBarHeight(); 237 if (availableScrollHeight <= 0) { 238 mScrollbar.setThumbOffset(-1, -1); 239 return; 240 } 241 242 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 243 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 244 // padding) 245 int scrollBarY = mBackgroundPadding.top + 246 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); 247 248 // Calculate the position and size of the scroll bar 249 mScrollbar.setThumbOffset(getScrollBarX(), scrollBarY); 250 } 251 252 /** 253 * @return the x position for the scrollbar thumb 254 */ 255 protected int getScrollBarX() { 256 if (Utilities.isRtl(getResources())) { 257 return mBackgroundPadding.left; 258 } else { 259 return getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); 260 } 261 } 262 263 /** 264 * @return whether fast scrolling is supported in the current state. 265 */ 266 protected boolean supportsFastScrolling() { 267 return true; 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 * 274 * @return the scroll top of this recycler view. 275 */ 276 public abstract int getCurrentScrollY(); 277 278 /** 279 * Maps the touch (from 0..1) to the adapter position that should be visible. 280 * <p>Override in each subclass of this base class. 281 */ 282 protected abstract String scrollToPositionAtProgress(float touchFraction); 283 284 /** 285 * Updates the bounds for the scrollbar. 286 * <p>Override in each subclass of this base class. 287 */ 288 protected abstract void onUpdateScrollbar(int dy); 289 290 /** 291 * <p>Override in each subclass of this base class. 292 */ 293 protected void onFastScrollCompleted() {} 294}