package com.fastaccess.ui.widgets.recyclerview.scroll; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.StateListDrawable; import android.support.annotation.AttrRes; import android.support.annotation.ColorInt; import android.support.v4.view.GravityCompat; import android.support.v4.view.animation.FastOutLinearInInterpolator; import android.support.v4.view.animation.LinearOutSlowInInterpolator; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import com.fastaccess.R; /** * Created by thermatk on 17/04/2017. * Original source: https://github.com/plusCubed/recycler-fast-scroll */ public class RecyclerFastScroller extends FrameLayout { private static final int DEFAULT_AUTO_HIDE_DELAY = 1500; private final View mBar; private final View mHandle; final int mHiddenTranslationX; private final Runnable mHide; private final int mMinScrollHandleHeight; RecyclerView mRecyclerView; AnimatorSet mAnimator; boolean mAnimatingIn; private int mHideDelay; private boolean mHidingEnabled; private int mHandleNormalColor; private int mHandlePressedColor; private int mBarColor; private int mTouchTargetWidth; private int mBarInset; private boolean mHideOverride; private RecyclerView.Adapter mAdapter; private RecyclerView.AdapterDataObserver mAdapterObserver = new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { super.onChanged(); requestLayout(); } }; public RecyclerFastScroller(Context context) { this(context, null); } public RecyclerFastScroller(Context context, AttributeSet attrs) { super(context, attrs, 0); mBarColor = resolveColor(context, R.attr.colorControlNormal); mHandleNormalColor = resolveColor(context, R.attr.colorControlNormal); mHandlePressedColor = resolveColor(context, R.attr.colorAccent); mTouchTargetWidth = convertDpToPx(context, 24); mHideDelay = DEFAULT_AUTO_HIDE_DELAY; mHidingEnabled = true; int fortyEightDp = convertDpToPx(context, 48); setLayoutParams(new ViewGroup.LayoutParams(fortyEightDp, ViewGroup.LayoutParams.MATCH_PARENT)); mBar = new View(context); mHandle = new View(context); addView(mBar); addView(mHandle); int eightDp = convertDpToPx(getContext(), 8); mBarInset = mTouchTargetWidth - eightDp; if (mTouchTargetWidth > fortyEightDp) { throw new RuntimeException("Touch target width cannot be larger than 48dp!"); } mBar.setLayoutParams(new LayoutParams(mTouchTargetWidth, ViewGroup.LayoutParams.MATCH_PARENT, GravityCompat.END)); mHandle.setLayoutParams(new LayoutParams(mTouchTargetWidth, ViewGroup.LayoutParams.MATCH_PARENT, GravityCompat.END)); updateHandleColorsAndInset(); updateBarColorAndInset(); mMinScrollHandleHeight = fortyEightDp; mHiddenTranslationX = (isRTL(getContext()) ? -1 : 1) * eightDp; mHide = () -> { if (!mHandle.isPressed()) { if (mAnimator != null && mAnimator.isStarted()) { mAnimator.cancel(); } mAnimator = new AnimatorSet(); ObjectAnimator animator2 = ObjectAnimator.ofFloat(RecyclerFastScroller.this, View.TRANSLATION_X, mHiddenTranslationX); animator2.setInterpolator(new FastOutLinearInInterpolator()); animator2.setDuration(150); mHandle.setEnabled(false); mAnimator.play(animator2); mAnimator.start(); } }; mHandle.setOnTouchListener(new OnTouchListener() { private float mInitialBarHeight; private float mLastPressedYAdjustedToInitial; @Override public boolean onTouch(View v, MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mHandle.setPressed(true); mRecyclerView.stopScroll(); mInitialBarHeight = mBar.getHeight(); mLastPressedYAdjustedToInitial = event.getY() + mHandle.getY() + mBar.getY(); } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { float newHandlePressedY = event.getY() + mHandle.getY() + mBar.getY(); int barHeight = mBar.getHeight(); float newHandlePressedYAdjustedToInitial = newHandlePressedY + (mInitialBarHeight - barHeight); float deltaPressedYFromLastAdjustedToInitial = newHandlePressedYAdjustedToInitial - mLastPressedYAdjustedToInitial; int dY = (int) ((deltaPressedYFromLastAdjustedToInitial / mInitialBarHeight) * (mRecyclerView.computeVerticalScrollRange())); if (mRecyclerView != null) { try { mRecyclerView.scrollBy(0, dY); } catch (Throwable t) { t.printStackTrace(); } } mLastPressedYAdjustedToInitial = newHandlePressedYAdjustedToInitial; } else if (event.getActionMasked() == MotionEvent.ACTION_UP) { mLastPressedYAdjustedToInitial = -1; mRecyclerView.stopNestedScroll(); mHandle.setPressed(false); postAutoHide(); } return true; } }); setTranslationX(mHiddenTranslationX); } private void updateHandleColorsAndInset() { StateListDrawable drawable = new StateListDrawable(); if (!isRTL(getContext())) { drawable.addState(View.PRESSED_ENABLED_STATE_SET, new InsetDrawable(new ColorDrawable(mHandlePressedColor), mBarInset, 0, 0, 0)); drawable.addState(View.EMPTY_STATE_SET, new InsetDrawable(new ColorDrawable(mHandleNormalColor), mBarInset, 0, 0, 0)); } else { drawable.addState(View.PRESSED_ENABLED_STATE_SET, new InsetDrawable(new ColorDrawable(mHandlePressedColor), 0, 0, mBarInset, 0)); drawable.addState(View.EMPTY_STATE_SET, new InsetDrawable(new ColorDrawable(mHandleNormalColor), 0, 0, mBarInset, 0)); } mHandle.setBackground(drawable); } private void updateBarColorAndInset() { Drawable drawable; if (!isRTL(getContext())) { drawable = new InsetDrawable(new ColorDrawable(mBarColor), mBarInset, 0, 0, 0); } else { drawable = new InsetDrawable(new ColorDrawable(mBarColor), 0, 0, mBarInset, 0); } drawable.setAlpha(57); mBar.setBackground(drawable); } public void attachRecyclerView(RecyclerView recyclerView) { mRecyclerView = recyclerView; mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); RecyclerFastScroller.this.show(); } }); if (recyclerView.getAdapter() != null && recyclerView.getAdapter() != mAdapter) { if (mAdapter != null) { mAdapter.unregisterAdapterDataObserver(mAdapterObserver); } recyclerView.getAdapter().registerAdapterDataObserver(mAdapterObserver); mAdapter = recyclerView.getAdapter(); } } /** * Show the fast scroller and hide after delay */ public void show() { requestLayout(); post(new Runnable() { @Override public void run() { if (mHideOverride) { return; } mHandle.setEnabled(true); if (!mAnimatingIn && getTranslationX() != 0) { if (mAnimator != null && mAnimator.isStarted()) { mAnimator.cancel(); } mAnimator = new AnimatorSet(); ObjectAnimator animator = ObjectAnimator.ofFloat(RecyclerFastScroller.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); mAnimatingIn = false; } }); mAnimatingIn = true; mAnimator.play(animator); mAnimator.start(); } postAutoHide(); } }); } void postAutoHide() { if (mRecyclerView != null && mHidingEnabled) { mRecyclerView.removeCallbacks(mHide); mRecyclerView.postDelayed(mHide, mHideDelay); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mRecyclerView == null) return; int scrollOffset = mRecyclerView.computeVerticalScrollOffset(); int verticalScrollRange = mRecyclerView.computeVerticalScrollRange() + mRecyclerView.getPaddingBottom(); int barHeight = mBar.getHeight(); float ratio = (float) scrollOffset / (verticalScrollRange - barHeight); int calculatedHandleHeight = (int) ((float) barHeight / verticalScrollRange * barHeight); if (calculatedHandleHeight < mMinScrollHandleHeight) { calculatedHandleHeight = mMinScrollHandleHeight; } if (calculatedHandleHeight >= barHeight) { setTranslationX(mHiddenTranslationX); mHideOverride = true; return; } mHideOverride = false; float y = ratio * (barHeight - calculatedHandleHeight); mHandle.layout(mHandle.getLeft(), (int) y, mHandle.getRight(), (int) y + calculatedHandleHeight); } private static boolean isRTL(Context context) { return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } @ColorInt public static int resolveColor(Context context, @AttrRes int color) { TypedArray a = context.obtainStyledAttributes(new int[]{color}); int resId = a.getColor(0, 0); a.recycle(); return resId; } public static int convertDpToPx(Context context, float dp) { return (int) (dp * context.getResources().getDisplayMetrics().density + 0.5f); } }