home: merge fast scroller

Merge the fast scroller back into Auxio once again, albeit this time
it's AFS instead of IndicatorFastScroll. This not only slims down the
implementation but also allows me to stop the scroller from appearing
every time the home view is recreated. All modifications were noted
and attribution is included to abide by the Apache 2.0 license.
This commit is contained in:
OxygenCobalt 2021-10-23 18:22:50 -06:00
parent cb6d02fecc
commit 97808ce1c3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
14 changed files with 847 additions and 60 deletions

View file

@ -102,10 +102,6 @@ dependencies {
// Material
implementation "com.google.android.material:material:1.5.0-alpha04"
// Fast scrolling
// TODO: Merge eventually
implementation 'me.zhanghai.android.fastscroll:library:1.1.7'
// --- DEBUG ---
// Lint

View file

@ -40,8 +40,6 @@ import org.oxycblt.auxio.music.toURI
import org.oxycblt.auxio.settings.SettingsManager
import java.io.ByteArrayInputStream
// LEFT-OFF: Make MosaicFetcher use these covers
/**
* Fetcher that returns the album art for a given [Album]. Handles settings on whether to use
* quality covers or not.
@ -138,11 +136,13 @@ class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
ext.embeddedPicture?.let { coverBytes ->
val stream = ByteArrayInputStream(coverBytes)
return SourceResult(
source = stream.source().buffer(),
mimeType = context.contentResolver.getType(songUri),
dataSource = DataSource.DISK
)
stream.use { stm ->
return SourceResult(
source = stm.source().buffer(),
mimeType = context.contentResolver.getType(songUri),
dataSource = DataSource.DISK
)
}
}
}
@ -196,15 +196,12 @@ class AlbumArtFetcher(private val context: Context) : Fetcher<Album> {
}
}
return if (stream != null) {
return stream?.use { stm ->
return SourceResult(
source = stream.source().buffer(),
source = stm.source().buffer(),
mimeType = context.contentResolver.getType(uri),
dataSource = DataSource.DISK
)
} else {
// No dice.
null
}
}

View file

@ -0,0 +1,633 @@
/*
* Copyright (c) 2021 Auxio Project
* FastScrollRecyclerView.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home.fastscroll
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.text.TextUtils
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.math.MathUtils
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.resolveAttr
import org.oxycblt.auxio.util.resolveDrawable
import kotlin.math.abs
/**
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
* Zhanghi's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
*
* Attributions as per the Apache 2.0 license:
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai]
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
* MODIFIER: OxygenCobalt [https://github.com/]
*
* !!! MODIFICATIONS !!!:
* - Scroller will no longer show itself on startup, which looked unpleasent with multiple
* views
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
* - FastScroller overlay was merged into RecyclerView instance
* - Removed FastScrollerBuilder
* - Converted all code to kotlin
* - Use modified Auxio resources instead of AFS resources
* - Track view is now only used for touch bounds
* - Redundant functions have been merged
* - Variable names are no longer prefixed with m
* - TODO: Added documentation
* - TODO: Popup will center itself to the thumb when possible
*
* TODO: Debug this
*/
class FastScrollRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = -1
) : RecyclerView(context, attrs, defStyleAttr) {
private var popupProvider: ((Int) -> String)? = null
private val minTouchTargetSize: Int = resources.getDimensionPixelSize(R.dimen.size_btn_small)
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
private val trackView: View
private val thumbView: View
private val popupView: TextView
private val thumbWidth: Int
private val thumbHeight: Int
private var thumbOffset = 0
private var downX = 0f
private var downY = 0f
private var lastY = 0f
private var dragStartY = 0f
private var dragStartThumbOffset = 0
private var dragging = false
private var didRelayout = true
private var showingScrollbar = false
private var showingPopup = false
private val childRect = Rect()
private val hideScrollbarRunnable = Runnable {
if (dragging) {
return@Runnable
}
hideScrollbar()
}
init {
val thumbDrawable = R.drawable.ui_scroll_thumb.resolveDrawable(context)
trackView = View(context)
thumbView = View(context).apply {
alpha = 0f
background = thumbDrawable
}
popupView = AppCompatTextView(context).apply {
alpha = 0f
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
minimumWidth = resources.getDimensionPixelSize(
R.dimen.popup_min_width
)
minimumHeight = resources.getDimensionPixelSize(
R.dimen.size_btn_large
)
val layoutParams = layoutParams as FrameLayout.LayoutParams
layoutParams.gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
layoutParams.marginEnd = resources.getDimensionPixelOffset(
R.dimen.spacing_small
)
setLayoutParams(layoutParams)
background = Md2PopupBackground(context)
elevation = resources.getDimensionPixelOffset(R.dimen.elevation_normal).toFloat()
ellipsize = TextUtils.TruncateAt.MIDDLE
gravity = Gravity.CENTER
includeFontPadding = false
isSingleLine = true
setTextColor(android.R.attr.textColorPrimaryInverse.resolveAttr(context))
setTextSize(
TypedValue.COMPLEX_UNIT_PX,
resources.getDimensionPixelSize(
R.dimen.text_size_insane
).toFloat()
)
}
thumbWidth = thumbDrawable.intrinsicWidth
thumbHeight = thumbDrawable.intrinsicHeight
check(thumbWidth >= 0)
check(thumbHeight >= 0)
overlay.add(trackView)
overlay.add(thumbView)
overlay.add(popupView)
addItemDecoration(object : ItemDecoration() {
override fun onDraw(
canvas: Canvas,
parent: RecyclerView,
state: State
) {
onPreDraw()
}
})
// We use a listener instead of overriding onTouchEvent so that we don't conflict with
// RecyclerView touch events.
addOnItemTouchListener(object : SimpleOnItemTouchListener() {
override fun onInterceptTouchEvent(
recyclerView: RecyclerView,
event: MotionEvent
): Boolean {
return onItemTouch(event)
}
override fun onTouchEvent(
recyclerView: RecyclerView,
event: MotionEvent
) {
onItemTouch(event)
}
})
}
fun setup(provider: (Int) -> String) {
popupProvider = provider
}
// --- RECYCLERVIEW EVENT MANAGEMENT ---
private fun onPreDraw() {
updateScrollbarState()
trackView.layoutDirection = layoutDirection
thumbView.layoutDirection = layoutDirection
popupView.layoutDirection = layoutDirection
val trackLeft = if (isRtl) {
paddingLeft
} else {
width - paddingRight - thumbWidth
}
trackView.layout(trackLeft, paddingTop, trackLeft + thumbWidth, height - paddingBottom)
val thumbLeft = if (isRtl) {
paddingLeft
} else {
width - paddingRight - thumbWidth
}
val thumbTop = paddingTop + thumbOffset
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
val firstPos = firstAdapterPos
val popupText = if (firstPos != NO_POSITION) {
popupProvider?.invoke(firstPos) ?: ""
} else {
""
}
val hasPopup = !TextUtils.isEmpty(popupText)
popupView.visibility = if (hasPopup) View.VISIBLE else View.INVISIBLE
if (hasPopup) {
val popupLayoutParams = popupView.layoutParams as FrameLayout.LayoutParams
if (popupView.text != popupText) {
popupView.text = popupText
val widthMeasureSpec = ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
paddingLeft + paddingRight + thumbWidth + popupLayoutParams.leftMargin +
popupLayoutParams.rightMargin,
popupLayoutParams.width
)
val heightMeasureSpec = ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
paddingTop + paddingBottom + popupLayoutParams.topMargin +
popupLayoutParams.bottomMargin,
popupLayoutParams.height
)
popupView.measure(widthMeasureSpec, heightMeasureSpec)
}
val popupWidth = popupView.measuredWidth
val popupHeight = popupView.measuredHeight
val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
paddingLeft + thumbWidth + popupLayoutParams.leftMargin
} else {
width - paddingRight - thumbWidth - popupLayoutParams.rightMargin - popupWidth
}
// We handle RTL separately, so it's okay if Gravity.RIGHT is used here
@SuppressLint("RtlHardcoded")
val popupAnchorY = when (popupLayoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
Gravity.CENTER_HORIZONTAL -> popupHeight / 2
Gravity.RIGHT -> popupHeight
else -> 0
}
val thumbAnchorY = when (popupLayoutParams.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
Gravity.CENTER_VERTICAL -> {
thumbView.paddingTop + (
thumbHeight - thumbView.paddingTop - thumbView.paddingBottom
) / 2
}
Gravity.BOTTOM -> thumbHeight - thumbView.paddingBottom
else -> thumbView.paddingTop
}
val popupTop = MathUtils.clamp(
thumbTop + thumbAnchorY - popupAnchorY,
paddingTop + popupLayoutParams.topMargin,
height - paddingBottom - popupLayoutParams.bottomMargin - popupHeight
)
popupView.layout(
popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight
)
}
}
override fun onScrolled(dx: Int, dy: Int) {
super.onScrolled(dx, dy)
updateScrollbarState()
if (didRelayout) {
didRelayout = false
return
}
showScrollbar()
postAutoHideScrollbar()
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
didRelayout = changed
}
private fun updateScrollbarState() {
// Getting a pixel-perfect scroll position from a recyclerview is a bit of an involved
// process. It's kind of expected given how RecyclerView well...recycles views, but it's
// still very annoying how many hoops one has to jump through.
// First, we need to get the first visible child. We will use this to extrapolate a rough
// scroll range/position for the view.
// Doing this does mean that the fast scroller will break if you have a header view that's
// a different height, but Auxio's home UI doesn't have something like that so we're okay.
val firstChild = getChildAt(0)
val itemPos = firstAdapterPos
val itemCount = itemCount
// Now get the bounds of the first child. These are the dimensions we use to extrapolate
// information for the whole recyclerview.
getDecoratedBoundsWithMargins(firstChild, childRect)
val itemHeight = childRect.height()
val itemTop = childRect.top
// This is where things get messy. We have to take everything we just calculated and
// do some arithmetic to get it into a working thumb position.
// The total scroll range based on the initial item
val scrollRange = paddingTop + (itemCount * itemHeight) + paddingBottom
// The scroll range where the items aren't visible
val scrollOffsetRange = scrollRange - height
// The scroll offset, or basically the y of the current item + the height of all
// the previous items
val scrollOffset = paddingTop + (itemPos * itemHeight) - itemTop
// The range of pixels where the thumb is not present
val thumbOffsetRange = height - paddingTop - paddingBottom - thumbHeight
// Finally, we can calculate the thumb position, which is just:
// [proportion of scroll position to scroll range] * [total thumb range]
thumbOffset = (thumbOffsetRange.toLong() * scrollOffset / scrollOffsetRange).toInt()
}
private fun onItemTouch(event: MotionEvent): Boolean {
val eventX = event.x
val eventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX = eventX
downY = eventY
val scrollX = trackView.scrollX
val isInScrollbar = (
eventX >= thumbView.left - scrollX && eventX < thumbView.right - scrollX
)
if (trackView.alpha > 0 && isInScrollbar) {
dragStartY = eventY
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
dragStartThumbOffset = thumbOffset
} else {
dragStartThumbOffset = (eventY - paddingTop - thumbHeight / 2f).toInt()
scrollToThumbOffset(dragStartThumbOffset)
}
setDragging(true)
}
}
MotionEvent.ACTION_MOVE -> {
if (!dragging && isInViewTouchTarget(trackView, downX, downY) &&
abs(eventY - downY) > touchSlop
) {
if (isInViewTouchTarget(thumbView, downX, downY)) {
dragStartY = lastY
dragStartThumbOffset = thumbOffset
} else {
dragStartY = eventY
dragStartThumbOffset = (eventY - paddingTop - thumbHeight / 2f).toInt()
scrollToThumbOffset(dragStartThumbOffset)
}
setDragging(true)
}
if (dragging) {
val thumbOffset = dragStartThumbOffset + (eventY - dragStartY).toInt()
scrollToThumbOffset(thumbOffset)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false)
}
lastY = eventY
return dragging
}
private fun isInViewTouchTarget(view: View, x: Float, y: Float): Boolean {
val scrollX = scrollX
val scrollY = scrollY
return (
isInTouchTarget(
x, view.left - scrollX, view.right - scrollX, 0,
width
) &&
isInTouchTarget(
y, view.top - scrollY, view.bottom - scrollY, 0,
height
)
)
}
private fun isInTouchTarget(
position: Float,
viewStart: Int,
viewEnd: Int,
parentStart: Int,
parentEnd: Int
): Boolean {
val viewSize = viewEnd - viewStart
if (viewSize >= minTouchTargetSize) {
return position >= viewStart && position < viewEnd
}
var touchTargetStart = viewStart - (minTouchTargetSize - viewSize) / 2
if (touchTargetStart < parentStart) {
touchTargetStart = parentStart
}
var touchTargetEnd = touchTargetStart + minTouchTargetSize
if (touchTargetEnd > parentEnd) {
touchTargetEnd = parentEnd
touchTargetStart = touchTargetEnd - minTouchTargetSize
if (touchTargetStart < parentStart) {
touchTargetStart = parentStart
}
}
return position >= touchTargetStart && position < touchTargetEnd
}
private fun scrollToThumbOffset(thumbOffset: Int) {
var newThumbOffset = thumbOffset
newThumbOffset = MathUtils.clamp(newThumbOffset, 0, thumbOffsetRange)
var scrollOffset = (scrollOffsetRange.toLong() * newThumbOffset / thumbOffsetRange).toInt()
scrollOffset -= paddingTop
scrollTo(scrollOffset)
}
private fun scrollTo(offset: Int) {
stopScroll()
val trueOffset = offset - paddingTop
val itemHeight = itemHeight
val firstItemPosition = 0.coerceAtLeast(trueOffset / itemHeight)
val firstItemTop = firstItemPosition * itemHeight - trueOffset
scrollToPositionWithOffset(firstItemPosition, firstItemTop)
}
private fun scrollToPositionWithOffset(position: Int, offset: Int) {
var targetPosition = position
val trueOffset = offset - paddingTop
when (val mgr = layoutManager) {
is GridLayoutManager -> {
targetPosition *= mgr.spanCount
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
}
is LinearLayoutManager -> {
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
}
}
}
private fun setDragging(dragging: Boolean) {
if (this.dragging == dragging) {
return
}
this.dragging = dragging
if (this.dragging) {
parent.requestDisallowInterceptTouchEvent(true)
}
trackView.isPressed = this.dragging
thumbView.isPressed = this.dragging
if (this.dragging) {
removeCallbacks(hideScrollbarRunnable)
showScrollbar()
showPopup()
} else {
postAutoHideScrollbar()
hidePopup()
}
}
// --- SCROLLBAR APPEARANCE ---
private fun postAutoHideScrollbar() {
removeCallbacks(hideScrollbarRunnable)
postDelayed(hideScrollbarRunnable, AUTO_HIDE_SCROLLBAR_DELAY_MILLIS.toLong())
}
private fun showScrollbar() {
if (showingScrollbar) {
return
}
showingScrollbar = true
animateView(thumbView, 1f)
}
private fun hideScrollbar() {
if (!showingScrollbar) {
return
}
showingScrollbar = false
animateView(thumbView, 0f)
}
private fun showPopup() {
if (showingPopup) {
return
}
showingPopup = true
animateView(popupView, 1f)
}
private fun hidePopup() {
if (!showingPopup) {
return
}
showingPopup = false
animateView(popupView, 0f)
}
private fun animateView(view: View, alpha: Float) {
view.animate()
.alpha(alpha)
.setDuration(ANIM_MILLIS)
.start()
}
// --- LAYOUT STATE ---
private val isRtl: Boolean
get() = layoutDirection == LAYOUT_DIRECTION_RTL
private val scrollOffsetRange: Int
get() = scrollRange - height
private val thumbOffsetRange: Int
get() {
return height - paddingTop - paddingBottom - thumbHeight
}
private val itemCount: Int
get() = when (val mgr = layoutManager) {
is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1
is LinearLayoutManager -> mgr.itemCount
else -> 0
}
private val itemHeight: Int
get() {
if (childCount == 0) {
return 0
}
val itemView = getChildAt(0)
getDecoratedBoundsWithMargins(itemView, childRect)
return childRect.height()
}
private val scrollRange: Int
get() {
val itemCount = itemCount
if (itemCount == 0) {
return 0
}
val itemHeight = itemHeight
return if (itemHeight != 0) {
paddingTop + itemCount * itemHeight + paddingBottom
} else {
0
}
}
private val firstAdapterPos: Int
get() {
if (childCount == 0) {
return NO_POSITION
}
val child = getChildAt(0)
return when (val mgr = layoutManager) {
is GridLayoutManager -> mgr.getPosition(child) / mgr.spanCount
is LinearLayoutManager -> mgr.getPosition(child)
else -> 0
}
}
companion object {
private const val ANIM_MILLIS = 150L
private const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2021 Auxio Project
* Md2PopupBackground.java is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.home.fastscroll
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Matrix
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Build
import android.view.View
import androidx.core.graphics.drawable.DrawableCompat
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.resolveAttr
import kotlin.math.sqrt
/**
* The custom drawable used as FastScrollRecyclerView's popup background.
* This is an adaptation from AndroidFastScroll's MD2 theme.
*
* Attributions as per the Apache 2.0 license:
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai]
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
* MODIFIER: OxygenCobalt [https://github.com/]
*
* !!! MODIFICATIONS !!!:
* - Use modified Auxio resources instead of AFS resources
* - Variable names are no longer prefixed with m
* - Suppressed deprecation warning when dealing with convexness
* - TODO: Popup will center itself to the thumb when possible
*/
class Md2PopupBackground(context: Context) : Drawable() {
private val paint: Paint = Paint()
private val paddingStart: Int
private val paddingEnd: Int
private val path = Path()
private val tempMatrix = Matrix()
override fun draw(canvas: Canvas) {
canvas.drawPath(path, paint)
}
override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
updatePath()
return true
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(colorFilter: ColorFilter?) {}
override fun isAutoMirrored(): Boolean {
return true
}
private fun needMirroring(): Boolean {
return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun onBoundsChange(bounds: Rect) {
updatePath()
}
private fun updatePath() {
path.reset()
val bounds = bounds
var width = bounds.width().toFloat()
val height = bounds.height().toFloat()
val r = height / 2
val sqrt2 = sqrt(2.0).toFloat()
// Ensure we are convex.
width = (r + sqrt2 * r).coerceAtLeast(width)
pathArcTo(path, r, r, r, 90f, 180f)
val o1X = width - sqrt2 * r
pathArcTo(path, o1X, r, r, -90f, 45f)
val r2 = r / 5
val o2X = width - sqrt2 * r2
pathArcTo(path, o2X, r, r2, -45f, 90f)
pathArcTo(path, o1X, r, r, 45f, 45f)
path.close()
if (needMirroring()) {
tempMatrix.setScale(-1f, 1f, width / 2, 0f)
} else {
tempMatrix.reset()
}
tempMatrix.postTranslate(bounds.left.toFloat(), bounds.top.toFloat())
path.transform(tempMatrix)
}
override fun getPadding(padding: Rect): Boolean {
if (needMirroring()) {
padding[paddingEnd, 0, paddingStart] = 0
} else {
padding[paddingStart, 0, paddingEnd] = 0
}
return true
}
@Suppress("DEPRECATION")
override fun getOutline(outline: Outline) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && !path.isConvex) {
// The outline path must be convex before Q, but we may run into floating point error
// caused by calculation involving sqrt(2) or OEM implementation difference, so in this
// case we just omit the shadow instead of crashing.
super.getOutline(outline)
return
}
outline.setConvexPath(path)
}
companion object {
private fun pathArcTo(
path: Path,
centerX: Float,
centerY: Float,
radius: Float,
startAngle: Float,
sweepAngle: Float
) {
path.arcTo(
centerX - radius, centerY - radius, centerX + radius, centerY + radius,
startAngle, sweepAngle, false
)
}
}
init {
paint.isAntiAlias = true
paint.color = R.attr.colorControlActivated.resolveAttr(context)
paint.style = Paint.Style.FILL
val resources = context.resources
paddingStart = resources.getDimensionPixelOffset(R.dimen.spacing_medium)
paddingEnd = resources.getDimensionPixelOffset(R.dimen.popup_padding_end)
}
}

View file

@ -23,7 +23,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import me.zhanghai.android.fastscroll.PopupTextProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Album
@ -55,8 +54,8 @@ class AlbumListFragment : HomeListFragment() {
return binding.root
}
override val popupProvider: PopupTextProvider
get() = PopupTextProvider { idx ->
override val popupProvider: (Int) -> String
get() = { idx ->
val album = homeModel.albums.value!![idx]
when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {

View file

@ -23,7 +23,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import me.zhanghai.android.fastscroll.PopupTextProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Artist
@ -53,8 +52,8 @@ class ArtistListFragment : HomeListFragment() {
return binding.root
}
override val popupProvider: PopupTextProvider
get() = PopupTextProvider { idx ->
override val popupProvider: (Int) -> String
get() = { idx ->
homeModel.artists.value!![idx].name.sliceArticle().first().uppercase()
}

View file

@ -23,7 +23,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import me.zhanghai.android.fastscroll.PopupTextProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.HomeFragmentDirections
import org.oxycblt.auxio.music.Genre
@ -53,8 +52,8 @@ class GenreListFragment : HomeListFragment() {
return binding.root
}
override val popupProvider: PopupTextProvider
get() = PopupTextProvider { idx ->
override val popupProvider: (Int) -> String
get() = { idx ->
homeModel.genres.value!![idx].name.sliceArticle().first().uppercase()
}

View file

@ -26,9 +26,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import me.zhanghai.android.fastscroll.PopupTextProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.music.BaseModel
@ -36,7 +33,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.memberBinding
import org.oxycblt.auxio.util.applyEdgeRespectingBar
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.resolveDrawable
/**
* A Base [Fragment] implementing the base features shared across all detail fragments.
@ -50,18 +46,12 @@ abstract class HomeListFragment : Fragment() {
protected val homeModel: HomeViewModel by activityViewModels()
protected val playbackModel: PlaybackViewModel by activityViewModels()
abstract val popupProvider: PopupTextProvider
abstract val popupProvider: (Int) -> String
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.homeRecycler.apply {
FastScrollerBuilder(this)
.useMd2Style()
.setPopupTextProvider(popupProvider)
.setTrackDrawable(R.drawable.ui_scroll_track.resolveDrawable(context))
.build()
}
binding.homeRecycler.setup(popupProvider)
}
protected fun <T : BaseModel, VH : RecyclerView.ViewHolder> setupRecycler(

View file

@ -23,7 +23,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import me.zhanghai.android.fastscroll.PopupTextProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemPlayShuffleBinding
import org.oxycblt.auxio.music.Song
@ -56,25 +55,25 @@ class SongListFragment : HomeListFragment() {
return binding.root
}
override val popupProvider: PopupTextProvider
get() = PopupTextProvider { idx ->
if (idx == 0) {
return@PopupTextProvider ""
}
override val popupProvider: (Int) -> String
get() = { idx ->
if (idx != 0) {
val song = homeModel.songs.value!![idx]
val song = homeModel.songs.value!![idx]
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
.first().uppercase()
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
.first().uppercase()
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
.first().uppercase()
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
.first().uppercase()
SortMode.ALBUM -> song.album.name.sliceArticle()
.first().uppercase()
SortMode.ALBUM -> song.album.name.sliceArticle()
.first().uppercase()
SortMode.YEAR -> song.album.year.toString()
SortMode.YEAR -> song.album.year.toString()
}
} else {
""
}
}

View file

@ -101,11 +101,13 @@ class WidgetProvider : AppWidgetProvider() {
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.transformations(RoundedCornersTransformation(
context.resources.getDimensionPixelSize(
android.R.dimen.system_app_widget_inner_radius
).toFloat()
))
builder.transformations(
RoundedCornersTransformation(
context.resources.getDimensionPixelSize(
android.R.dimen.system_app_widget_inner_radius
).toFloat()
)
)
}
Coil.imageLoader(context).enqueue(builder.build())

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
android:tint="?attr/colorSecondary">
<corners android:radius="8dp" />
<padding android:top="4dp" android:bottom="4dp" android:right="2dp" android:left="2dp"/>
<size android:width="8dp" android:height="52dp" />
<solid android:color="@android:color/white" />
</shape>

View file

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView
<org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
android:id="@+id/home_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -11,7 +11,7 @@
<android.widget.ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.3"
android:alpha="0.275"
android:contentDescription="@string/desc_no_cover"
android:scaleType="centerCrop"
android:src="@drawable/ic_song" />

View file

@ -31,10 +31,14 @@
<dimen name="text_size_medium">18sp</dimen>
<dimen name="text_size_large">20sp</dimen>
<dimen name="text_size_huge">26sp</dimen>
<dimen name="text_size_insane">34sp</dimen>
<!-- Misc -->
<dimen name="elevation_small">2dp</dimen>
<dimen name="elevation_normal">4dp</dimen>
<dimen name="offset_thumb">4dp</dimen>
<dimen name="popup_min_width">78dp</dimen>
<dimen name="popup_padding_end">28dp</dimen>
</resources>