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