Using inbuild smooth scroller instead of custom fastscrolling logic

Change-Id: I9208720c3bf164bd664e5bacd75b672573fe7601
This commit is contained in:
Sunny Goyal
2020-04-17 16:31:40 -07:00
parent f01f25b579
commit 355e845851
4 changed files with 69 additions and 209 deletions
@@ -15,206 +15,90 @@
*/
package com.android.launcher3.allapps;
import com.android.launcher3.util.Thunk;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import java.util.HashSet;
import java.util.List;
import com.android.launcher3.allapps.AlphabeticalAppsList.FastScrollSectionInfo;
import androidx.recyclerview.widget.RecyclerView;
public class AllAppsFastScrollHelper {
public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
private static final int NO_POSITION = -1;
private static final int INITIAL_TOUCH_SETTLING_DURATION = 100;
private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
private int mTargetFastScrollPosition = NO_POSITION;
private AllAppsRecyclerView mRv;
private AlphabeticalAppsList mApps;
private ViewHolder mLastSelectedViewHolder;
// Keeps track of the current and targeted fast scroll section (the section to scroll to after
// the initial delay)
int mTargetFastScrollPosition = -1;
@Thunk String mCurrentFastScrollSection;
@Thunk String mTargetFastScrollSection;
// The settled states affect the delay before the fast scroll animation is applied
private boolean mHasFastScrollTouchSettled;
private boolean mHasFastScrollTouchSettledAtLeastOnce;
// Set of all views animated during fast scroll. We keep track of these ourselves since there
// is no way to reset a view once it gets scrapped or recycled without other hacks
private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>();
// Smooth fast-scroll animation frames
@Thunk int mFastScrollFrameIndex;
@Thunk final int[] mFastScrollFrames = new int[10];
/**
* This runnable runs a single frame of the smooth scroll animation and posts the next frame
* if necessary.
*/
@Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
@Override
public void run() {
if (mFastScrollFrameIndex < mFastScrollFrames.length) {
mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
mFastScrollFrameIndex++;
mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
}
}
};
/**
* This runnable updates the current fast scroll section to the target fastscroll section.
*/
Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
@Override
public void run() {
// Update to the target section
mCurrentFastScrollSection = mTargetFastScrollSection;
mHasFastScrollTouchSettled = true;
mHasFastScrollTouchSettledAtLeastOnce = true;
updateTrackedViewsFastScrollFocusState();
}
};
public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
public AllAppsFastScrollHelper(AllAppsRecyclerView rv) {
mRv = rv;
mApps = apps;
}
public void onSetAdapter(AllAppsGridAdapter adapter) {
adapter.setBindViewCallback(this);
}
/**
* Smooth scrolls the recycler view to the given section.
*
* @return whether the fastscroller can scroll to the new section.
*/
public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
AlphabeticalAppsList.FastScrollSectionInfo info) {
if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
mTargetFastScrollPosition = info.fastScrollToItem.position;
smoothSnapToPosition(scrollY, availableScrollHeight, info);
return true;
public void smoothScrollToSection(FastScrollSectionInfo info) {
if (mTargetFastScrollPosition == info.fastScrollToItem.position) {
return;
}
return false;
}
/**
* Smoothly snaps to a given position. We do this manually by calculating the keyframes
* ourselves and animating the scroll on the recycler view.
*/
private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
AlphabeticalAppsList.FastScrollSectionInfo info) {
mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
trackAllChildViews();
if (mHasFastScrollTouchSettled) {
// In this case, the user has already settled once (and the fast scroll state has
// animated) and they are just fine-tuning their section from the last section, so
// we should make it feel fast and update immediately.
mCurrentFastScrollSection = info.sectionName;
mTargetFastScrollSection = null;
updateTrackedViewsFastScrollFocusState();
} else {
// Otherwise, the user has scrubbed really far, and we don't want to distract the user
// with the flashing fast scroll state change animation in addition to the fast scroll
// section popup, so reset the views to normal, and wait for the touch to settle again
// before animating the fast scroll state.
mCurrentFastScrollSection = null;
mTargetFastScrollSection = info.sectionName;
mHasFastScrollTouchSettled = false;
updateTrackedViewsFastScrollFocusState();
// Delay scrolling to a new section until after some duration. If the user has been
// scrubbing a while and makes multiple big jumps, then reduce the time needed for the
// fast scroll to settle so it doesn't feel so long.
mRv.postDelayed(mFastScrollToTargetSectionRunnable,
mHasFastScrollTouchSettledAtLeastOnce ?
REPEAT_TOUCH_SETTLING_DURATION :
INITIAL_TOUCH_SETTLING_DURATION);
}
// Calculate the full animation from the current scroll position to the final scroll
// position, and then run the animation for the duration. If we are scrolling to the
// first fast scroll section, then just scroll to the top of the list itself.
List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
mApps.getFastScrollerSections();
int newPosition = info.fastScrollToItem.position;
int newScrollY = fastScrollSections.size() > 0 && fastScrollSections.get(0) == info
? 0
: Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
int numFrames = mFastScrollFrames.length;
int deltaY = newScrollY - scrollY;
float ySign = Math.signum(deltaY);
int step = (int) (ySign * Math.ceil((float) Math.abs(deltaY) / numFrames));
for (int i = 0; i < numFrames; i++) {
// TODO(winsonc): We can interpolate this as well.
mFastScrollFrames[i] = (int) (ySign * Math.min(Math.abs(step), Math.abs(deltaY)));
deltaY -= step;
}
mFastScrollFrameIndex = 0;
mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
mTargetFastScrollPosition = info.fastScrollToItem.position;
mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition));
}
public void onFastScrollCompleted() {
// TODO(winsonc): Handle the case when the user scrolls and releases before the animation
// runs
// Stop animating the fast scroll position and state
mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
// Reset the tracking variables
mHasFastScrollTouchSettled = false;
mHasFastScrollTouchSettledAtLeastOnce = false;
mCurrentFastScrollSection = null;
mTargetFastScrollSection = null;
mTargetFastScrollPosition = -1;
updateTrackedViewsFastScrollFocusState();
mTrackedFastScrollViews.clear();
mTargetFastScrollPosition = NO_POSITION;
setLastHolderSelected(false);
mLastSelectedViewHolder = null;
}
@Override
public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
// Update newly bound views to the current fast scroll state if we are fast scrolling
if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
mTrackedFastScrollViews.add(holder);
private void setLastHolderSelected(boolean isSelected) {
if (mLastSelectedViewHolder != null) {
mLastSelectedViewHolder.itemView.setActivated(isSelected);
mLastSelectedViewHolder.setIsRecyclable(!isSelected);
}
}
/**
* Starts tracking all the recycler view's children which are FastScrollFocusableViews.
*/
private void trackAllChildViews() {
int childCount = mRv.getChildCount();
for (int i = 0; i < childCount; i++) {
RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i));
if (viewHolder != null) {
mTrackedFastScrollViews.add(viewHolder);
}
}
}
private class MyScroller extends LinearSmoothScroller {
/**
* Updates the fast scroll focus on all the children.
*/
private void updateTrackedViewsFastScrollFocusState() {
for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) {
int pos = viewHolder.getAdapterPosition();
boolean isActive = false;
if (mCurrentFastScrollSection != null
&& pos > RecyclerView.NO_POSITION
&& pos < mApps.getAdapterItems().size()) {
AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
isActive = item != null &&
mCurrentFastScrollSection.equals(item.sectionName) &&
item.position == mTargetFastScrollPosition;
private final int mTargetPosition;
public MyScroller(int targetPosition) {
super(mRv.getContext());
mTargetPosition = targetPosition;
setTargetPosition(targetPosition);
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;
}
@Override
protected void onStop() {
super.onStop();
if (mTargetPosition != mTargetFastScrollPosition) {
// Target changed, before the last scroll can finish
return;
}
ViewHolder currentHolder = mRv.findViewHolderForAdapterPosition(mTargetPosition);
if (currentHolder == mLastSelectedViewHolder) {
return;
}
setLastHolderSelected(false);
mLastSelectedViewHolder = currentHolder;
setLastHolderSelected(true);
}
@Override
protected void onStart() {
super.onStart();
if (mTargetPosition != mTargetFastScrollPosition) {
setLastHolderSelected(false);
mLastSelectedViewHolder = null;
}
viewHolder.itemView.setActivated(isActive);
}
}
}