blob: 41eeb78bc77cba9008bcb150e83ebf2ee02670bd [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3;
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.RecyclerViewFastScroller;
/**
* A base {@link RecyclerView}, which does the following:
* <ul>
* <li> NOT intercept a touch unless the scrolling velocity is below a predefined threshold.
* <li> Enable fast scroller.
* </ul>
*/
public abstract class BaseRecyclerView extends RecyclerView {
protected RecyclerViewFastScroller mScrollbar;
public BaseRecyclerView(Context context) {
this(context, null);
}
public BaseRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
bindFastScrollbar();
}
public void bindFastScrollbar() {
ViewGroup parent = (ViewGroup) getParent().getParent();
mScrollbar = parent.findViewById(R.id.fast_scroller);
mScrollbar.setRecyclerView(this, parent.findViewById(R.id.fast_scroller_popup));
onUpdateScrollbar(0);
}
public RecyclerViewFastScroller getScrollbar() {
return mScrollbar;
}
public int getScrollBarTop() {
return getPaddingTop();
}
/**
* Returns the height of the fast scroll bar
*/
public int getScrollbarTrackHeight() {
return mScrollbar.getHeight() - getScrollBarTop() - getPaddingBottom();
}
/**
* Returns the available scroll height:
* AvailableScrollHeight = Total height of the all items - last page height
*/
protected abstract int getAvailableScrollHeight();
/**
* Returns the available scroll bar height:
* AvailableScrollBarHeight = Total height of the visible view - thumb height
*/
protected int getAvailableScrollBarHeight() {
int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight();
return availableScrollBarHeight;
}
/**
* Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does
* this by mapping the available scroll area of the recycler view to the available space for the
* scroll bar.
*
* @param scrollY the current scroll y
*/
protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY,
int availableScrollHeight) {
// Only show the scrollbar if there is height to be scrolled
if (availableScrollHeight <= 0) {
mScrollbar.setThumbOffsetY(-1);
return;
}
// Calculate the current scroll position, the scrollY of the recycler view accounts for the
// view padding, while the scrollBarY is drawn right up to the background padding (ignoring
// padding)
int scrollBarY =
(int) (((float) scrollY / availableScrollHeight) * getAvailableScrollBarHeight());
// Calculate the position and size of the scroll bar
mScrollbar.setThumbOffsetY(scrollBarY);
}
/**
* Returns whether the view itself will handle the touch event or not.
* @param ev MotionEvent in {@param eventSource}
*/
public boolean shouldContainerScroll(MotionEvent ev, View eventSource) {
float[] point = new float[2];
point[0] = ev.getX();
point[1] = ev.getY();
Utilities.mapCoordInSelfToDescendant(mScrollbar, eventSource, point);
// IF the MotionEvent is inside the thumb, container should not be pulled down.
if (mScrollbar.shouldBlockIntercept((int) point[0], (int) point[1])) {
return false;
}
// IF scroller is at the very top OR there is no scroll bar because there is probably not
// enough items to scroll, THEN it's okay for the container to be pulled down.
if (getCurrentScrollY() == 0) {
return true;
}
return getAdapter() == null || getAdapter().getItemCount() == 0;
}
/**
* @return whether fast scrolling is supported in the current state.
*/
public boolean supportsFastScrolling() {
return true;
}
/**
* Maps the touch (from 0..1) to the adapter position that should be visible.
* <p>Override in each subclass of this base class.
*
* @return the scroll top of this recycler view.
*/
public abstract int getCurrentScrollY();
/**
* Maps the touch (from 0..1) to the adapter position that should be visible.
* <p>Override in each subclass of this base class.
*/
public abstract String scrollToPositionAtProgress(float touchFraction);
/**
* Updates the bounds for the scrollbar.
* <p>Override in each subclass of this base class.
*/
public abstract void onUpdateScrollbar(int dy);
/**
* <p>Override in each subclass of this base class.
*/
public void onFastScrollCompleted() {}
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
if (TestProtocol.sDebugTracing) {
Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onScrollStateChanged: " + state);
}
if (state == SCROLL_STATE_IDLE) {
AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
}
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
if (isLayoutSuppressed()) info.setScrollable(false);
if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
"onInitializeAccessibilityNodeInfo, scrollable: " + info.isScrollable());
}
}
@Override
public void setLayoutFrozen(boolean frozen) {
final boolean changing = frozen != isLayoutSuppressed();
super.setLayoutFrozen(frozen);
if (changing) {
if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
+ " @ " + Log.getStackTraceString(new Throwable()));
ActivityContext.lookupContext(getContext()).getDragLayer()
.sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
}
}
}
}