Refactor fast scroll
Completely write my own fast scroller thumb and also redo how the fast scroller is configured in SongsFragment.
This commit is contained in:
parent
a9765d2ad6
commit
917540e626
10 changed files with 225 additions and 143 deletions
|
@ -12,6 +12,7 @@ import coil.request.ImageRequest
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
@ -53,8 +54,12 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
/**
|
/**
|
||||||
* Custom extension function similar to the stock coil load extensions, but handles whether
|
* Custom extension function similar to the stock coil load extensions, but handles whether
|
||||||
* to show images and custom fetchers.
|
* to show images and custom fetchers.
|
||||||
|
* @param T Any datatype that inherits [BaseModel]
|
||||||
|
* @param data The data itself
|
||||||
|
* @param error Drawable resource to use when loading failed/should not occur.
|
||||||
|
* @param fetcher Required fetcher that uses [T] as its datatype
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : Any> ImageView.load(
|
inline fun <reified T : BaseModel> ImageView.load(
|
||||||
data: T,
|
data: T,
|
||||||
@DrawableRes error: Int,
|
@DrawableRes error: Int,
|
||||||
fetcher: Fetcher<T>,
|
fetcher: Fetcher<T>,
|
||||||
|
@ -63,7 +68,6 @@ inline fun <reified T : Any> ImageView.load(
|
||||||
|
|
||||||
if (!settingsManager.showCovers) {
|
if (!settingsManager.showCovers) {
|
||||||
setImageResource(error)
|
setImageResource(error)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
private val mPosition = MutableLiveData(0L)
|
private val mPosition = MutableLiveData(0L)
|
||||||
|
|
||||||
// Queue
|
// Queue
|
||||||
private val mQueue = MutableLiveData(mutableListOf<Song>())
|
private val mQueue = MutableLiveData(listOf<Song>())
|
||||||
private val mUserQueue = MutableLiveData(mutableListOf<Song>())
|
private val mUserQueue = MutableLiveData(listOf<Song>())
|
||||||
private val mIndex = MutableLiveData(0)
|
private val mIndex = MutableLiveData(0)
|
||||||
private val mMode = MutableLiveData(PlaybackMode.ALL_SONGS)
|
private val mMode = MutableLiveData(PlaybackMode.ALL_SONGS)
|
||||||
|
|
||||||
|
@ -64,9 +64,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
val position: LiveData<Long> get() = mPosition
|
val position: LiveData<Long> get() = mPosition
|
||||||
|
|
||||||
/** The current queue determined by [mode] and [parent] */
|
/** The current queue determined by [mode] and [parent] */
|
||||||
val queue: LiveData<MutableList<Song>> get() = mQueue
|
val queue: LiveData<List<Song>> get() = mQueue
|
||||||
/** The queue created by the user. */
|
/** The queue created by the user. */
|
||||||
val userQueue: LiveData<MutableList<Song>> get() = mUserQueue
|
val userQueue: LiveData<List<Song>> get() = mUserQueue
|
||||||
/** The current [PlaybackMode] that also determines the queue */
|
/** The current [PlaybackMode] that also determines the queue */
|
||||||
val mode: LiveData<PlaybackMode> get() = mMode
|
val mode: LiveData<PlaybackMode> get() = mMode
|
||||||
/** Whether playback is originating from the user-generated queue or not */
|
/** Whether playback is originating from the user-generated queue or not */
|
||||||
|
@ -451,11 +451,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueUpdate(queue: MutableList<Song>) {
|
override fun onQueueUpdate(queue: List<Song>) {
|
||||||
mQueue.value = queue
|
mQueue.value = queue
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUserQueueUpdate(userQueue: MutableList<Song>) {
|
override fun onUserQueueUpdate(userQueue: List<Song>) {
|
||||||
mUserQueue.value = userQueue
|
mUserQueue.value = userQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -101,9 +101,9 @@ class PlaybackStateManager private constructor() {
|
||||||
/** The current playback progress */
|
/** The current playback progress */
|
||||||
val position: Long get() = mPosition
|
val position: Long get() = mPosition
|
||||||
/** The current queue determined by [parent] and [mode] */
|
/** The current queue determined by [parent] and [mode] */
|
||||||
val queue: MutableList<Song> get() = mQueue
|
val queue: List<Song> get() = mQueue
|
||||||
/** The queue created by the user. */
|
/** The queue created by the user. */
|
||||||
val userQueue: MutableList<Song> get() = mUserQueue
|
val userQueue: List<Song> get() = mUserQueue
|
||||||
/** The current index of the queue */
|
/** The current index of the queue */
|
||||||
val index: Int get() = mIndex
|
val index: Int get() = mIndex
|
||||||
/** The current [PlaybackMode] */
|
/** The current [PlaybackMode] */
|
||||||
|
@ -116,7 +116,7 @@ class PlaybackStateManager private constructor() {
|
||||||
val loopMode: LoopMode get() = mLoopMode
|
val loopMode: LoopMode get() = mLoopMode
|
||||||
/** Whether this instance has already been restored */
|
/** Whether this instance has already been restored */
|
||||||
val isRestored: Boolean get() = mIsRestored
|
val isRestored: Boolean get() = mIsRestored
|
||||||
/** Whether this instance has started playing or not */
|
/** Whether playback has begun in this instance during **PlaybackService's Lifecycle.** */
|
||||||
val hasPlayed: Boolean get() = mHasPlayed
|
val hasPlayed: Boolean get() = mHasPlayed
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
@ -788,8 +788,8 @@ class PlaybackStateManager private constructor() {
|
||||||
fun onSongUpdate(song: Song?) {}
|
fun onSongUpdate(song: Song?) {}
|
||||||
fun onParentUpdate(parent: Parent?) {}
|
fun onParentUpdate(parent: Parent?) {}
|
||||||
fun onPositionUpdate(position: Long) {}
|
fun onPositionUpdate(position: Long) {}
|
||||||
fun onQueueUpdate(queue: MutableList<Song>) {}
|
fun onQueueUpdate(queue: List<Song>) {}
|
||||||
fun onUserQueueUpdate(userQueue: MutableList<Song>) {}
|
fun onUserQueueUpdate(userQueue: List<Song>) {}
|
||||||
fun onModeUpdate(mode: PlaybackMode) {}
|
fun onModeUpdate(mode: PlaybackMode) {}
|
||||||
fun onIndexUpdate(index: Int) {}
|
fun onIndexUpdate(index: Int) {}
|
||||||
fun onPlayingUpdate(isPlaying: Boolean) {}
|
fun onPlayingUpdate(isPlaying: Boolean) {}
|
||||||
|
|
|
@ -252,7 +252,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoopUpdate(loopMode: LoopMode) {
|
override fun onLoopUpdate(loopMode: LoopMode) {
|
||||||
player.setLoopMode(loopMode)
|
player.repeatMode = if (loopMode == LoopMode.NONE) {
|
||||||
|
Player.REPEAT_MODE_OFF
|
||||||
|
} else {
|
||||||
|
Player.REPEAT_MODE_ALL
|
||||||
|
}
|
||||||
|
|
||||||
if (!settingsManager.useAltNotifAction) {
|
if (!settingsManager.useAltNotifAction) {
|
||||||
notification.setLoop(this, loopMode)
|
notification.setLoop(this, loopMode)
|
||||||
|
@ -344,27 +348,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
onLoopUpdate(playbackManager.loopMode)
|
onLoopUpdate(playbackManager.loopMode)
|
||||||
onSongUpdate(playbackManager.song)
|
onSongUpdate(playbackManager.song)
|
||||||
onSeek(playbackManager.position)
|
onSeek(playbackManager.position)
|
||||||
|
|
||||||
/*
|
|
||||||
Old Manual restore code, restore this if the above causes bugs
|
|
||||||
notification.setParent(this, playbackManager.parent)
|
|
||||||
notification.setPlaying(this, playbackManager.isPlaying)
|
|
||||||
|
|
||||||
if (settingsManager.useAltNotifAction) {
|
|
||||||
notification.setShuffle(this, playbackManager.isShuffling)
|
|
||||||
} else {
|
|
||||||
notification.setLoop(this, playbackManager.loopMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
player.setLoopMode(playbackManager.loopMode)
|
|
||||||
|
|
||||||
playbackManager.song?.let { song ->
|
|
||||||
notification.setMetadata(this, song, settingsManager.colorizeNotif) {}
|
|
||||||
|
|
||||||
player.setMediaItem(MediaItem.fromUri(song.id.toURI()))
|
|
||||||
player.seekTo(playbackManager.position)
|
|
||||||
player.prepare()
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -406,17 +389,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut to transform a [LoopMode] into a player repeat mode
|
|
||||||
*/
|
|
||||||
private fun Player.setLoopMode(mode: LoopMode) {
|
|
||||||
repeatMode = if (mode == LoopMode.NONE) {
|
|
||||||
Player.REPEAT_MODE_OFF
|
|
||||||
} else {
|
|
||||||
Player.REPEAT_MODE_ALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bring the service into the foreground and show the notification, or refresh the notification.
|
* Bring the service into the foreground and show the notification, or refresh the notification.
|
||||||
*/
|
*/
|
||||||
|
|
112
app/src/main/java/org/oxycblt/auxio/songs/CobaltScrollThumb.kt
Normal file
112
app/src/main/java/org/oxycblt/auxio/songs/CobaltScrollThumb.kt
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package org.oxycblt.auxio.songs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||||
|
import androidx.dynamicanimation.animation.SpringAnimation
|
||||||
|
import androidx.dynamicanimation.animation.SpringForce
|
||||||
|
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
||||||
|
import com.reddit.indicatorfastscroll.FastScrollerView
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.addIndicatorCallback
|
||||||
|
import org.oxycblt.auxio.ui.inflater
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A slimmed-down variant of [com.reddit.indicatorfastscroll.FastScrollerThumbView] designed
|
||||||
|
* specifically for Auxio. Also fixes a memory leak that occurs from a bug fix they
|
||||||
|
* added.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class CobaltScrollThumb @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = -1
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||||
|
private val thumbView: ViewGroup
|
||||||
|
private val textView: TextView
|
||||||
|
private val thumbAnim: SpringAnimation
|
||||||
|
|
||||||
|
init {
|
||||||
|
context.inflater.inflate(R.layout.fast_scroller_thumb_view, this, true)
|
||||||
|
|
||||||
|
val accent = Accent.get().getStateList(context)
|
||||||
|
|
||||||
|
thumbView = findViewById<ViewGroup>(R.id.fast_scroller_thumb).apply {
|
||||||
|
textView = findViewById(R.id.fast_scroller_thumb_text)
|
||||||
|
|
||||||
|
backgroundTintList = accent
|
||||||
|
|
||||||
|
// Workaround for API 21 tint bug
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
(background as GradientDrawable).apply {
|
||||||
|
mutate()
|
||||||
|
color = accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.apply {
|
||||||
|
isVisible = true
|
||||||
|
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_ThumbIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbAnim = SpringAnimation(thumbView, DynamicAnimation.TRANSLATION_Y).apply {
|
||||||
|
spring = SpringForce().also {
|
||||||
|
it.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isActivated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up this view with a [FastScrollerView]. Should only be called once.
|
||||||
|
*/
|
||||||
|
fun setup(scrollView: FastScrollerView) {
|
||||||
|
scrollView.addIndicatorCallback { indicator, centerY, _ ->
|
||||||
|
thumbAnim.animateToFinalPosition(centerY.toFloat() - (thumbView.measuredHeight / 2))
|
||||||
|
|
||||||
|
if (indicator is FastScrollItemIndicator.Text) {
|
||||||
|
textView.text = indicator.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ClickableViewAccessibility")
|
||||||
|
scrollView.setOnTouchListener { v, event ->
|
||||||
|
scrollView.onTouchEvent(event)
|
||||||
|
scrollView.performClick()
|
||||||
|
|
||||||
|
val action = event.actionMasked
|
||||||
|
|
||||||
|
// If we arent deselecting the scroll view, determine if we are selecting an item.
|
||||||
|
isActivated = if (
|
||||||
|
action != MotionEvent.ACTION_UP && action != MotionEvent.ACTION_CANCEL
|
||||||
|
) isPointerOnItem(scrollView, event.y.toInt()) else false
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hack that determines whether the pointer is currently on the [scrollView] or not.
|
||||||
|
*/
|
||||||
|
private fun isPointerOnItem(scrollView: FastScrollerView, touchY: Int): Boolean {
|
||||||
|
scrollView.children.forEach { child ->
|
||||||
|
if (touchY in (child.top until child.bottom)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
||||||
import com.reddit.indicatorfastscroll.FastScrollerView
|
import com.reddit.indicatorfastscroll.FastScrollerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
@ -18,6 +19,7 @@ import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.addIndicatorCallback
|
||||||
import org.oxycblt.auxio.ui.canScroll
|
import org.oxycblt.auxio.ui.canScroll
|
||||||
import org.oxycblt.auxio.ui.getSpans
|
import org.oxycblt.auxio.ui.getSpans
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
|
@ -57,9 +59,7 @@ class SongsFragment : Fragment() {
|
||||||
if (it.itemId == R.id.action_shuffle) {
|
if (it.itemId == R.id.action_shuffle) {
|
||||||
playbackModel.shuffleAll()
|
playbackModel.shuffleAll()
|
||||||
true
|
true
|
||||||
}
|
} else false
|
||||||
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,26 +82,18 @@ class SongsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFastScroller(binding)
|
binding.songFastScroll.setup(binding.songRecycler, binding.songFastScrollThumb)
|
||||||
|
|
||||||
logD("Fragment created.")
|
logD("Fragment created.")
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
requireView().rootView.clearFocus()
|
|
||||||
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go through the fast scroller setup process.
|
* Perform the (Frustratingly Long and Complicated) FastScrollerView setup.
|
||||||
* @param binding Binding required
|
|
||||||
*/
|
*/
|
||||||
private fun setupFastScroller(binding: FragmentSongsBinding) {
|
private fun FastScrollerView.setup(recycler: RecyclerView, thumb: CobaltScrollThumb) {
|
||||||
binding.songFastScroll.apply {
|
var concatInterval: Int = -1
|
||||||
var concatInterval = -1
|
|
||||||
|
|
||||||
// API 22 and below don't support the state color, so just use the accent.
|
// API 22 and below don't support the state color, so just use the accent.
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
|
@ -109,25 +101,9 @@ class SongsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupWithRecyclerView(
|
setupWithRecyclerView(
|
||||||
binding.songRecycler,
|
recycler,
|
||||||
{ pos ->
|
{ pos ->
|
||||||
val item = musicStore.songs[pos]
|
val char = musicStore.songs[pos].name.first
|
||||||
|
|
||||||
// If the item starts with "the"/"a", then actually use the character after that
|
|
||||||
// as its initial. Yes, this is stupidly western-centric but the code [hopefully]
|
|
||||||
// shouldn't run with other languages.
|
|
||||||
val char: Char = if (item.name.length > 5 &&
|
|
||||||
item.name.startsWith("the ", ignoreCase = true)
|
|
||||||
) {
|
|
||||||
item.name[4].toUpperCase()
|
|
||||||
} else if (item.name.length > 3 &&
|
|
||||||
item.name.startsWith("a ", ignoreCase = true)
|
|
||||||
) {
|
|
||||||
item.name[2].toUpperCase()
|
|
||||||
} else {
|
|
||||||
// If it doesn't begin with that word, then just use the first character.
|
|
||||||
item.name[0].toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use "#" if the character is a digit, also has the nice side-effect of
|
// Use "#" if the character is a digit, also has the nice side-effect of
|
||||||
// truncating extra numbers.
|
// truncating extra numbers.
|
||||||
|
@ -136,12 +112,11 @@ class SongsFragment : Fragment() {
|
||||||
} else {
|
} else {
|
||||||
FastScrollItemIndicator.Text(char.toString())
|
FastScrollItemIndicator.Text(char.toString())
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
null, false
|
||||||
)
|
)
|
||||||
|
|
||||||
showIndicator = { _, i, total ->
|
showIndicator = { _, i, total ->
|
||||||
var isGood = true
|
|
||||||
|
|
||||||
if (concatInterval == -1) {
|
if (concatInterval == -1) {
|
||||||
// If the scroller size is too small to contain all the entries, truncate entries
|
// If the scroller size is too small to contain all the entries, truncate entries
|
||||||
// so that the fast scroller entries fit.
|
// so that the fast scroller entries fit.
|
||||||
|
@ -150,46 +125,46 @@ class SongsFragment : Fragment() {
|
||||||
if (total > maxEntries.toInt()) {
|
if (total > maxEntries.toInt()) {
|
||||||
concatInterval = ceil(total / maxEntries).toInt()
|
concatInterval = ceil(total / maxEntries).toInt()
|
||||||
|
|
||||||
logD("More entries than screen space, truncating by $concatInterval.")
|
|
||||||
|
|
||||||
check(concatInterval > 1) {
|
check(concatInterval > 1) {
|
||||||
"ConcatInterval was one despite truncation being needed"
|
"Needed to truncate, but concatInterval was 1 or lower anyway"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logD("More entries than screen space, truncating by $concatInterval.")
|
||||||
} else {
|
} else {
|
||||||
concatInterval = 1
|
concatInterval = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((i % concatInterval) != 0) {
|
// Any items that need to be truncated will be hidden
|
||||||
isGood = false
|
(i % concatInterval) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
isGood
|
addIndicatorCallback { _, _, pos ->
|
||||||
}
|
recycler.apply {
|
||||||
|
|
||||||
useDefaultScroller = false
|
|
||||||
|
|
||||||
addIndicatorCallback { pos ->
|
|
||||||
binding.songRecycler.apply {
|
|
||||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0)
|
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0)
|
||||||
|
|
||||||
stopScroll()
|
stopScroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thumb.setup(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.songFastScrollThumb.setupWithFastScroller(binding.songFastScroll)
|
/**
|
||||||
|
* Dumb shortcut for getting the first letter in a string, while regarding certain
|
||||||
|
* semantics when it comes to articles.
|
||||||
|
*/
|
||||||
|
private val String.first: Char get() {
|
||||||
|
// If the name actually starts with "The" or "A", get the character *after* that word.
|
||||||
|
// Yes, this is stupidly english centric but it wont run with other languages.
|
||||||
|
if (length > 5 && startsWith("the ", true)) {
|
||||||
|
return get(4).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FastScrollerView.addIndicatorCallback(callback: (pos: Int) -> Unit) {
|
if (length > 3 && startsWith("a ", true)) {
|
||||||
itemIndicatorSelectedCallbacks.add(
|
return get(2).toUpperCase()
|
||||||
object : FastScrollerView.ItemIndicatorSelectedCallback {
|
|
||||||
override fun onItemIndicatorSelected(
|
|
||||||
indicator: FastScrollItemIndicator,
|
|
||||||
indicatorCenterY: Int,
|
|
||||||
itemPosition: Int
|
|
||||||
) = callback(itemPosition)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
return get(0).toUpperCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
||||||
|
import com.reddit.indicatorfastscroll.FastScrollerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.logE
|
import org.oxycblt.auxio.logE
|
||||||
|
|
||||||
|
@ -121,6 +123,22 @@ fun String.createToast(context: Context) {
|
||||||
Toast.makeText(context.applicationContext, this, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context.applicationContext, this, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut that allows me to add a indicator callback to [FastScrollerView] without
|
||||||
|
* the nightmarish boilerplate that entails.
|
||||||
|
*/
|
||||||
|
fun FastScrollerView.addIndicatorCallback(
|
||||||
|
callback: (indicator: FastScrollItemIndicator, centerY: Int, pos: Int) -> Unit
|
||||||
|
) {
|
||||||
|
itemIndicatorSelectedCallbacks += object : FastScrollerView.ItemIndicatorSelectedCallback {
|
||||||
|
override fun onItemIndicatorSelected(
|
||||||
|
indicator: FastScrollItemIndicator,
|
||||||
|
indicatorCenterY: Int,
|
||||||
|
itemPosition: Int
|
||||||
|
) = callback(indicator, indicatorCenterY, itemPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- CONFIGURATION ---
|
// --- CONFIGURATION ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/song_toolbar" />
|
app:layout_constraintTop_toBottomOf="@+id/song_toolbar" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.recycler.NoLeakThumbView
|
<org.oxycblt.auxio.songs.CobaltScrollThumb
|
||||||
android:id="@+id/song_fast_scroll_thumb"
|
android:id="@+id/song_fast_scroll_thumb"
|
||||||
android:layout_width="@dimen/width_thumb_view"
|
android:layout_width="@dimen/width_thumb_view"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
|
|
@ -100,8 +100,8 @@
|
||||||
|
|
||||||
<!-- Fast scroll theme -->
|
<!-- Fast scroll theme -->
|
||||||
<style name="FastScrollTheme" parent="Widget.IndicatorFastScroll.FastScroller">
|
<style name="FastScrollTheme" parent="Widget.IndicatorFastScroll.FastScroller">
|
||||||
<item name="android:textColor">@color/color_scroll_tints</item>
|
|
||||||
<item name="android:textAppearance">@style/TextAppearance.FastScroll</item>
|
<item name="android:textAppearance">@style/TextAppearance.FastScroll</item>
|
||||||
|
<item name="android:textColor">@color/color_scroll_tints</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Fast scroll text appearance -->
|
<!-- Fast scroll text appearance -->
|
||||||
|
@ -112,6 +112,7 @@
|
||||||
<!-- Fast scroll thumb appearance -->
|
<!-- Fast scroll thumb appearance -->
|
||||||
<style name="TextAppearance.ThumbIndicator" parent="TextAppearance.FastScroll">
|
<style name="TextAppearance.ThumbIndicator" parent="TextAppearance.FastScroll">
|
||||||
<item name="android:textSize">@dimen/text_size_thumb</item>
|
<item name="android:textSize">@dimen/text_size_thumb</item>
|
||||||
|
<item name="android:textColor">?android:attr/windowBackground</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Style for the general item background -->
|
<!-- Style for the general item background -->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.4.30'
|
ext.kotlin_version = '1.4.31'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
|
Loading…
Reference in a new issue