mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +00:00
feat: added fast scrollbar to folder navigation screen.
This commit is contained in:
parent
b267b904cc
commit
4ad2722e81
7 changed files with 290 additions and 9 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
app/src/main/res/drawable/fast_scrollbar_bubble.xml
Normal file
15
app/src/main/res/drawable/fast_scrollbar_bubble.xml
Normal 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>
|
||||
17
app/src/main/res/drawable/fast_scrollbar_handle.xml
Normal file
17
app/src/main/res/drawable/fast_scrollbar_handle.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
26
app/src/main/res/layout/layout_fast_scrollbar.xml
Normal file
26
app/src/main/res/layout/layout_fast_scrollbar.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue