feat: added fast scrollbar to folder navigation screen.

This commit is contained in:
antonio 2023-08-01 10:34:39 +02:00
parent b267b904cc
commit 4ad2722e81
7 changed files with 290 additions and 9 deletions

View file

@ -0,0 +1,197 @@
package com.cappielloantonio.tempo.helper.recyclerview;
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 FastScrollbar 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 FastScrollbar(final Context context, final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public FastScrollbar(final Context context) {
super(context);
init(context);
}
public FastScrollbar(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();
}
}

View file

@ -9,16 +9,17 @@ import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.databinding.ItemLibraryMusicIndexBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.helper.recyclerview.FastScrollbar;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.Artist;
import com.cappielloantonio.tempo.util.Constants;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@UnstableApi
public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.ViewHolder> {
public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.ViewHolder> implements FastScrollbar.BubbleTextGetter {
private final ClickCallback click;
private List<Artist> artists;
@ -41,10 +42,10 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
holder.item.musicIndexTitleTextView.setText(artist.getName());
CustomGlideRequest.Builder
/* CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getName())
.build()
.into(holder.item.musicIndexCoverImageView);
.into(holder.item.musicIndexCoverImageView); */
}
@Override
@ -57,6 +58,11 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
notifyDataSetChanged();
}
@Override
public String getTextToShowInBubble(int pos) {
return Character.toString(Objects.requireNonNull(artists.get(pos).getName().toUpperCase()).charAt(0));
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryMusicIndexBinding item;

View file

@ -96,6 +96,9 @@ public class IndexFragment extends Fragment implements ClickCallback {
musicIndexAdapter.setItems(IndexUtil.getArtist(indexes));
}
});
bind.fastScrollbar.setRecyclerView(bind.indexRecyclerView);
bind.fastScrollbar.setViewsToUse(R.layout.layout_fast_scrollbar, R.id.fastscroller_bubble, R.id.fastscroller_handle);
}
@Override

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="44dp"
android:bottomRightRadius="0px"
android:topLeftRadius="44dp"
android:topRightRadius="44dp" />
<solid android:color="?attr/colorPrimary" />
<size
android:width="88dp"
android:height="88dp" />
</shape>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="?attr/colorPrimary" />
<size android:width="4dp" android:height="32dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="?attr/colorPrimaryContainer" />
<size android:width="4dp" android:height="32dp" />
</shape>
</item>
</selector>

View file

@ -47,13 +47,30 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/index_recycler_view"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/index_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.cappielloantonio.tempo.helper.recyclerview.FastScrollbar
android:id="@+id/fast_scrollbar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/global_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/fastscroller_bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/fast_scrollbar_bubble"
android:gravity="center"
android:textColor="?attr/colorOnPrimary"
android:textSize="48sp"
android:visibility="visible"
tools:text="A" />
<ImageView
android:id="@+id/fastscroller_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:src="@drawable/fast_scrollbar_handle" />
</merge>