diff --git a/app/build.gradle b/app/build.gradle index 15583a9fd..35624981b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt b/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt index 737456373..984a3d27d 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt @@ -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 { 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 { } } - 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 } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt new file mode 100644 index 000000000..6ef983cf2 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -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 . + */ + +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 + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/Md2PopupBackground.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/Md2PopupBackground.kt new file mode 100644 index 000000000..4bded0af5 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/Md2PopupBackground.kt @@ -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 . + */ +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) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index a733b089b..4a3c7a76c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -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)) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index a477e1fd2..dccc3fd86 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index a57945642..d04d513cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index 503ea6769..d9860baed 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -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 setupRecycler( diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 7d169e371..4121f9cf9 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -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 { + "" } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 1a3d73f71..ee46dca8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -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()) diff --git a/app/src/main/res/drawable/ui_scroll_thumb.xml b/app/src/main/res/drawable/ui_scroll_thumb.xml new file mode 100644 index 000000000..d1e260d5e --- /dev/null +++ b/app/src/main/res/drawable/ui_scroll_thumb.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_list.xml b/app/src/main/res/layout/fragment_home_list.xml index 5f9f30419..d58e02755 100644 --- a/app/src/main/res/layout/fragment_home_list.xml +++ b/app/src/main/res/layout/fragment_home_list.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1cebb1202..961d50943 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -31,10 +31,14 @@ 18sp 20sp 26sp + 34sp 2dp 4dp 4dp + + 78dp + 28dp \ No newline at end of file