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 android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED; 20 21 import android.content.Context; 22 import android.util.AttributeSet; 23 import android.util.Log; 24 import android.view.MotionEvent; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.view.accessibility.AccessibilityNodeInfo; 28 29 import androidx.recyclerview.widget.RecyclerView; 30 31 import com.android.launcher3.compat.AccessibilityManagerCompat; 32 import com.android.launcher3.testing.TestProtocol; 33 import com.android.launcher3.views.ActivityContext; 34 import com.android.launcher3.views.RecyclerViewFastScroller; 35 36 37 /** 38 * A base {@link RecyclerView}, which does the following: 39 * <ul> 40 * <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold. 41 * <li> Enable fast scroller. 42 * </ul> 43 */ 44 public abstract class BaseRecyclerView extends RecyclerView { 45 46 protected RecyclerViewFastScroller mScrollbar; 47 BaseRecyclerView(Context context)48 public BaseRecyclerView(Context context) { 49 this(context, null); 50 } 51 BaseRecyclerView(Context context, AttributeSet attrs)52 public BaseRecyclerView(Context context, AttributeSet attrs) { 53 this(context, attrs, 0); 54 } 55 BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr)56 public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 57 super(context, attrs, defStyleAttr); 58 } 59 60 @Override onAttachedToWindow()61 protected void onAttachedToWindow() { 62 super.onAttachedToWindow(); 63 bindFastScrollbar(); 64 } 65 bindFastScrollbar()66 public void bindFastScrollbar() { 67 ViewGroup parent = (ViewGroup) getParent().getParent(); 68 mScrollbar = parent.findViewById(R.id.fast_scroller); 69 mScrollbar.setRecyclerView(this, parent.findViewById(R.id.fast_scroller_popup)); 70 onUpdateScrollbar(0); 71 } 72 getScrollbar()73 public RecyclerViewFastScroller getScrollbar() { 74 return mScrollbar; 75 } 76 getScrollBarTop()77 public int getScrollBarTop() { 78 return getPaddingTop(); 79 } 80 81 /** 82 * Returns the height of the fast scroll bar 83 */ getScrollbarTrackHeight()84 public int getScrollbarTrackHeight() { 85 return mScrollbar.getHeight() - getScrollBarTop() - getPaddingBottom(); 86 } 87 88 /** 89 * Returns the available scroll height: 90 * AvailableScrollHeight = Total height of the all items - last page height 91 */ getAvailableScrollHeight()92 protected abstract int getAvailableScrollHeight(); 93 94 /** 95 * Returns the available scroll bar height: 96 * AvailableScrollBarHeight = Total height of the visible view - thumb height 97 */ getAvailableScrollBarHeight()98 protected int getAvailableScrollBarHeight() { 99 int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight(); 100 return availableScrollBarHeight; 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 if (getCurrentScrollY() == 0) { 145 return true; 146 } 147 return getAdapter() == null || getAdapter().getItemCount() == 0; 148 } 149 150 /** 151 * @return whether fast scrolling is supported in the current state. 152 */ supportsFastScrolling()153 public boolean supportsFastScrolling() { 154 return true; 155 } 156 157 /** 158 * Maps the touch (from 0..1) to the adapter position that should be visible. 159 * <p>Override in each subclass of this base class. 160 * 161 * @return the scroll top of this recycler view. 162 */ getCurrentScrollY()163 public abstract int getCurrentScrollY(); 164 165 /** 166 * Maps the touch (from 0..1) to the adapter position that should be visible. 167 * <p>Override in each subclass of this base class. 168 */ scrollToPositionAtProgress(float touchFraction)169 public abstract String scrollToPositionAtProgress(float touchFraction); 170 171 /** 172 * Updates the bounds for the scrollbar. 173 * <p>Override in each subclass of this base class. 174 */ onUpdateScrollbar(int dy)175 public abstract void onUpdateScrollbar(int dy); 176 177 /** 178 * <p>Override in each subclass of this base class. 179 */ onFastScrollCompleted()180 public void onFastScrollCompleted() {} 181 182 @Override onScrollStateChanged(int state)183 public void onScrollStateChanged(int state) { 184 super.onScrollStateChanged(state); 185 186 if (state == SCROLL_STATE_IDLE) { 187 AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext()); 188 } 189 } 190 191 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)192 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 193 super.onInitializeAccessibilityNodeInfo(info); 194 if (isLayoutSuppressed()) info.setScrollable(false); 195 } 196 197 @Override setLayoutFrozen(boolean frozen)198 public void setLayoutFrozen(boolean frozen) { 199 final boolean changing = frozen != isLayoutSuppressed(); 200 super.setLayoutFrozen(frozen); 201 if (changing) { 202 ActivityContext.lookupContext(getContext()).getDragLayer() 203 .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED); 204 } 205 } 206 }