Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bb743884 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Using inbuild smooth scroller instead of custom fastscrolling logic"...

Merge "Using inbuild smooth scroller instead of custom fastscrolling logic" into ub-launcher3-rvc-dev
parents f5a10fe9 355e8458
Loading
Loading
Loading
Loading
+57 −173
Original line number Diff line number Diff line
@@ -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;
        mTargetFastScrollPosition = info.fastScrollToItem.position;
        mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition));
    }

    /**
     * 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);
    public void onFastScrollCompleted() {
        mTargetFastScrollPosition = NO_POSITION;
        setLastHolderSelected(false);
        mLastSelectedViewHolder = null;
    }

        // 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;

    private void setLastHolderSelected(boolean isSelected) {
        if (mLastSelectedViewHolder != null) {
            mLastSelectedViewHolder.itemView.setActivated(isSelected);
            mLastSelectedViewHolder.setIsRecyclable(!isSelected);
        }
        mFastScrollFrameIndex = 0;
        mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
    }

    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();
    private class MyScroller extends LinearSmoothScroller {

        private final int mTargetPosition;

        public MyScroller(int targetPosition) {
            super(mRv.getContext());

            mTargetPosition = targetPosition;
            setTargetPosition(targetPosition);
        }

        @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);
        }
        protected int getVerticalSnapPreference() {
            return SNAP_TO_START;
        }

    /**
     * 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);
        @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);
        }

    /**
     * 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;
        @Override
        protected void onStart() {
            super.onStart();
            if (mTargetPosition != mTargetFastScrollPosition) {
                setLastHolderSelected(false);
                mLastSelectedViewHolder = null;
            }
            viewHolder.itemView.setActivated(isActive);
        }
    }
}
+0 −16
Original line number Diff line number Diff line
@@ -71,11 +71,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
    public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
    public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;


    public interface BindViewCallback {
        void onBindView(ViewHolder holder);
    }

    /**
     * ViewHolder for each icon.
     */
@@ -186,7 +181,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.

    private int mAppsPerRow;

    private BindViewCallback mBindViewCallback;
    private OnFocusChangeListener mIconFocusListener;

    // The text to show when there are no search results and no market search handler.
@@ -247,13 +241,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
        mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
    }

    /**
     * Sets the callback for when views are bound.
     */
    public void setBindViewCallback(BindViewCallback cb) {
        mBindViewCallback = cb;
    }

    /**
     * Returns the grid layout manager.
     */
@@ -319,9 +306,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.
                // nothing to do
                break;
        }
        if (mBindViewCallback != null) {
            mBindViewCallback.onBindView(holder);
        }
    }

    @Override
+5 −12
Original line number Diff line number Diff line
@@ -53,12 +53,12 @@ import java.util.List;
public class AllAppsRecyclerView extends BaseRecyclerView implements LogContainerProvider {

    private AlphabeticalAppsList mApps;
    private AllAppsFastScrollHelper mFastScrollHelper;
    private final int mNumAppsPerRow;

    // The specific view heights that we use to calculate scroll
    private SparseIntArray mViewHeights = new SparseIntArray();
    private SparseIntArray mCachedScrollPositions = new SparseIntArray();
    private final SparseIntArray mViewHeights = new SparseIntArray();
    private final SparseIntArray mCachedScrollPositions = new SparseIntArray();
    private final AllAppsFastScrollHelper mFastScrollHelper;

    // The empty-search result background
    private AllAppsBackgroundDrawable mEmptySearchBackground;
@@ -85,6 +85,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
        mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                R.dimen.all_apps_empty_search_bg_top_offset);
        mNumAppsPerRow = LauncherAppState.getIDP(context).numColumns;
        mFastScrollHelper = new AllAppsFastScrollHelper(this);
    }

    /**
@@ -92,7 +93,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
     */
    public void setApps(AlphabeticalAppsList apps) {
        mApps = apps;
        mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
    }

    public AlphabeticalAppsList getApps() {
@@ -221,9 +221,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
            return "";
        }

        // Stop the scroller if it is scrolling
        stopScroll();

        // Find the fastscroll section that maps to this touch fraction
        List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                mApps.getFastScrollerSections();
@@ -236,10 +233,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
            lastInfo = info;
        }

        // Update the fast scroll
        int scrollY = getCurrentScrollY();
        int availableScrollHeight = getAvailableScrollHeight();
        mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
        mFastScrollHelper.smoothScrollToSection(lastInfo);
        return lastInfo.sectionName;
    }

@@ -257,7 +251,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
                mCachedScrollPositions.clear();
            }
        });
        mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
    }

    @Override
+4 −5
Original line number Diff line number Diff line
@@ -67,7 +67,6 @@ public class RecyclerViewFastScroller extends View {

    private final static int MAX_TRACK_ALPHA = 30;
    private final static int SCROLL_BAR_VIS_DURATION = 150;
    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f;

    private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
            Collections.singletonList(new Rect());
@@ -184,7 +183,7 @@ public class RecyclerViewFastScroller extends View {
        if (mThumbOffsetY == y) {
            return;
        }
        updatePopupY((int) y);
        updatePopupY(y);
        mThumbOffsetY = y;
        invalidate();
    }
@@ -237,7 +236,7 @@ public class RecyclerViewFastScroller extends View {
                } else if (mRv.supportsFastScrolling()
                        && isNearScrollBar(mDownX)) {
                    calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
                    updateFastScrollSectionNameAndThumbOffset(mLastY, y);
                    updateFastScrollSectionNameAndThumbOffset(y);
                }
                break;
            case MotionEvent.ACTION_MOVE:
@@ -252,7 +251,7 @@ public class RecyclerViewFastScroller extends View {
                    calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
                }
                if (mIsDragging) {
                    updateFastScrollSectionNameAndThumbOffset(mLastY, y);
                    updateFastScrollSectionNameAndThumbOffset(y);
                }
                break;
            case MotionEvent.ACTION_UP:
@@ -281,7 +280,7 @@ public class RecyclerViewFastScroller extends View {
        showActiveScrollbar(true);
    }

    private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) {
    private void updateFastScrollSectionNameAndThumbOffset(int y) {
        // Update the fastscroller section name at this touch position
        int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight;
        float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY));