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