content: package com.lb.recyclerview_fast_scroller;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.ObjectAnimator;
  import android.content.Context;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.view.LayoutInflater;
  import android.view.MotionEvent;
  import android.view.View;
  import android.widget.LinearLayout;
  import android.widget.TextView;
  
  import androidx.annotation.IdRes;
  import androidx.annotation.LayoutRes;
  import androidx.annotation.NonNull;
  import androidx.core.view.ViewCompat;
  import androidx.recyclerview.widget.LinearLayoutManager;
  import androidx.recyclerview.widget.RecyclerView;
  
  public class RecyclerViewFastScroller extends LinearLayout {
      private static final int BUBBLE_ANIMATION_DURATION = 100;
      private static final int TRACK_SNAP_RANGE = 5;
  
      private TextView bubble;
      private View handle;
      private RecyclerView recyclerView;
      private int height;
      private boolean isInitialized = false;
      private ObjectAnimator currentAnimator = null;
  
      private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
          @Override
          public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
              updateBubbleAndHandlePosition();
          }
      };
  
      public interface BubbleTextGetter {
          String getTextToShowInBubble(int pos);
      }
  
      public RecyclerViewFastScroller(final Context context, final AttributeSet attrs, final int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init(context);
      }
  
      public RecyclerViewFastScroller(final Context context) {
          super(context);
          init(context);
      }
  
      public RecyclerViewFastScroller(final Context context, final AttributeSet attrs) {
          super(context, attrs);
          init(context);
      }
  
      protected void init(Context context) {
          if (isInitialized)
              return;
          isInitialized = true;
          setOrientation(HORIZONTAL);
          setClipChildren(false);
      }
  
      public void setViewsToUse(@LayoutRes int layoutResId, @IdRes int bubbleResId, @IdRes int handleResId) {
          final LayoutInflater inflater = LayoutInflater.from(getContext());
          inflater.inflate(layoutResId, this, true);
          bubble = findViewById(bubbleResId);
          if (bubble != null)
              bubble.setVisibility(INVISIBLE);
          handle = findViewById(handleResId);
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
          height = h;
          updateBubbleAndHandlePosition();
      }
  
      @Override
      public boolean onTouchEvent(@NonNull MotionEvent event) {
          final int action = event.getAction();
          switch (action) {
              case MotionEvent.ACTION_DOWN:
                  if (event.getX() < handle.getX() - ViewCompat.getPaddingStart(handle))
                      return false;
                  if (currentAnimator != null)
                      currentAnimator.cancel();
                  if (bubble != null && bubble.getVisibility() == INVISIBLE)
                      showBubble();
                  handle.setSelected(true);
              case MotionEvent.ACTION_MOVE:
                  final float y = event.getY();
                  setBubbleAndHandlePosition(y);
                  setRecyclerViewPosition(y);
                  return true;
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL:
                  handle.setSelected(false);
                  hideBubble();
                  return true;
          }
          return super.onTouchEvent(event);
      }
  
      public void setRecyclerView(final RecyclerView recyclerView) {
          if (this.recyclerView != recyclerView) {
              if (this.recyclerView != null)
                  this.recyclerView.removeOnScrollListener(onScrollListener);
              this.recyclerView = recyclerView;
              if (this.recyclerView == null)
                  return;
              recyclerView.addOnScrollListener(onScrollListener);
          }
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          if (recyclerView != null) {
              recyclerView.removeOnScrollListener(onScrollListener);
              recyclerView = null;
          }
      }
  
      private void setRecyclerViewPosition(float y) {
          if (recyclerView != null) {
              final int itemCount = recyclerView.getAdapter().getItemCount();
              float proportion;
              if (handle.getY() == 0)
                  proportion = 0f;
              else if (handle.getY() + handle.getHeight() >= height - TRACK_SNAP_RANGE)
                  proportion = 1f;
              else
                  proportion = y / (float) height;
              final int targetPos = getValueInRange(0, itemCount - 1, (int) (proportion * (float) itemCount));
              ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0);
              final String bubbleText = ((BubbleTextGetter) recyclerView.getAdapter()).getTextToShowInBubble(targetPos);
              if (bubble != null) {
                  bubble.setText(bubbleText);
                  if (TextUtils.isEmpty(bubbleText)) {
                      hideBubble();
                  } else if (bubble.getVisibility() == View.INVISIBLE) {
                      showBubble();
                  }
              }
          }
      }
  
      private int getValueInRange(int min, int max, int value) {
          int minimum = Math.max(min, value);
          return Math.min(minimum, max);
      }
  
      private void updateBubbleAndHandlePosition() {
          if (bubble == null || handle.isSelected())
              return;
  
          final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
          final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
          float proportion = (float) verticalScrollOffset / ((float) verticalScrollRange - height);
          setBubbleAndHandlePosition(height * proportion);
      }
  
      private void setBubbleAndHandlePosition(float y) {
          final int handleHeight = handle.getHeight();
          handle.setY(getValueInRange(0, height - handleHeight, (int) (y - handleHeight / 2)));
          if (bubble != null) {
              int bubbleHeight = bubble.getHeight();
              bubble.setY(getValueInRange(0, height - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight)));
          }
      }
  
      private void showBubble() {
          if (bubble == null)
              return;
          bubble.setVisibility(VISIBLE);
          if (currentAnimator != null)
              currentAnimator.cancel();
          currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION);
          currentAnimator.start();
      }
  
      private void hideBubble() {
          if (bubble == null)
              return;
          if (currentAnimator != null)
              currentAnimator.cancel();
          currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION);
          currentAnimator.addListener(new AnimatorListenerAdapter() {
              @Override
              public void onAnimationEnd(Animator animation) {
                  super.onAnimationEnd(animation);
                  bubble.setVisibility(INVISIBLE);
                  currentAnimator = null;
              }
  
              @Override
              public void onAnimationCancel(Animator animation) {
                  super.onAnimationCancel(animation);
                  bubble.setVisibility(INVISIBLE);
                  currentAnimator = null;
              }
          });
          currentAnimator.start();
      }
  }
  