content: /*
    This file is part of Subsonic.
      Subsonic is free software: you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
      the Free Software Foundation, either version 3 of the License, or
      (at your option) any later version.
      Subsonic is distributed in the hope that it will be useful,
      but WITHOUT ANY WARRANTY; without even the implied warranty of
      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      GNU General Public License for more details.
      You should have received a copy of the GNU General Public License
      along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
      Copyright 2015 (C) Scott Jackson
  */
  
  package net.nullsum.audinaut.view;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.ObjectAnimator;
  import android.content.Context;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.TypedValue;
  import android.view.LayoutInflater;
  import android.view.MotionEvent;
  import android.view.View;
  import android.widget.LinearLayout;
  import android.widget.TextView;
  
  import androidx.annotation.NonNull;
  import androidx.recyclerview.widget.LinearLayoutManager;
  import androidx.recyclerview.widget.RecyclerView;
  import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
  
  import net.nullsum.audinaut.R;
  
  import static androidx.recyclerview.widget.RecyclerView.OnScrollListener;
  
  public class FastScroller extends LinearLayout {
      private static final String TAG = FastScroller.class.getSimpleName();
      private static final int BUBBLE_ANIMATION_DURATION = 100;
      private final ScrollListener scrollListener = new ScrollListener();
      private TextView bubble;
      private View handle;
      private RecyclerView recyclerView;
      private int height;
      private int visibleRange = -1;
      private RecyclerView.Adapter adapter;
      private AdapterDataObserver adapterObserver;
      private boolean visibleBubble = true;
      private boolean hasScrolled = false;
  
      private ObjectAnimator currentAnimator = null;
  
      public FastScroller(final Context context, final AttributeSet attrs, final int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          initialise(context);
      }
  
      public FastScroller(final Context context) {
          super(context);
          initialise(context);
      }
  
      public FastScroller(final Context context, final AttributeSet attrs) {
          super(context, attrs);
          initialise(context);
      }
  
      private void initialise(Context context) {
          setOrientation(HORIZONTAL);
          setClipChildren(false);
          LayoutInflater inflater = LayoutInflater.from(context);
          inflater.inflate(R.layout.fast_scroller, this, true);
          bubble = findViewById(R.id.fastscroller_bubble);
          handle = findViewById(R.id.fastscroller_handle);
          bubble.setVisibility(INVISIBLE);
          setVisibility(GONE);
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
          height = h;
          visibleRange = -1;
      }
  
      @Override
      public boolean onTouchEvent(@NonNull MotionEvent event) {
          final int action = event.getAction();
          switch (action) {
              case MotionEvent.ACTION_DOWN:
                  if (event.getX() < (handle.getX() - 30)) {
                      return false;
                  }
  
                  if (currentAnimator != null)
                      currentAnimator.cancel();
                  if (bubble.getVisibility() == INVISIBLE) {
                      if (visibleBubble) {
                          showBubble();
                      }
                  } else if (!visibleBubble) {
                      hideBubble();
                  }
                  handle.setSelected(true);
              case MotionEvent.ACTION_MOVE:
                  setRecyclerViewPosition(event.getY());
                  return true;
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL:
                  handle.setSelected(false);
                  hideBubble();
                  return true;
          }
          return super.onTouchEvent(event);
      }
  
      public void attachRecyclerView(RecyclerView recyclerView) {
          this.recyclerView = recyclerView;
          recyclerView.addOnScrollListener(scrollListener);
          registerAdapter();
          visibleRange = -1;
      }
  
      public boolean isAttached() {
          return recyclerView != null;
      }
  
      private void setRecyclerViewPosition(float y) {
          if (recyclerView != null) {
              if (recyclerView.getChildCount() == 0) {
                  return;
              }
  
              int itemCount = recyclerView.getAdapter().getItemCount();
              float proportion = getValueInRange(1f, y / (float) height);
  
              float targetPosFloat = getValueInRange(itemCount - 1, proportion * (float) itemCount);
              int targetPos = (int) targetPosFloat;
  
              // Immediately make sure that the target is visible
              LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
              // layoutManager.scrollToPositionWithOffset(targetPos, 0);
              View firstVisibleView = recyclerView.getChildAt(0);
  
              // Calculate how far through this position we are
              int columns = Math.round(recyclerView.getWidth() / firstVisibleView.getWidth());
              int firstVisiblePosition = recyclerView.getChildAdapterPosition(firstVisibleView);
              int remainder = (targetPos - firstVisiblePosition) % columns;
              float offsetPercentage = (targetPosFloat - targetPos + remainder) / columns;
              if (offsetPercentage < 0) {
                  offsetPercentage = 1 + offsetPercentage;
              }
              int firstVisibleHeight = firstVisibleView.getHeight();
              if (columns > 1) {
                  firstVisibleHeight += (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, GridSpacingDecoration.SPACING, firstVisibleView.getResources().getDisplayMetrics());
              }
              int offset = (int) (offsetPercentage * firstVisibleHeight);
  
              layoutManager.scrollToPositionWithOffset(targetPos, -offset);
              onUpdateScroll(1, 1);
  
              try {
                  String bubbleText = null;
                  RecyclerView.Adapter adapter = recyclerView.getAdapter();
                  if (adapter instanceof BubbleTextGetter) {
                      bubbleText = ((BubbleTextGetter) adapter).getTextToShowInBubble(targetPos);
                  }
  
                  if (bubbleText == null) {
                      visibleBubble = false;
                      bubble.setVisibility(View.INVISIBLE);
                  } else {
                      bubble.setText(bubbleText);
                      bubble.setVisibility(View.VISIBLE);
                      visibleBubble = true;
                  }
              } catch (Exception e) {
                  Log.e(TAG, "Error getting text for bubble", e);
              }
          }
      }
  
      private float getValueInRange(float max, float value) {
          float minimum = Math.max((float) 0, value);
          return Math.min(minimum, max);
      }
  
      private void setBubbleAndHandlePosition(float y) {
          int bubbleHeight = bubble.getHeight();
          int handleHeight = handle.getHeight();
          handle.setY(getValueInRange(height - handleHeight, (int) (y - handleHeight / 2)));
          bubble.setY(getValueInRange(height - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight)));
      }
  
      private void showBubble() {
          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 (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();
      }
  
      private void registerAdapter() {
          RecyclerView.Adapter newAdapter = recyclerView.getAdapter();
          if (newAdapter != adapter) {
              unregisterAdapter();
          }
  
          if (newAdapter != null) {
              adapterObserver = new AdapterDataObserver() {
                  @Override
                  public void onChanged() {
                      visibleRange = -1;
                  }
  
                  @Override
                  public void onItemRangeChanged(int positionStart, int itemCount) {
                      visibleRange = -1;
                  }
  
                  @Override
                  public void onItemRangeInserted(int positionStart, int itemCount) {
                      visibleRange = -1;
                  }
  
                  @Override
                  public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
                      visibleRange = -1;
                  }
  
                  @Override
                  public void onItemRangeRemoved(int positionStart, int itemCount) {
                      visibleRange = -1;
                  }
              };
              newAdapter.registerAdapterDataObserver(adapterObserver);
              adapter = newAdapter;
          }
      }
  
      private void unregisterAdapter() {
          if (adapter != null) {
              adapter.unregisterAdapterDataObserver(adapterObserver);
              adapter = null;
              adapterObserver = null;
          }
      }
  
      private void onUpdateScroll(int dx, int dy) {
          if (recyclerView.getWidth() == 0) {
              return;
          }
          registerAdapter();
  
          View firstVisibleView = recyclerView.getChildAt(0);
          if (firstVisibleView == null) {
              return;
          }
          int firstVisiblePosition = recyclerView.getChildAdapterPosition(firstVisibleView);
  
          int itemCount = recyclerView.getAdapter().getItemCount();
          int columns = Math.round(recyclerView.getWidth() / firstVisibleView.getWidth());
          if (visibleRange == -1) {
              visibleRange = recyclerView.getChildCount();
          }
  
          // Add the percentage of the item the user has scrolled past already
          float pastFirst = -firstVisibleView.getY() / firstVisibleView.getHeight() * columns;
          float position = firstVisiblePosition + pastFirst;
  
          // Scale this so as we move down the visible range gets added to position from 0 -> visible range
          float scaledVisibleRange = position / (float) (itemCount - visibleRange) * visibleRange;
          position += scaledVisibleRange;
  
          float proportion = position / itemCount;
          setBubbleAndHandlePosition(height * proportion);
  
          if ((visibleRange * 2) < itemCount) {
              if (!hasScrolled && (dx > 0 || dy > 0)) {
                  setVisibility(View.VISIBLE);
                  hasScrolled = true;
                  recyclerView.setVerticalScrollBarEnabled(false);
              }
          } else if (hasScrolled) {
              setVisibility(View.GONE);
              hasScrolled = false;
              recyclerView.setVerticalScrollBarEnabled(true);
          }
      }
  
      public interface BubbleTextGetter {
          String getTextToShowInBubble(int position);
      }
  
      private class ScrollListener extends OnScrollListener {
          @Override
          public void onScrolled(RecyclerView rv, int dx, int dy) {
              onUpdateScroll(dx, dy);
          }
      }
  }
  