list: re-add fast scroll thumb
This commit is contained in:
parent
4c58590cb0
commit
171c0c795e
5 changed files with 208 additions and 18 deletions
|
@ -24,6 +24,6 @@ import org.oxycblt.musikr.tag.Name
|
|||
fun Name.thumb() =
|
||||
when (this) {
|
||||
is Name.Known ->
|
||||
tokens.firstOrNull()?.let { if (it.value.isDigitsOnly()) "#" else it.value }
|
||||
tokens.firstOrNull()?.let { if (it.value.isDigitsOnly()) "#" else it.value.first().uppercase() }
|
||||
is Name.Unknown -> "?"
|
||||
}
|
||||
|
|
|
@ -22,22 +22,36 @@ import android.animation.Animator
|
|||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.MaterialFadingSlider
|
||||
import org.oxycblt.auxio.ui.MaterialSlider
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getDimenPixels
|
||||
import org.oxycblt.auxio.util.getDrawableCompat
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.isRtl
|
||||
import org.oxycblt.auxio.util.isUnder
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
|
||||
|
@ -74,11 +88,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
// Thumb
|
||||
private val thumbWidth = context.getDimenPixels(R.dimen.spacing_mid_medium)
|
||||
private val thumbHeight = context.getDimenPixels(R.dimen.size_touchable_medium)
|
||||
private val slider = MaterialSlider(context, thumbWidth)
|
||||
private val thumbSlider = MaterialSlider.small(context, thumbWidth)
|
||||
private var thumbAnimator: Animator? = null
|
||||
|
||||
private val thumbView =
|
||||
context.inflater.inflate(R.layout.view_scroll_thumb, null).apply { slider.jumpOut(this) }
|
||||
context.inflater.inflate(R.layout.view_scroll_thumb, null)
|
||||
.apply { thumbSlider.jumpOut(this) }
|
||||
private val thumbPadding = Rect(0, 0, 0, 0)
|
||||
private var thumbOffset = 0
|
||||
|
||||
|
@ -89,6 +104,36 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
}
|
||||
}
|
||||
|
||||
private val popupView = MaterialTextView(context).apply {
|
||||
minimumWidth = context.getDimenPixels(R.dimen.size_touchable_large)
|
||||
minimumHeight = context.getDimenPixels(R.dimen.size_touchable_small)
|
||||
|
||||
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineMedium)
|
||||
setTextColor(context.getAttrColorCompat(com.google.android.material.R.attr.colorOnSecondary))
|
||||
ellipsize = TextUtils.TruncateAt.MIDDLE
|
||||
gravity = Gravity.CENTER
|
||||
includeFontPadding = false
|
||||
|
||||
elevation =
|
||||
context.getDimenPixels(com.google.android.material.R.dimen.m3_sys_elevation_level1)
|
||||
.toFloat()
|
||||
background = context.getDrawableCompat(R.drawable.ui_popup)
|
||||
updatePaddingRelative(end = context.getDimenPixels(R.dimen.spacing_tiny) / 2)
|
||||
layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
.apply {
|
||||
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
||||
}
|
||||
}
|
||||
private val popupSlider =
|
||||
MaterialFadingSlider(MaterialSlider.large(context, popupView.minimumWidth / 2)).apply {
|
||||
jumpOut(popupView)
|
||||
}
|
||||
private var popupAnimator: Animator? = null
|
||||
private var showingPopup = false
|
||||
|
||||
// Touch
|
||||
private val minTouchTargetSize = context.getDimenPixels(R.dimen.size_touchable_small)
|
||||
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
|
@ -109,6 +154,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
if (!value) {
|
||||
removeCallbacks(hideThumbRunnable)
|
||||
hideScrollbar()
|
||||
hidePopup()
|
||||
}
|
||||
|
||||
listener?.onFastScrollingChanged(field)
|
||||
|
@ -131,7 +177,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
if (field) {
|
||||
removeCallbacks(hideThumbRunnable)
|
||||
showScrollbar()
|
||||
showPopup()
|
||||
} else {
|
||||
hidePopup()
|
||||
postAutoHideScrollbar()
|
||||
}
|
||||
|
||||
|
@ -143,6 +191,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
init {
|
||||
overlay.add(thumbView)
|
||||
overlay.add(popupView)
|
||||
|
||||
addItemDecoration(
|
||||
object : ItemDecoration() {
|
||||
|
@ -176,7 +225,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
thumbView.layoutDirection = layoutDirection
|
||||
thumbView.measure(
|
||||
MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY))
|
||||
MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY)
|
||||
)
|
||||
val thumbTop = thumbPadding.top + thumbOffset
|
||||
val thumbLeft =
|
||||
if (isRtl) {
|
||||
|
@ -185,6 +235,77 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
width - thumbPadding.right - thumbWidth
|
||||
}
|
||||
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
|
||||
|
||||
popupView.layoutDirection = layoutDirection
|
||||
val child = getChildAt(0)
|
||||
val firstAdapterPos =
|
||||
if (child != null) {
|
||||
layoutManager?.getPosition(child) ?: NO_POSITION
|
||||
} else {
|
||||
NO_POSITION
|
||||
}
|
||||
|
||||
val popupText: String
|
||||
val provider = popupProvider
|
||||
if (firstAdapterPos != NO_POSITION && provider != null) {
|
||||
popupView.isInvisible = false
|
||||
// Get the popup text. If there is none, we default to "?".
|
||||
popupText = provider.getPopup(firstAdapterPos) ?: "?"
|
||||
} else {
|
||||
// No valid position or provider, do not show the popup.
|
||||
popupView.isInvisible = false
|
||||
popupText = ""
|
||||
}
|
||||
val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams
|
||||
|
||||
if (popupView.text != popupText) {
|
||||
popupView.text = popupText
|
||||
|
||||
val widthMeasureSpec =
|
||||
ViewGroup.getChildMeasureSpec(
|
||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
thumbPadding.left +
|
||||
thumbPadding.right +
|
||||
thumbWidth +
|
||||
popupLayoutParams.leftMargin +
|
||||
popupLayoutParams.rightMargin,
|
||||
popupLayoutParams.width
|
||||
)
|
||||
|
||||
val heightMeasureSpec =
|
||||
ViewGroup.getChildMeasureSpec(
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
||||
thumbPadding.top +
|
||||
thumbPadding.bottom +
|
||||
popupLayoutParams.topMargin +
|
||||
popupLayoutParams.bottomMargin,
|
||||
popupLayoutParams.height
|
||||
)
|
||||
|
||||
popupView.measure(widthMeasureSpec, heightMeasureSpec)
|
||||
Timber.d("Updating popup text to ${popupView.measuredHeight} ${popupView.measuredWidth}")
|
||||
}
|
||||
|
||||
val popupWidth = popupView.measuredWidth
|
||||
val popupHeight = popupView.measuredHeight
|
||||
val popupLeft =
|
||||
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||
thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin + popupWidth / 2
|
||||
} else {
|
||||
width - thumbPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth - popupWidth / 2
|
||||
}
|
||||
|
||||
val popupAnchorY = popupHeight / 2
|
||||
val thumbAnchorY = thumbView.height / 2
|
||||
|
||||
val popupTop =
|
||||
(thumbTop + thumbAnchorY - popupAnchorY)
|
||||
.coerceAtLeast(thumbPadding.top + popupLayoutParams.topMargin)
|
||||
.coerceAtMost(
|
||||
height - thumbPadding.bottom - popupLayoutParams.bottomMargin - popupHeight
|
||||
)
|
||||
|
||||
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
|
||||
}
|
||||
|
||||
override fun onScrolled(dx: Int, dy: Int) {
|
||||
|
@ -249,10 +370,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
dragging = true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (!dragging &&
|
||||
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
|
||||
abs(eventY - downY) > touchSlop) {
|
||||
abs(eventY - downY) > touchSlop
|
||||
) {
|
||||
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
|
||||
dragStartY = lastY
|
||||
dragStartThumbOffset = thumbOffset
|
||||
|
@ -271,6 +394,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
scrollToThumbOffset(thumbOffset)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_CANCEL -> dragging = false
|
||||
}
|
||||
|
@ -312,7 +436,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
showingThumb = true
|
||||
thumbAnimator?.cancel()
|
||||
thumbAnimator = slider.slideIn(thumbView).also { it.start() }
|
||||
thumbAnimator = thumbSlider.slideIn(thumbView).also { it.start() }
|
||||
}
|
||||
|
||||
private fun hideScrollbar() {
|
||||
|
@ -322,7 +446,30 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
|
||||
showingThumb = false
|
||||
thumbAnimator?.cancel()
|
||||
thumbAnimator = slider.slideOut(thumbView).also { it.start() }
|
||||
thumbAnimator = thumbSlider.slideOut(thumbView).also { it.start() }
|
||||
}
|
||||
|
||||
private fun showPopup() {
|
||||
if (!thumbEnabled) {
|
||||
return
|
||||
}
|
||||
if (showingPopup) {
|
||||
return
|
||||
}
|
||||
|
||||
showingPopup = true
|
||||
popupAnimator?.cancel()
|
||||
popupAnimator = popupSlider.slideIn(popupView).also { it.start() }
|
||||
}
|
||||
|
||||
private fun hidePopup() {
|
||||
if (!showingPopup) {
|
||||
return
|
||||
}
|
||||
|
||||
showingPopup = false
|
||||
popupAnimator?.cancel()
|
||||
popupAnimator = popupSlider.slideOut(popupView).also { it.start() }
|
||||
}
|
||||
|
||||
// --- LAYOUT STATE ---
|
||||
|
|
|
@ -201,19 +201,24 @@ class MaterialFlipper(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
class MaterialSlider(context: Context, private val x: Int) {
|
||||
private val outConfig =
|
||||
AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, AnimConfig.SHORT3)
|
||||
private val inConfig =
|
||||
AnimConfig.of(context, AnimConfig.EMPHASIZED_DECELERATE, AnimConfig.MEDIUM1)
|
||||
class MaterialSlider private constructor(context: Context, private val x: Int?, inDuration: Pair<Int, Int>, outDuration: Pair<Int, Int>) {
|
||||
private val outConfig = AnimConfig.of(context, AnimConfig.EMPHASIZED_ACCELERATE, outDuration)
|
||||
private val inConfig = AnimConfig.of(context, AnimConfig.EMPHASIZED_DECELERATE, inDuration)
|
||||
|
||||
fun jumpOut(view: View) {
|
||||
view.translationX = x.toFloat()
|
||||
if (x == null) {
|
||||
view.translationX = 100000f
|
||||
}
|
||||
view.translationX = (x ?: view.width).toFloat()
|
||||
}
|
||||
|
||||
fun slideOut(view: View): Animator {
|
||||
val target = (x ?: view.width).toFloat()
|
||||
if (view.translationX > target) {
|
||||
view.translationX = target
|
||||
}
|
||||
val animator =
|
||||
outConfig.genericFloat(view.translationX, x.toFloat()) { view.translationX = it }
|
||||
outConfig.genericFloat(view.translationX, target) { view.translationX = it }
|
||||
return animator
|
||||
}
|
||||
|
||||
|
@ -221,4 +226,36 @@ class MaterialSlider(context: Context, private val x: Int) {
|
|||
val animator = inConfig.genericFloat(view.translationX, 0f) { view.translationX = it }
|
||||
return animator
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun small(context: Context, x: Int?) = MaterialSlider(context, x, AnimConfig.SHORT3, AnimConfig.MEDIUM1)
|
||||
|
||||
fun large(context: Context, x: Int?) = MaterialSlider(context, x, AnimConfig.MEDIUM3, AnimConfig.SHORT3)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class MaterialFadingSlider(private val slider: MaterialSlider) {
|
||||
fun jumpOut(view: View) {
|
||||
slider.jumpOut(view)
|
||||
view.alpha = 0f
|
||||
}
|
||||
|
||||
fun slideOut(view: View): Animator {
|
||||
val slideOut = slider.slideOut(view)
|
||||
val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
|
||||
duration = slideOut.duration
|
||||
addUpdateListener { view.alpha = it.animatedValue as Float }
|
||||
}
|
||||
return AnimatorSet().apply { playTogether(slideOut, alphaOut) }
|
||||
}
|
||||
|
||||
fun slideIn(view: View): Animator {
|
||||
val slideIn = slider.slideIn(view)
|
||||
val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = slideIn.duration
|
||||
addUpdateListener { view.alpha = it.animatedValue as Float }
|
||||
}
|
||||
return AnimatorSet().apply { playTogether(slideIn, alphaIn) }
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@
|
|||
android:shape="rectangle"
|
||||
android:tint="?attr/colorSecondary">
|
||||
|
||||
<corners android:radius="16dp" />
|
||||
<corners android:radius="48dp" />
|
||||
<size
|
||||
android:width="56dp"
|
||||
android:height="56dp" />
|
||||
android:width="48dp"
|
||||
android:height="48dp" />
|
||||
<solid android:color="@android:color/white" />
|
||||
</shape>
|
|
@ -29,6 +29,12 @@
|
|||
<dimen name="height_scroll_thumb">48dp</dimen>
|
||||
|
||||
<!-- Misc -->
|
||||
<dimen name="fast_scroll_popup_min_width">72dp</dimen>
|
||||
<dimen name="fast_scroll_popup_min_height">52dp</dimen>
|
||||
<dimen name="fast_scroll_popup_padding_start">@dimen/spacing_medium</dimen>
|
||||
<dimen name="fast_scroll_popup_padding_end">30dp</dimen>
|
||||
<dimen name="fast_scroll_thumb_touch_target_size">48dp</dimen>
|
||||
|
||||
<dimen name="m3_shape_corners_large">16dp</dimen>
|
||||
<dimen name="m3_shape_corners_full">128dp</dimen>
|
||||
|
||||
|
|
Loading…
Reference in a new issue