327 lines
12 KiB
Java
327 lines
12 KiB
Java
/*
|
|
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);
|
|
}
|
|
}
|
|
}
|