From 86587d8f1a505471fc39f64977945ccea634d6f6 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Sat, 27 Jun 2015 15:52:30 +0200 Subject: [PATCH] Fixed the fastscroller lag on huge lists (e.g. song list) by replacing the old fastscroller with an own implementation. --- .../AbsMainActivityRecyclerViewFragment.java | 19 +- .../gramophone/views/FastScroller.java | 324 ++++++++---------- ...vertical_recycler_fast_scroller_layout.xml | 8 +- app/src/main/res/values/dimens.xml | 5 +- 4 files changed, 160 insertions(+), 196 deletions(-) diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java index 3c6ed09f..3a47c64f 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java +++ b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/mainactivityfragments/AbsMainActivityRecyclerViewFragment.java @@ -8,10 +8,8 @@ import android.support.design.widget.AppBarLayout; import android.support.design.widget.AppBarLayout.OnOffsetChangedListener; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; import android.widget.TextView; import com.kabouzeid.gramophone.R; @@ -54,13 +52,6 @@ public abstract class AbsMainActivityRecyclerViewFragment extends AbsMainActivit if (fastScroller != null) { fastScroller.setRecyclerView(recyclerView); - fastScroller.setPressedHandleColor(getMainActivity().getThemeColorPrimary()); - fastScroller.setOnHandleTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - return false; - } - }); } getMainActivity().addOnAppBarOffsetChangedListener(this); @@ -91,9 +82,13 @@ public abstract class AbsMainActivityRecyclerViewFragment extends AbsMainActivit @Override public void onOffsetChanged(AppBarLayout appBarLayout, int i) { if (fastScroller != null) { - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) fastScroller.getLayoutParams(); - params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, getMainActivity().getTotalAppBarScrollingRange() + i); - fastScroller.setLayoutParams(params); + fastScroller.setPadding( + fastScroller.getPaddingLeft(), + fastScroller.getPaddingTop(), + fastScroller.getPaddingRight(), + getMainActivity().getTotalAppBarScrollingRange() + i + ); + fastScroller.updateHandlePosition(); } } diff --git a/app/src/main/java/com/kabouzeid/gramophone/views/FastScroller.java b/app/src/main/java/com/kabouzeid/gramophone/views/FastScroller.java index 3fe2a8e5..4d533a13 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/views/FastScroller.java +++ b/app/src/main/java/com/kabouzeid/gramophone/views/FastScroller.java @@ -2,133 +2,181 @@ package com.kabouzeid.gramophone.views; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; import android.content.Context; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.StateListDrawable; -import android.support.v4.view.animation.FastOutLinearInInterpolator; -import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewPropertyAnimator; import android.widget.FrameLayout; +import com.afollestad.materialdialogs.ThemeSingleton; import com.kabouzeid.gramophone.R; import com.kabouzeid.gramophone.util.Util; -/** - * Defines a basic widget that will allow for fast scrolling a RecyclerView using the basic paradigm of - * a handle and a bar. - */ +import static android.support.v7.widget.RecyclerView.OnScrollListener; + public class FastScroller extends FrameLayout { + private static final int HANDLE_HIDE_DELAY = 1500; + private static final int HANDLE_ANIMATION_DURATION = 300; - /** - * The long bar along which a handle travels - */ - protected final View mBar; - /** - * The handle that signifies the user's progress in the list - */ - protected final View mHandle; - protected RecyclerView.OnScrollListener mOnScrollListener; - protected OnTouchListener mOnTouchListener; - private RecyclerView mRecyclerView; - private AnimatorSet mAnimator; - private boolean animatingIn; - private final Runnable mHide; - private final int mMinScrollHandleHeight; - private final int mHiddenTranslationX; + private View handle; + private View bar; - public FastScroller(Context context) { - this(context, null, 0); - } + private RecyclerView recyclerView; + + private final HandleHider handleHider = new HandleHider(); + private final ScrollListener scrollListener = new ScrollListener(); + + private boolean isHidden; + private int hideTranslationX; + + private ViewPropertyAnimator currentAnimator = null; public FastScroller(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); + initialise(context); } public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + initialise(context); + } + private void initialise(Context context) { + hideTranslationX = getContext().getResources().getDimensionPixelSize(R.dimen.scrollbar_width) * (Util.isRTL(context) ? -1 : 1); + setClipChildren(false); inflate(context, R.layout.vertical_recycler_fast_scroller_layout, this); + handle = findViewById(R.id.scroll_handle); + bar = findViewById(R.id.scroll_bar); + handle.setEnabled(true); + setPressedHandleColor(ThemeSingleton.get().positiveColor); + setUpBarBackground(); + postDelayed(handleHider, HANDLE_HIDE_DELAY); + } - mBar = findViewById(R.id.scroll_bar); - mHandle = findViewById(R.id.scroll_handle); - mMinScrollHandleHeight = getResources().getDimensionPixelSize(R.dimen.min_scrollhandle_height); + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) { + setHandlePosition(event.getY()); + handle.setPressed(true); + setRecyclerViewPosition(event.getY()); + showIfHidden(); + return true; + } else if (event.getAction() == MotionEvent.ACTION_UP) { + handle.setPressed(false); + scheduleHide(); + return true; + } + return super.onTouchEvent(event); + } - mHiddenTranslationX = (Util.isRTL(getContext()) ? -1 : 1) * getResources().getDimensionPixelSize(R.dimen.scrollbar_width); - mHide = new Runnable() { - @Override - public void run() { - if (!mHandle.isPressed()) { - if (mAnimator != null && mAnimator.isStarted()) { - mAnimator.cancel(); - } - mAnimator = new AnimatorSet(); - ObjectAnimator animator2 = ObjectAnimator.ofFloat(FastScroller.this, View.TRANSLATION_X, - mHiddenTranslationX); - animator2.setInterpolator(new FastOutLinearInInterpolator()); - animator2.setDuration(150); - mHandle.setEnabled(false); - mAnimator.play(animator2); - mAnimator.start(); - } - } - }; + public void setRecyclerView(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + recyclerView.addOnScrollListener(scrollListener); + } - mHandle.setOnTouchListener(new OnTouchListener() { - private float mInitialBarHeight; - private float mLastPressedYAdjustedToInitial; + private void setRecyclerViewPosition(float y) { + if (recyclerView != null) { + int itemCount = recyclerView.getAdapter().getItemCount(); + float proportion = y / (float) getHeightMinusPadding(); + int targetPos = getValueInRange(0, itemCount - 1, (int) (proportion * (float) itemCount)); + recyclerView.scrollToPosition(targetPos); + } + } - @Override - public boolean onTouch(View v, MotionEvent event) { - mOnTouchListener.onTouch(v, event); - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mHandle.setPressed(true); + private int getValueInRange(int min, int max, int value) { + int minimum = Math.max(min, value); + return Math.min(minimum, max); + } - mInitialBarHeight = getBarHeight(); - mLastPressedYAdjustedToInitial = event.getY() + mHandle.getY() + mBar.getY(); - } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { - float newHandlePressedY = event.getY() + mHandle.getY() + mBar.getY(); - int barHeight = getBarHeight(); - float newHandlePressedYAdjustedToInitial = - newHandlePressedY + (mInitialBarHeight - barHeight); + private void setHandlePosition(float y) { + float position = y / getHeightMinusPadding(); + int handleHeight = handle.getHeight(); + handle.setY(getValueInRange(0, getHeightMinusPadding() - handleHeight, (int) ((getHeightMinusPadding() - handleHeight) * position))); + } - float deltaPressedYFromLastAdjustedToInitial = - newHandlePressedYAdjustedToInitial - mLastPressedYAdjustedToInitial; + private void showImpl() { + isHidden = false; + if (currentAnimator != null) { + currentAnimator.cancel(); + } + currentAnimator = animate().translationX(0).setDuration(HANDLE_ANIMATION_DURATION); + currentAnimator.start(); + } - int dY = (int) ((deltaPressedYFromLastAdjustedToInitial / mInitialBarHeight) * - (mRecyclerView.computeVerticalScrollRange())); + private void hideImpl() { + isHidden = true; + if (currentAnimator != null) { + currentAnimator.cancel(); + } + currentAnimator = animate().translationX(hideTranslationX).setDuration(HANDLE_ANIMATION_DURATION); + currentAnimator.setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + currentAnimator = null; + } - updateRvScroll(dY); + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + currentAnimator = null; + } + }); + currentAnimator.start(); + } + + private class HandleHider implements Runnable { + @Override + public void run() { + hideImpl(); + } + } - mLastPressedYAdjustedToInitial = newHandlePressedYAdjustedToInitial; - } else if (event.getActionMasked() == MotionEvent.ACTION_UP) { - mLastPressedYAdjustedToInitial = -1; + private void showIfHidden() { + if (isHidden) { + getHandler().removeCallbacks(handleHider); + showImpl(); + } + } - mHandle.setPressed(false); - postAutoHide(); - } + private void scheduleHide() { + getHandler().removeCallbacks(handleHider); + getHandler().postDelayed(handleHider, HANDLE_HIDE_DELAY); + } - return true; - } - }); + private int getHeightMinusPadding() { + return getHeight() - getPaddingBottom() - getPaddingTop(); + } - setTranslationX(mHiddenTranslationX); + private float computeHandlePosition() { + View firstVisibleView = recyclerView.getChildAt(0); + int firstVisiblePosition = recyclerView.getChildAdapterPosition(firstVisibleView); + int visibleRange = recyclerView.getChildCount(); + int lastVisiblePosition = firstVisiblePosition + visibleRange; + int itemCount = recyclerView.getAdapter().getItemCount(); + int position; + if (firstVisiblePosition == 0) { + position = 0; + } else if (lastVisiblePosition == itemCount - 1) { + position = itemCount - 1; + } else { + position = firstVisiblePosition; + } + float proportion = (float) position / (float) itemCount; + return getHeightMinusPadding() * proportion; + } - //Default selected handle color - setPressedHandleColor(Color.BLACK); - setUpBarBackground(); + public void updateHandlePosition() { + setHandlePosition(computeHandlePosition()); } - /** - * Provides the ability to programmatically set the color of the fast scroller's handle - */ public void setPressedHandleColor(int accent) { StateListDrawable drawable = new StateListDrawable(); @@ -141,11 +189,11 @@ public class FastScroller extends FrameLayout { new InsetDrawable(new ColorDrawable(colorControlNormal), getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0, 0, 0)); } else { drawable.addState(View.PRESSED_ENABLED_STATE_SET, - new InsetDrawable(new ColorDrawable(accent), 0, getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0, 0)); + new InsetDrawable(new ColorDrawable(accent), 0, 0, getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0)); drawable.addState(View.EMPTY_STATE_SET, - new InsetDrawable(new ColorDrawable(colorControlNormal), 0, getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0, 0)); + new InsetDrawable(new ColorDrawable(colorControlNormal), 0, 0, getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0)); } - mHandle.setBackground(drawable); + handle.setBackground(drawable); } private void setUpBarBackground() { @@ -156,97 +204,17 @@ public class FastScroller extends FrameLayout { if (!Util.isRTL(getContext())) { drawable = new InsetDrawable(new ColorDrawable(colorControlNormal), getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0, 0, 0); } else { - drawable = new InsetDrawable(new ColorDrawable(colorControlNormal), 0, getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0, 0); + drawable = new InsetDrawable(new ColorDrawable(colorControlNormal), 0, 0, getResources().getDimensionPixelSize(R.dimen.scrollbar_inset), 0); } - mBar.setBackground(drawable); + bar.setBackground(drawable); } - public void setRecyclerView(RecyclerView recyclerView) { - mRecyclerView = recyclerView; - initRecyclerViewOnScrollListener(); - } - - public void initRecyclerViewOnScrollListener() { - mOnScrollListener = new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - requestLayout(); - - mHandle.setEnabled(true); - if (!animatingIn && getTranslationX() != 0) { - if (mAnimator != null && mAnimator.isStarted()) { - mAnimator.cancel(); - } - mAnimator = new AnimatorSet(); - ObjectAnimator animator = ObjectAnimator.ofFloat(FastScroller.this, View.TRANSLATION_X, 0); - animator.setInterpolator(new LinearOutSlowInInterpolator()); - animator.setDuration(100); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - animatingIn = false; - } - }); - animatingIn = true; - mAnimator.play(animator); - mAnimator.start(); - } - postAutoHide(); - } - }; - mRecyclerView.addOnScrollListener(mOnScrollListener); - } - - public void setOnHandleTouchListener(OnTouchListener listener) { - mOnTouchListener = listener; - } - - private void postAutoHide() { - if (mRecyclerView != null) { - mRecyclerView.removeCallbacks(mHide); - mRecyclerView.postDelayed(mHide, 1500); + private class ScrollListener extends OnScrollListener { + @Override + public void onScrolled(RecyclerView rv, int dx, int dy) { + updateHandlePosition(); + showIfHidden(); + scheduleHide(); } } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - int scrollOffset = mRecyclerView.computeVerticalScrollOffset(); - int verticalScrollRange = mRecyclerView.computeVerticalScrollRange() - + mRecyclerView.getPaddingBottom(); - - int barHeight = getBarHeight(); - float ratio = (float) scrollOffset / (verticalScrollRange - barHeight); - - int calculatedHandleHeight = (int) ((float) barHeight / verticalScrollRange * barHeight); - if (calculatedHandleHeight < mMinScrollHandleHeight) { - calculatedHandleHeight = mMinScrollHandleHeight; - } - - if (calculatedHandleHeight >= barHeight) { - setTranslationX(mHiddenTranslationX); - return; - } - - float y = ratio * (barHeight - calculatedHandleHeight); - - mHandle.layout(mHandle.getLeft(), (int) y, mHandle.getRight(), (int) y + calculatedHandleHeight); - } - - public void updateRvScroll(int dY) { - if (mRecyclerView != null && mHandle != null) { - try { - mRecyclerView.scrollBy(0, dY); - } catch (Throwable t) { - t.printStackTrace(); - } - } - } - - private int getBarHeight() { - return mBar.getHeight(); - } } \ No newline at end of file diff --git a/app/src/main/res/layout/vertical_recycler_fast_scroller_layout.xml b/app/src/main/res/layout/vertical_recycler_fast_scroller_layout.xml index 147373d0..765bbfaa 100644 --- a/app/src/main/res/layout/vertical_recycler_fast_scroller_layout.xml +++ b/app/src/main/res/layout/vertical_recycler_fast_scroller_layout.xml @@ -1,20 +1,20 @@ + android:alpha="0.3" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 35956e6e..18702535 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -51,9 +51,10 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout 128dp 86dp - 48dp 8dp - 40dp + 8dp + + 16dp 0dp