Roll custom fast scroller
Drop the old IndicatorFastScroll library from Auxio and replace it with a hyper-specialized variant designed specifically for Auxio. This not only eliminates a source of hacks/problems/bloat, it also removes a dependency on jcenter (Which is shutting down soon)
This commit is contained in:
parent
816dc2394a
commit
02fed16c31
11 changed files with 376 additions and 229 deletions
|
@ -97,9 +97,6 @@ dependencies {
|
||||||
// Material
|
// Material
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
|
|
||||||
// Fast-Scroll
|
|
||||||
implementation 'com.reddit:indicator-fast-scroll:1.3.0'
|
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
implementation 'com.afollestad.material-dialogs:core:3.3.0'
|
implementation 'com.afollestad.material-dialogs:core:3.3.0'
|
||||||
implementation 'com.afollestad.material-dialogs:files:3.3.0'
|
implementation 'com.afollestad.material-dialogs:files:3.3.0'
|
||||||
|
|
|
@ -1,119 +0,0 @@
|
||||||
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.View
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visibility = View.INVISIBLE
|
|
||||||
isActivated = false
|
|
||||||
|
|
||||||
post {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 { _, event ->
|
|
||||||
scrollView.onTouchEvent(event)
|
|
||||||
scrollView.performClick()
|
|
||||||
|
|
||||||
val action = event.actionMasked
|
|
||||||
val actionValid = action != MotionEvent.ACTION_UP && action != MotionEvent.ACTION_CANCEL
|
|
||||||
|
|
||||||
isActivated = if (actionValid) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
81
app/src/main/java/org/oxycblt/auxio/songs/FastScrollThumb.kt
Normal file
81
app/src/main/java/org/oxycblt/auxio/songs/FastScrollThumb.kt
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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.View
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.dynamicanimation.animation.DynamicAnimation
|
||||||
|
import androidx.dynamicanimation.animation.SpringAnimation
|
||||||
|
import androidx.dynamicanimation.animation.SpringForce
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.ViewScrollThumbBinding
|
||||||
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.inflater
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The companion thumb for [FastScrollView]. This does not need any setup, instead pass it as an
|
||||||
|
* argument to [FastScrollView.setup].
|
||||||
|
* This code is fundamentally an adaptation of Reddit's IndicatorFastScroll, targeted towards
|
||||||
|
* Auxio specifically. Check them out here: https://github.com/reddit/IndicatorFastScroll/
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class FastScrollThumb @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = -1
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||||
|
private val thumbAnim: SpringAnimation
|
||||||
|
private val binding = DataBindingUtil.inflate<ViewScrollThumbBinding>(
|
||||||
|
context.inflater, R.layout.view_scroll_thumb, this, true
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val accent = Accent.get().getStateList(context)
|
||||||
|
|
||||||
|
binding.thumbLayout.apply {
|
||||||
|
backgroundTintList = accent
|
||||||
|
|
||||||
|
// Workaround for API 21 tint bug
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
(background as GradientDrawable).apply {
|
||||||
|
mutate()
|
||||||
|
color = accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.thumbText.apply {
|
||||||
|
isVisible = true
|
||||||
|
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_ThumbIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbAnim = SpringAnimation(binding.thumbLayout, DynamicAnimation.TRANSLATION_Y).apply {
|
||||||
|
spring = SpringForce().also {
|
||||||
|
it.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
isActivated = false
|
||||||
|
|
||||||
|
post {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the thumb jump to a new position and update its text to the given [indicator].
|
||||||
|
* This is not meant for use outside of the main [FastScrollView] code, don't use it.
|
||||||
|
*/
|
||||||
|
fun jumpTo(indicator: FastScrollView.Indicator, centerY: Int) {
|
||||||
|
binding.thumbText.text = indicator.char.toString()
|
||||||
|
thumbAnim.animateToFinalPosition(
|
||||||
|
centerY.toFloat() - (binding.thumbLayout.measuredHeight / 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
217
app/src/main/java/org/oxycblt/auxio/songs/FastScrollView.kt
Normal file
217
app/src/main/java/org/oxycblt/auxio/songs/FastScrollView.kt
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package org.oxycblt.auxio.songs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.logD
|
||||||
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.resolveAttr
|
||||||
|
import org.oxycblt.auxio.ui.toColor
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view that allows for quick scrolling through a [RecyclerView] with many items. Unlike other
|
||||||
|
* fast-scrollers, this one displays indicators instead of simply a scroll bar.
|
||||||
|
* This code is fundamentally an adaptation of Reddit's IndicatorFastScroll, targeted towards
|
||||||
|
* Auxio specifically. Check them out here: https://github.com/reddit/IndicatorFastScroll/
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class FastScrollView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = -1
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
// --- BASIC SETUP ---
|
||||||
|
|
||||||
|
private var mRecycler: RecyclerView? = null
|
||||||
|
private var mThumb: FastScrollThumb? = null
|
||||||
|
private var mGetItem: ((Int) -> Char)? = null
|
||||||
|
|
||||||
|
// --- INDICATORS ---
|
||||||
|
|
||||||
|
/** Representation of a single Indicator character in the view */
|
||||||
|
data class Indicator(val char: Char, val pos: Int)
|
||||||
|
|
||||||
|
private var indicators = listOf<Indicator>()
|
||||||
|
|
||||||
|
private val indicatorText: TextView
|
||||||
|
private val activeColor = Accent.get().color.toColor(context)
|
||||||
|
private val inactiveColor = android.R.attr.textColorSecondary.resolveAttr(context)
|
||||||
|
|
||||||
|
// --- STATE ---
|
||||||
|
|
||||||
|
private var hasPostedItemUpdate = false
|
||||||
|
private var lastPos = -1
|
||||||
|
|
||||||
|
init {
|
||||||
|
isFocusableInTouchMode = true
|
||||||
|
isClickable = true
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
|
||||||
|
val textPadding = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, 4F, resources.displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
// Making this entire view a TextView will cause distortions due to the touch calculations
|
||||||
|
// using a height that is not wrapped to the text.
|
||||||
|
indicatorText = AppCompatTextView(context).apply {
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
includeFontPadding = false
|
||||||
|
|
||||||
|
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_FastScroll)
|
||||||
|
setLineSpacing(textPadding, lineSpacingMultiplier)
|
||||||
|
setTextColor(inactiveColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(indicatorText)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up this view with a [RecyclerView] and a corresponding [FastScrollThumb].
|
||||||
|
*/
|
||||||
|
fun setup(recycler: RecyclerView, thumb: FastScrollThumb, getItem: (Int) -> Char) {
|
||||||
|
check(mRecycler == null) { "Only set up this view once." }
|
||||||
|
|
||||||
|
mRecycler = recycler
|
||||||
|
mThumb = thumb
|
||||||
|
mGetItem = getItem
|
||||||
|
|
||||||
|
postIndicatorUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- INDICATOR UPDATES ---
|
||||||
|
|
||||||
|
private fun postIndicatorUpdate() {
|
||||||
|
if (!hasPostedItemUpdate) {
|
||||||
|
hasPostedItemUpdate = true
|
||||||
|
|
||||||
|
post {
|
||||||
|
val recycler = requireNotNull(mRecycler)
|
||||||
|
|
||||||
|
if (recycler.isAttachedToWindow && recycler.adapter != null) {
|
||||||
|
updateIndicators()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPostedItemUpdate = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateIndicators() {
|
||||||
|
val recycler = requireNotNull(mRecycler)
|
||||||
|
val getItem = requireNotNull(mGetItem)
|
||||||
|
|
||||||
|
indicators = 0.until(recycler.adapter!!.itemCount).mapNotNull { pos ->
|
||||||
|
Indicator(getItem(pos), pos)
|
||||||
|
}.distinctBy { it.char }
|
||||||
|
|
||||||
|
val textHeight = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_SP, 14F, resources.displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the scroller size is too small to contain all the entries, truncate entries
|
||||||
|
// so that the fast scroller entries fit.
|
||||||
|
val maxEntries = height / textHeight
|
||||||
|
|
||||||
|
if (indicators.size > maxEntries.toInt()) {
|
||||||
|
val truncateInterval = ceil(indicators.size / maxEntries).toInt()
|
||||||
|
|
||||||
|
check(truncateInterval > 1) {
|
||||||
|
"Needed to truncate, but truncateInterval was 1 or lower anyway"
|
||||||
|
}
|
||||||
|
|
||||||
|
logD("More entries than screen space, truncating by $truncateInterval.")
|
||||||
|
|
||||||
|
indicators = indicators.filterIndexed { index, _ ->
|
||||||
|
index % truncateInterval == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indicatorText.apply {
|
||||||
|
tag = indicators
|
||||||
|
text = indicators.joinToString("\n") { it.char.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TOUCH ---
|
||||||
|
|
||||||
|
@Suppress("ClickableViewAccessibility")
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
performClick()
|
||||||
|
|
||||||
|
val success = handleTouch(event.action, event.y.toInt())
|
||||||
|
|
||||||
|
// Depending on the results, update the visibility of the thumb and the pressed state of
|
||||||
|
// this view.
|
||||||
|
isPressed = success
|
||||||
|
mThumb?.isActivated = success
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun handleTouch(action: Int, touchY: Int): Boolean {
|
||||||
|
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||||
|
indicatorText.setTextColor(inactiveColor)
|
||||||
|
lastPos = -1
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchY in (indicatorText.top until indicatorText.bottom)) {
|
||||||
|
val textHeight = indicatorText.height / indicators.size
|
||||||
|
val indicatorIndex = min(
|
||||||
|
(touchY - indicatorText.top) / textHeight, indicators.lastIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
val centerY = y.toInt() + (textHeight / 2) + (indicatorIndex * textHeight)
|
||||||
|
|
||||||
|
val touchedIndicator = indicators[indicatorIndex]
|
||||||
|
|
||||||
|
selectIndicator(touchedIndicator, centerY)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectIndicator(indicator: Indicator, indicatorCenterY: Int) {
|
||||||
|
if (indicator.pos != lastPos) {
|
||||||
|
lastPos = indicator.pos
|
||||||
|
indicatorText.setTextColor(activeColor)
|
||||||
|
|
||||||
|
// Stop any scroll momentum and snap-scroll to the position
|
||||||
|
mRecycler?.apply {
|
||||||
|
stopScroll()
|
||||||
|
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||||
|
indicator.pos, 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mThumb?.jumpTo(indicator, indicatorCenterY)
|
||||||
|
|
||||||
|
performHapticFeedback(
|
||||||
|
if (Build.VERSION.SDK_INT >= 27) {
|
||||||
|
// Dragging across a scroller is closer to moving a text handle
|
||||||
|
HapticFeedbackConstants.TEXT_HANDLE_MOVE
|
||||||
|
} else {
|
||||||
|
HapticFeedbackConstants.KEYBOARD_TAP
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,21 @@
|
||||||
package org.oxycblt.auxio.songs
|
package org.oxycblt.auxio.songs
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
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.RecyclerView
|
|
||||||
import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
|
||||||
import com.reddit.indicatorfastscroll.FastScrollerView
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
||||||
import org.oxycblt.auxio.logD
|
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.addIndicatorCallback
|
|
||||||
import org.oxycblt.auxio.ui.canScroll
|
import org.oxycblt.auxio.ui.canScroll
|
||||||
|
import org.oxycblt.auxio.ui.fixAnimInfoLeak
|
||||||
import org.oxycblt.auxio.ui.getSpans
|
import org.oxycblt.auxio.ui.getSpans
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that shows a list of all songs on the device.
|
* A [Fragment] that shows a list of all songs on the device.
|
||||||
|
@ -73,75 +65,21 @@ class SongsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.songFastScroll.setup(binding.songRecycler, binding.songFastScrollThumb)
|
binding.songFastScroll.setup(binding.songRecycler, binding.songFastScrollThumb) { pos ->
|
||||||
|
val char = musicStore.songs[pos].name.first
|
||||||
|
|
||||||
|
if (char.isDigit()) '#' else char
|
||||||
|
}
|
||||||
|
|
||||||
logD("Fragment created.")
|
logD("Fragment created.")
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun onDestroyView() {
|
||||||
* Perform the (Frustratingly Long and Complicated) FastScrollerView setup.
|
super.onDestroyView()
|
||||||
* TODO: Roll FastScrollerView yourself and eliminate its dependency, you're already customizing it enough as it is.
|
|
||||||
*/
|
|
||||||
private fun FastScrollerView.setup(recycler: RecyclerView, thumb: CobaltScrollThumb) {
|
|
||||||
var truncateInterval: Int = -1
|
|
||||||
val indicatorTextSize = TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_SP, 14F,
|
|
||||||
resources.displayMetrics
|
|
||||||
)
|
|
||||||
|
|
||||||
// API 22 and below don't support the state color, so just use the accent.
|
fixAnimInfoLeak()
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
|
||||||
textColor = Accent.get().getStateList(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
setupWithRecyclerView(
|
|
||||||
recycler,
|
|
||||||
{ pos ->
|
|
||||||
val char = musicStore.songs[pos].name.first
|
|
||||||
|
|
||||||
FastScrollItemIndicator.Text(
|
|
||||||
// Use "#" if the character is a digit, also has the nice side-effect of
|
|
||||||
// truncating extra numbers.
|
|
||||||
if (char.isDigit()) "#" else char.toString()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
null, false
|
|
||||||
)
|
|
||||||
|
|
||||||
showIndicator = { _, i, total ->
|
|
||||||
if (truncateInterval == -1) {
|
|
||||||
// If the scroller size is too small to contain all the entries, truncate entries
|
|
||||||
// so that the fast scroller entries fit.
|
|
||||||
val maxEntries = (height / (indicatorTextSize + textPadding))
|
|
||||||
|
|
||||||
if (total > maxEntries.toInt()) {
|
|
||||||
truncateInterval = ceil(total / maxEntries).toInt()
|
|
||||||
|
|
||||||
check(truncateInterval > 1) {
|
|
||||||
"Needed to truncate, but truncateInterval was 1 or lower anyway"
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("More entries than screen space, truncating by $truncateInterval.")
|
|
||||||
} else {
|
|
||||||
truncateInterval = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Any items that need to be truncated will be hidden
|
|
||||||
(i % truncateInterval) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
addIndicatorCallback { _, _, pos ->
|
|
||||||
recycler.apply {
|
|
||||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0)
|
|
||||||
|
|
||||||
stopScroll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
thumb.setup(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.graphics.drawable.AnimatedVectorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
@ -23,8 +24,6 @@ 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
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
@ -129,6 +128,24 @@ fun Int.toDrawable(context: Context) = ContextCompat.getDrawable(context, this)
|
||||||
*/
|
*/
|
||||||
fun Int.toAnimDrawable(context: Context) = toDrawable(context) as AnimatedVectorDrawable
|
fun Int.toAnimDrawable(context: Context) = toDrawable(context) as AnimatedVectorDrawable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve this int into a color as if it was an attribute
|
||||||
|
*/
|
||||||
|
fun Int.resolveAttr(context: Context): Int {
|
||||||
|
// Convert the attribute into its color
|
||||||
|
val resolvedAttr = TypedValue()
|
||||||
|
context.theme.resolveAttribute(this, resolvedAttr, true)
|
||||||
|
|
||||||
|
// Then convert it to a proper color
|
||||||
|
val color = if (resolvedAttr.resourceId != 0) {
|
||||||
|
resolvedAttr.resourceId
|
||||||
|
} else {
|
||||||
|
resolvedAttr.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return color.toColor(context)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [Toast] from a [String]
|
* Create a [Toast] from a [String]
|
||||||
* @param context [Context] required to create the toast
|
* @param context [Context] required to create the toast
|
||||||
|
@ -137,22 +154,6 @@ 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 ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -253,8 +254,8 @@ private fun isSystemBarOnBottom(activity: Activity): Boolean {
|
||||||
// --- HACKY NIGHTMARES ---
|
// --- HACKY NIGHTMARES ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use reflection to fix a memory leak in the [Fragment] source code where the focused view will
|
* Use ***REFLECTION*** to fix a memory leak in the [Fragment] source code where the focused view
|
||||||
* never be cleared. I can't believe I have to do this.
|
* will never be cleared. I can't believe I have to do this.
|
||||||
*/
|
*/
|
||||||
fun Fragment.fixAnimInfoLeak() {
|
fun Fragment.fixAnimInfoLeak() {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:color="?attr/colorPrimary" android:state_pressed="true" />
|
|
||||||
<item android:color="?android:attr/textColorSecondary" />
|
|
||||||
</selector>
|
|
|
@ -31,7 +31,7 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/song_toolbar"
|
app:layout_constraintTop_toBottomOf="@+id/song_toolbar"
|
||||||
tools:listitem="@layout/item_song" />
|
tools:listitem="@layout/item_song" />
|
||||||
|
|
||||||
<com.reddit.indicatorfastscroll.FastScrollerView
|
<org.oxycblt.auxio.songs.FastScrollView
|
||||||
android:id="@+id/song_fast_scroll"
|
android:id="@+id/song_fast_scroll"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
@ -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.songs.CobaltScrollThumb
|
<org.oxycblt.auxio.songs.FastScrollThumb
|
||||||
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"
|
||||||
|
|
30
app/src/main/res/layout/view_scroll_thumb.xml
Normal file
30
app/src/main/res/layout/view_scroll_thumb.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:layout_height="match_parent"
|
||||||
|
tools:layout_width="@dimen/fast_scroller_thumb_size"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/thumb_layout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@drawable/thumb_circle"
|
||||||
|
android:elevation="@dimen/fast_scroller_thumb_elevation"
|
||||||
|
android:stateListAnimator="@animator/fast_scroll_thumb"
|
||||||
|
app:layout_constraintDimensionRatio="W,1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/thumb_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
tools:text="A" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
</merge>
|
||||||
|
</layout>
|
|
@ -16,7 +16,6 @@
|
||||||
<item name="popupMenuStyle">@style/Widget.CustomPopup</item>
|
<item name="popupMenuStyle">@style/Widget.CustomPopup</item>
|
||||||
<item name="colorControlNormal">@color/control_color</item>
|
<item name="colorControlNormal">@color/control_color</item>
|
||||||
<item name="alertDialogTheme">@style/Theme.CustomDialog</item>
|
<item name="alertDialogTheme">@style/Theme.CustomDialog</item>
|
||||||
<item name="indicatorFastScrollerStyle">@style/FastScrollTheme</item>
|
|
||||||
<item name="colorControlHighlight">@color/selection_color</item>
|
<item name="colorControlHighlight">@color/selection_color</item>
|
||||||
<item name="colorControlActivated">?attr/colorPrimary</item>
|
<item name="colorControlActivated">?attr/colorPrimary</item>
|
||||||
<item name="cornerRadius">0dp</item>
|
<item name="cornerRadius">0dp</item>
|
||||||
|
@ -99,15 +98,10 @@
|
||||||
<item name="android:fontFamily">@font/inter_exbold</item>
|
<item name="android:fontFamily">@font/inter_exbold</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Fast scroll theme -->
|
|
||||||
<style name="FastScrollTheme" parent="Widget.IndicatorFastScroll.FastScroller">
|
|
||||||
<item name="android:textAppearance">@style/TextAppearance.FastScroll</item>
|
|
||||||
<item name="android:textColor">@color/color_scroll_tints</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Fast scroll text appearance -->
|
<!-- Fast scroll text appearance -->
|
||||||
<style name="TextAppearance.FastScroll" parent="TextAppearance.AppCompat.Body2">
|
<style name="TextAppearance.FastScroll" parent="TextAppearance.AppCompat.Body2">
|
||||||
<item name="android:fontFamily">@font/inter_semibold</item>
|
<item name="android:fontFamily">@font/inter_semibold</item>
|
||||||
|
<item name="android:lineSpacingExtra">@dimen/padding_tiny</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Fast scroll thumb appearance -->
|
<!-- Fast scroll thumb appearance -->
|
||||||
|
|
17
build.gradle
17
build.gradle
|
@ -5,8 +5,15 @@ buildscript {
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter() // TODO: Remove JCenter when Exoplayer migrates
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
||||||
|
// TODO: Eliminate Exoplayer when it migrates to GMaven
|
||||||
|
jcenter {
|
||||||
|
content {
|
||||||
|
includeGroup("org.jetbrains.trove4j")
|
||||||
|
includeGroup("com.google.android.exoplayer")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -22,8 +29,14 @@ buildscript {
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
||||||
|
jcenter {
|
||||||
|
content {
|
||||||
|
includeGroup("org.jetbrains.trove4j")
|
||||||
|
includeGroup("com.google.android.exoplayer")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue