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.support.v7.widget.RecyclerView; 22 import android.util.AttributeSet; 23 import android.view.MotionEvent; 24 import android.view.ViewGroup; 25 26 import com.android.launcher3.util.Thunk; 27 28 29 /** 30 * A base {@link RecyclerView}, which does the following: 31 * <ul> 32 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold. 33 * <li> Enable fast scroller. 34 * </ul> 35 */ 36 public abstract class BaseRecyclerView extends RecyclerView 37 implements RecyclerView.OnItemTouchListener { 38 39 private static final int SCROLL_DELTA_THRESHOLD_DP = 4; 40 41 /** Keeps the last known scrolling delta/velocity along y-axis. */ 42 @Thunk int mDy = 0; 43 private float mDeltaThreshold; 44 45 protected final BaseRecyclerViewFastScrollBar mScrollbar; 46 47 private int mDownX; 48 private int mDownY; 49 private int mLastY; 50 BaseRecyclerView(Context context)51 public BaseRecyclerView(Context context) { 52 this(context, null); 53 } 54 BaseRecyclerView(Context context, AttributeSet attrs)55 public BaseRecyclerView(Context context, AttributeSet attrs) { 56 this(context, attrs, 0); 57 } 58 BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)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 { ScrollListener()69 public ScrollListener() { 70 // Do nothing 71 } 72 73 @Override onScrolled(RecyclerView recyclerView, int dx, int dy)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 reset()85 public void reset() { 86 mScrollbar.reattachThumbToScroll(); 87 } 88 89 @Override onFinishInflate()90 protected void onFinishInflate() { 91 super.onFinishInflate(); 92 addOnItemTouchListener(this); 93 } 94 95 @Override onAttachedToWindow()96 protected void onAttachedToWindow() { 97 super.onAttachedToWindow(); 98 mScrollbar.setPopupView(((ViewGroup) getParent()).findViewById(R.id.fast_scroller_popup)); 99 } 100 101 /** 102 * We intercept the touch handling only to support fast scrolling when initiated from the 103 * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. 104 */ 105 @Override onInterceptTouchEvent(RecyclerView rv, MotionEvent ev)106 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { 107 return handleTouchEvent(ev); 108 } 109 110 @Override onTouchEvent(RecyclerView rv, MotionEvent ev)111 public void onTouchEvent(RecyclerView rv, MotionEvent ev) { 112 handleTouchEvent(ev); 113 } 114 115 /** 116 * Handles the touch event and determines whether to show the fast scroller (or updates it if 117 * it is already showing). 118 */ handleTouchEvent(MotionEvent ev)119 private boolean handleTouchEvent(MotionEvent ev) { 120 int action = ev.getAction(); 121 int x = (int) ev.getX(); 122 int y = (int) ev.getY(); 123 switch (action) { 124 case MotionEvent.ACTION_DOWN: 125 // Keep track of the down positions 126 mDownX = x; 127 mDownY = mLastY = y; 128 if (shouldStopScroll(ev)) { 129 stopScroll(); 130 } 131 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 132 break; 133 case MotionEvent.ACTION_MOVE: 134 mLastY = y; 135 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 136 break; 137 case MotionEvent.ACTION_UP: 138 case MotionEvent.ACTION_CANCEL: 139 onFastScrollCompleted(); 140 mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); 141 break; 142 } 143 return mScrollbar.isDraggingThumb(); 144 } 145 onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)146 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { 147 // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS 148 } 149 150 /** 151 * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. 152 */ shouldStopScroll(MotionEvent ev)153 protected boolean shouldStopScroll(MotionEvent ev) { 154 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 155 if ((Math.abs(mDy) < mDeltaThreshold && 156 getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { 157 // now the touch events are being passed to the {@link WidgetCell} until the 158 // touch sequence goes over the touch slop. 159 return true; 160 } 161 } 162 return false; 163 } 164 165 /** 166 * Returns the height of the fast scroll bar 167 */ getScrollbarTrackHeight()168 protected int getScrollbarTrackHeight() { 169 return getHeight(); 170 } 171 172 /** 173 * Returns the available scroll height: 174 * AvailableScrollHeight = Total height of the all items - last page height 175 */ getAvailableScrollHeight()176 protected abstract int getAvailableScrollHeight(); 177 178 /** 179 * Returns the available scroll bar height: 180 * AvailableScrollBarHeight = Total height of the visible view - thumb height 181 */ getAvailableScrollBarHeight()182 protected int getAvailableScrollBarHeight() { 183 int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight(); 184 return availableScrollBarHeight; 185 } 186 187 /** 188 * Returns the track color (ignoring alpha), can be overridden by each subclass. 189 */ getFastScrollerTrackColor(int defaultTrackColor)190 public int getFastScrollerTrackColor(int defaultTrackColor) { 191 return defaultTrackColor; 192 } 193 194 /** 195 * Returns the scrollbar for this recycler view. 196 */ getScrollBar()197 public BaseRecyclerViewFastScrollBar getScrollBar() { 198 return mScrollbar; 199 } 200 201 @Override dispatchDraw(Canvas canvas)202 protected void dispatchDraw(Canvas canvas) { 203 super.dispatchDraw(canvas); 204 onUpdateScrollbar(0); 205 mScrollbar.draw(canvas); 206 } 207 208 /** 209 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does 210 * this by mapping the available scroll area of the recycler view to the available space for the 211 * scroll bar. 212 * 213 * @param scrollY the current scroll y 214 */ synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, int availableScrollHeight)215 protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, 216 int availableScrollHeight) { 217 // Only show the scrollbar if there is height to be scrolled 218 if (availableScrollHeight <= 0) { 219 mScrollbar.setThumbOffsetY(-1); 220 return; 221 } 222 223 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 224 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 225 // padding) 226 int scrollBarY = 227 (int) (((float) scrollY / availableScrollHeight) * getAvailableScrollBarHeight()); 228 229 // Calculate the position and size of the scroll bar 230 mScrollbar.setThumbOffsetY(scrollBarY); 231 } 232 233 /** 234 * @return whether fast scrolling is supported in the current state. 235 */ supportsFastScrolling()236 protected boolean supportsFastScrolling() { 237 return true; 238 } 239 240 /** 241 * Maps the touch (from 0..1) to the adapter position that should be visible. 242 * <p>Override in each subclass of this base class. 243 * 244 * @return the scroll top of this recycler view. 245 */ getCurrentScrollY()246 public abstract int getCurrentScrollY(); 247 248 /** 249 * Maps the touch (from 0..1) to the adapter position that should be visible. 250 * <p>Override in each subclass of this base class. 251 */ scrollToPositionAtProgress(float touchFraction)252 protected abstract String scrollToPositionAtProgress(float touchFraction); 253 254 /** 255 * Updates the bounds for the scrollbar. 256 * <p>Override in each subclass of this base class. 257 */ onUpdateScrollbar(int dy)258 protected abstract void onUpdateScrollbar(int dy); 259 260 /** 261 * <p>Override in each subclass of this base class. 262 */ onFastScrollCompleted()263 protected void onFastScrollCompleted() {} 264 }