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.util.AttributeSet; 21 import android.view.MotionEvent; 22 import android.view.View; 23 import android.view.accessibility.AccessibilityNodeInfo; 24 25 import androidx.annotation.Nullable; 26 import androidx.recyclerview.widget.RecyclerView; 27 28 import com.android.launcher3.compat.AccessibilityManagerCompat; 29 import com.android.launcher3.views.RecyclerViewFastScroller; 30 31 32 /** 33 * A base {@link RecyclerView}, which does the following: 34 * <ul> 35 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold. 36 * <li> Enable fast scroller. 37 * </ul> 38 */ 39 public abstract class FastScrollRecyclerView extends RecyclerView { 40 41 protected RecyclerViewFastScroller mScrollbar; 42 FastScrollRecyclerView(Context context)43 public FastScrollRecyclerView(Context context) { 44 this(context, null); 45 } 46 FastScrollRecyclerView(Context context, AttributeSet attrs)47 public FastScrollRecyclerView(Context context, AttributeSet attrs) { 48 this(context, attrs, 0); 49 } 50 FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)51 public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 52 super(context, attrs, defStyleAttr); 53 } 54 bindFastScrollbar(RecyclerViewFastScroller scrollbar)55 public void bindFastScrollbar(RecyclerViewFastScroller scrollbar) { 56 mScrollbar = scrollbar; 57 mScrollbar.setRecyclerView(this); 58 onUpdateScrollbar(0); 59 } 60 61 @Nullable getScrollbar()62 public RecyclerViewFastScroller getScrollbar() { 63 return mScrollbar; 64 } 65 getScrollBarTop()66 public int getScrollBarTop() { 67 return getPaddingTop(); 68 } 69 getScrollBarMarginBottom()70 public int getScrollBarMarginBottom() { 71 return getPaddingBottom(); 72 } 73 74 /** 75 * Returns the height of the fast scroll bar 76 */ getScrollbarTrackHeight()77 public int getScrollbarTrackHeight() { 78 return mScrollbar.getHeight() - getScrollBarTop() - getScrollBarMarginBottom(); 79 } 80 81 /** 82 * Returns the available scroll height: 83 * AvailableScrollHeight = Total height of the all items - last page height 84 */ getAvailableScrollHeight()85 protected int getAvailableScrollHeight() { 86 // AvailableScrollHeight = Total height of the all items - first page height 87 int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 88 int availableScrollHeight = computeVerticalScrollRange() - firstPageHeight; 89 return Math.max(0, availableScrollHeight); 90 } 91 92 /** 93 * Returns the available scroll bar height: 94 * AvailableScrollBarHeight = Total height of the visible view - thumb height 95 */ getAvailableScrollBarHeight()96 protected int getAvailableScrollBarHeight() { 97 return getScrollbarTrackHeight() - mScrollbar.getThumbHeight(); 98 } 99 100 /** 101 * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does 102 * this by mapping the available scroll area of the recycler view to the available space for the 103 * scroll bar. 104 * 105 * @param scrollY the current scroll y 106 */ synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, int availableScrollHeight)107 protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY, 108 int availableScrollHeight) { 109 // Only show the scrollbar if there is height to be scrolled 110 if (availableScrollHeight <= 0) { 111 mScrollbar.setThumbOffsetY(-1); 112 return; 113 } 114 115 // Calculate the current scroll position, the scrollY of the recycler view accounts for the 116 // view padding, while the scrollBarY is drawn right up to the background padding (ignoring 117 // padding) 118 int scrollBarY = 119 (int) (((float) scrollY / availableScrollHeight) * getAvailableScrollBarHeight()); 120 121 // Calculate the position and size of the scroll bar 122 mScrollbar.setThumbOffsetY(scrollBarY); 123 } 124 125 /** 126 * Returns whether the view itself will handle the touch event or not. 127 * @param ev MotionEvent in {@param eventSource} 128 */ shouldContainerScroll(MotionEvent ev, View eventSource)129 public boolean shouldContainerScroll(MotionEvent ev, View eventSource) { 130 float[] point = new float[2]; 131 point[0] = ev.getX(); 132 point[1] = ev.getY(); 133 Utilities.mapCoordInSelfToDescendant(mScrollbar, eventSource, point); 134 // IF the MotionEvent is inside the thumb, container should not be pulled down. 135 if (mScrollbar.shouldBlockIntercept((int) point[0], (int) point[1])) { 136 return false; 137 } 138 139 // IF scroller is at the very top OR there is no scroll bar because there is probably not 140 // enough items to scroll, THEN it's okay for the container to be pulled down. 141 return computeVerticalScrollOffset() == 0; 142 } 143 144 /** 145 * @return whether fast scrolling is supported in the current state. 146 */ supportsFastScrolling()147 public boolean supportsFastScrolling() { 148 return true; 149 } 150 151 /** 152 * Maps the touch (from 0..1) to the adapter position that should be visible. 153 * <p>Override in each subclass of this base class. 154 */ scrollToPositionAtProgress(float touchFraction)155 public abstract String scrollToPositionAtProgress(float touchFraction); 156 157 /** 158 * Updates the bounds for the scrollbar. 159 * <p>Override in each subclass of this base class. 160 */ onUpdateScrollbar(int dy)161 public abstract void onUpdateScrollbar(int dy); 162 163 /** 164 * <p>Override in each subclass of this base class. 165 */ onFastScrollCompleted()166 public void onFastScrollCompleted() {} 167 168 @Override onScrollStateChanged(int state)169 public void onScrollStateChanged(int state) { 170 super.onScrollStateChanged(state); 171 172 if (state == SCROLL_STATE_IDLE) { 173 AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext()); 174 } 175 } 176 177 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)178 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 179 super.onInitializeAccessibilityNodeInfo(info); 180 if (isLayoutSuppressed()) info.setScrollable(false); 181 } 182 183 /** 184 * Scrolls this recycler view to the top. 185 */ scrollToTop()186 public void scrollToTop() { 187 if (mScrollbar != null) { 188 mScrollbar.reattachThumbToScroll(); 189 } 190 scrollToPosition(0); 191 } 192 } 193