From fe6c07a34260140bab374b85ca07421ebd5c6ab8 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 7 Nov 2024 20:52:48 -0700 Subject: [PATCH] recycler: redesign fast scroller - Use new "bump" design - Base off fundamental RV primitives over custom item calculations - Make possible to use by non-home views --- .../home/fastscroll/FastScrollPopupView.kt | 185 ----------- .../auxio/home/list/AlbumListFragment.kt | 2 +- .../auxio/home/list/ArtistListFragment.kt | 2 +- .../auxio/home/list/GenreListFragment.kt | 2 +- .../auxio/home/list/PlaylistListFragment.kt | 2 +- .../auxio/home/list/SongListFragment.kt | 2 +- .../recycler}/FastScrollRecyclerView.kt | 288 +++--------------- .../java/org/oxycblt/auxio/ui/Animations.kt | 22 ++ app/src/main/res/drawable/ic_scroll_24.xml | 11 + app/src/main/res/drawable/ui_popup.xml | 11 + app/src/main/res/drawable/ui_scroll_thumb.xml | 11 +- .../main/res/layout/fragment_home_list.xml | 2 +- app/src/main/res/layout/view_scroll_thumb.xml | 15 + app/src/main/res/values/dimens.xml | 3 + 14 files changed, 115 insertions(+), 443 deletions(-) delete mode 100644 app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt rename app/src/main/java/org/oxycblt/auxio/{home/fastscroll => list/recycler}/FastScrollRecyclerView.kt (52%) create mode 100644 app/src/main/res/drawable/ic_scroll_24.xml create mode 100644 app/src/main/res/drawable/ui_popup.xml create mode 100644 app/src/main/res/layout/view_scroll_thumb.xml diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt deleted file mode 100644 index c96ceaeb6..000000000 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * FastScrollPopupView.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.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.text.TextUtils -import android.util.AttributeSet -import android.view.Gravity -import androidx.core.widget.TextViewCompat -import com.google.android.material.R as MR -import com.google.android.material.textview.MaterialTextView -import org.oxycblt.auxio.R -import org.oxycblt.auxio.util.getAttrColorCompat -import org.oxycblt.auxio.util.getDimenPixels -import org.oxycblt.auxio.util.isRtl - -/** - * A [MaterialTextView] that displays the popup indicator used in FastScrollRecyclerView - * - * @author Alexander Capehart (OxygenCobalt), Hai Zhang - */ -class FastScrollPopupView -@JvmOverloads -constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) : - MaterialTextView(context, attrs, defStyleRes) { - init { - minimumWidth = context.getDimenPixels(R.dimen.size_touchable_mid_huge) - minimumHeight = context.getDimenPixels(R.dimen.size_touchable_large) - - TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) - setTextColor(context.getAttrColorCompat(MR.attr.colorOnSecondary)) - ellipsize = TextUtils.TruncateAt.MIDDLE - gravity = Gravity.CENTER - includeFontPadding = false - - alpha = 0f - elevation = context.getDimenPixels(MR.dimen.m3_sys_elevation_level2).toFloat() - background = FastScrollPopupDrawable(context) - } - - private class FastScrollPopupDrawable(context: Context) : Drawable() { - private val paint: Paint = - Paint().apply { - isAntiAlias = true - color = - context - .getAttrColorCompat(com.google.android.material.R.attr.colorSecondary) - .defaultColor - style = Paint.Style.FILL - } - - private val path = Path() - private val matrix = Matrix() - - private val paddingStart = context.getDimenPixels(R.dimen.spacing_medium) - private val paddingEnd = context.getDimenPixels(R.dimen.spacing_mid_huge) - - override fun draw(canvas: Canvas) { - canvas.drawPath(path, paint) - } - - override fun onBoundsChange(bounds: Rect) { - updatePath() - } - - override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean { - updatePath() - return true - } - - @Suppress("DEPRECATION") - override fun getOutline(outline: Outline) { - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> outline.setPath(path) - - // Paths don't need to be convex on android Q, but the API was mislabeled and so - // we still have to use this method. - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path) - else -> - if (!path.isConvex) { - // The outline path must be convex before Q, but we may run into floating - // point errors caused by calculations involving sqrt(2) or OEM differences, - // so in this case we just omit the shadow instead of crashing. - super.getOutline(outline) - } - } - } - - override fun getPadding(padding: Rect): Boolean { - if (isRtl) { - padding[paddingEnd, 0, paddingStart] = 0 - } else { - padding[paddingStart, 0, paddingEnd] = 0 - } - - return true - } - - override fun isAutoMirrored(): Boolean = true - - override fun setAlpha(alpha: Int) {} - - override fun setColorFilter(colorFilter: ColorFilter?) {} - - override fun getOpacity(): Int = PixelFormat.TRANSLUCENT - - private fun updatePath() { - val r = bounds.height().toFloat() / 2 - val w = (r + SQRT2 * r).coerceAtLeast(bounds.width().toFloat()) - - path.apply { - reset() - - // Draw the left pill shape - val o1X = w - SQRT2 * r - arcToSafe(r, r, r, 90f, 180f) - arcToSafe(o1X, r, r, -90f, 45f) - - // Draw the right arrow shape - val point = r / 5 - val o2X = w - SQRT2 * point - arcToSafe(o2X, r, point, -45f, 90f) - arcToSafe(o1X, r, r, 45f, 45f) - - close() - } - - matrix.apply { - reset() - if (isRtl) setScale(-1f, 1f, w / 2, 0f) - postTranslate(bounds.left.toFloat(), bounds.top.toFloat()) - } - - path.transform(matrix) - } - - private fun Path.arcToSafe( - centerX: Float, - centerY: Float, - radius: Float, - startAngle: Float, - sweepAngle: Float - ) { - arcTo( - centerX - radius, - centerY - radius, - centerX + radius, - centerY + radius, - startAngle, - sweepAngle, - false) - } - } - - private companion object { - // Pre-calculate sqrt(2) - const val SQRT2 = 1.4142135f - } -} 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 a3ad98835..4d241eaf7 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 @@ -29,12 +29,12 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel -import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.AlbumViewHolder +import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Music 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 0f99cdb48..b2e805a75 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 @@ -27,12 +27,12 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel -import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.ArtistViewHolder +import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music 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 ef001d36e..4ea68bb5c 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 @@ -27,11 +27,11 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel -import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.GenreViewHolder import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Genre diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt index e0fe15bec..901693eb9 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/PlaylistListFragment.kt @@ -26,11 +26,11 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.home.HomeViewModel -import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.PlaylistViewHolder import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Music 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 6a5d91bcf..1d455b58c 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 @@ -28,11 +28,11 @@ import java.util.Formatter import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel -import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.recycler.FastScrollRecyclerView import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Music diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt similarity index 52% rename from app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt rename to app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt index 4a0e35873..89e0b5e74 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/FastScrollRecyclerView.kt @@ -16,31 +16,25 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.home.fastscroll +package org.oxycblt.auxio.list.recycler import android.animation.Animator import android.content.Context import android.graphics.Canvas import android.graphics.Rect 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 androidx.annotation.AttrRes -import androidx.core.view.isInvisible -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt import org.oxycblt.auxio.R -import org.oxycblt.auxio.list.recycler.AuxioRecyclerView -import org.oxycblt.auxio.ui.MaterialFader +import org.oxycblt.auxio.ui.MaterialSlider 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 @@ -67,6 +61,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * - Variable names are no longer prefixed with m * - Added drag listener * - Added documentation + * - Completely new design * * @author Hai Zhang, Alexander Capehart (OxygenCobalt) * @@ -78,14 +73,12 @@ class FastScrollRecyclerView constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : AuxioRecyclerView(context, attrs, defStyleAttr) { // Thumb - private val thumbView = - View(context).apply { - scaleX = 0f - background = context.getDrawableCompat(R.drawable.ui_scroll_thumb) - } + private val thumbSize = context.getDimenPixels(R.dimen.size_touchable_small) + private val slider = MaterialSlider(context, thumbSize) + private var thumbAnimator: Animator? = null - private val thumbWidth = thumbView.background.intrinsicWidth - private val thumbHeight = thumbView.background.intrinsicHeight + private val thumbView = + context.inflater.inflate(R.layout.view_scroll_thumb, null).apply { slider.jumpOut(this) } private val thumbPadding = Rect(0, 0, 0, 0) private var thumbOffset = 0 @@ -96,27 +89,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - // Popup - private val popupView = - FastScrollPopupView(context).apply { - scaleX = 0f - scaleY = 0f - alpha = 0f - layoutParams = - FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - .apply { - gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP - marginEnd = context.getDimenPixels(R.dimen.spacing_small) - } - } - - private val fader = MaterialFader.quickLopsided(context) - private var thumbAnimator: Animator? = null - 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 @@ -144,23 +116,18 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (field) { removeCallbacks(hideThumbRunnable) showScrollbar() - showPopup() } else { postAutoHideScrollbar() - hidePopup() } listener?.onFastScrollingChanged(field) } - private val tRect = Rect() - var popupProvider: PopupProvider? = null var listener: Listener? = null init { overlay.add(thumbView) - overlay.add(popupView) addItemDecoration( object : ItemDecoration() { @@ -192,85 +159,17 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr updateScrollbarState() thumbView.layoutDirection = layoutDirection - popupView.layoutDirection = layoutDirection - + thumbView.measure( + MeasureSpec.makeMeasureSpec(thumbSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(thumbSize, MeasureSpec.EXACTLY)) + val thumbTop = thumbPadding.top + thumbOffset val thumbLeft = if (isRtl) { thumbPadding.left } else { - width - thumbPadding.right - thumbWidth - } - - val thumbTop = thumbPadding.top + thumbOffset - - thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight) - - val child = getChildAt(0) - val firstAdapterPos = - if (child != null) { - layoutManager?.getPosition(child) ?: NO_POSITION - } else { - NO_POSITION + width - thumbPadding.right - thumbSize } - - 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 = true - 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) - } - - val popupWidth = popupView.measuredWidth - val popupHeight = popupView.measuredHeight - val popupLeft = - if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { - thumbPadding.left + thumbWidth + popupLayoutParams.leftMargin - } else { - width - thumbPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth - } - - val popupAnchorY = popupHeight / 2 - val thumbAnchorY = thumbView.paddingTop - - 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) + thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbSize, thumbTop + thumbSize) } override fun onScrolled(dx: Int, dy: Int) { @@ -295,26 +194,15 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private fun updateScrollbarState() { - if (scrollRange <= height || childCount == 0) { - return - } - - // Combine the previous item dimensions with the current item top to find our scroll - // position - getDecoratedBoundsWithMargins(getChildAt(0), tRect) - val child = getChildAt(0) - val firstAdapterPos = - when (val mgr = layoutManager) { - is GridLayoutManager -> mgr.getPosition(child) / mgr.spanCount - is LinearLayoutManager -> mgr.getPosition(child) - else -> 0 - } - - val scrollOffset = paddingTop + (firstAdapterPos * itemHeight) - tRect.top - // Then calculate the thumb position, which is just: // [proportion of scroll position to scroll range] * [total thumb range] - thumbOffset = (thumbOffsetRange.toLong() * scrollOffset / scrollOffsetRange).toInt() + val offsetY = computeVerticalScrollOffset() + if (computeVerticalScrollRange() < height || childCount == 0) { + return + } + val extentY = computeVerticalScrollExtent() + val fraction = (offsetY).toFloat() / (computeVerticalScrollRange() - extentY) + thumbOffset = (thumbOffsetRange * fraction).toInt() } private fun onItemTouch(event: MotionEvent): Boolean { @@ -331,10 +219,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (thumbView.isUnder(eventX, eventY, minTouchTargetSize)) { dragStartThumbOffset = thumbOffset - } else { - dragStartThumbOffset = - (eventY - thumbPadding.top - thumbHeight / 2f).toInt() + } else if (eventX > thumbView.right - thumbSize / 4) { + dragStartThumbOffset = (eventY - thumbPadding.top - thumbSize / 2f).toInt() scrollToThumbOffset(dragStartThumbOffset) + } else { + return false } dragging = true @@ -349,8 +238,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr dragStartThumbOffset = thumbOffset } else { dragStartY = eventY - dragStartThumbOffset = - (eventY - thumbPadding.top - thumbHeight / 2f).toInt() + dragStartThumbOffset = (eventY - thumbPadding.top - thumbSize / 2f).toInt() scrollToThumbOffset(dragStartThumbOffset) } @@ -371,44 +259,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private fun scrollToThumbOffset(thumbOffset: Int) { - val clampedThumbOffset = thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange) - - val scrollOffset = - (scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange).toInt() - - paddingTop - - scrollTo(scrollOffset) - } - - private fun scrollTo(offset: Int) { - if (childCount == 0) { + val rangeY = computeVerticalScrollRange() - computeVerticalScrollExtent() + val previousThumbOffset = this.thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange) + val previousOffsetY = rangeY * (previousThumbOffset / thumbOffsetRange.toFloat()) + val newThumbOffset = thumbOffset.coerceAtLeast(0).coerceAtMost(thumbOffsetRange) + val newOffsetY = rangeY * (newThumbOffset / thumbOffsetRange.toFloat()) + if (newOffsetY == 0f) { + // Hacky workaround to drift in vertical scroll offset where we just snap + // to the top if the thumb offset hit zero. + scrollToPosition(0) return } - - 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) - } - } + val dy = newOffsetY - previousOffsetY + scrollBy(0, max(dy.roundToInt(), -computeVerticalScrollOffset())) } // --- SCROLLBAR APPEARANCE --- @@ -425,7 +288,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr showingThumb = true thumbAnimator?.cancel() - thumbAnimator = fader.fadeIn(thumbView).also { it.start() } + thumbAnimator = slider.slideIn(thumbView).also { it.start() } } private fun hideScrollbar() { @@ -435,79 +298,16 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr showingThumb = false thumbAnimator?.cancel() - thumbAnimator = fader.fadeOut(thumbView).also { it.start() } - } - - private fun showPopup() { - if (showingPopup) { - return - } - - popupView.scaleX = 0f - popupView.scaleY = 0f - - popupView.alpha = 1f - showingPopup = true - popupAnimator?.cancel() - popupAnimator = fader.fadeIn(popupView).also { it.start() } - } - - private fun hidePopup() { - if (!showingPopup) { - return - } - - showingPopup = false - popupAnimator?.cancel() - popupAnimator = fader.fadeOut(popupView).also { it.start() } + thumbAnimator = slider.slideOut(thumbView).also { it.start() } } // --- LAYOUT STATE --- private val thumbOffsetRange: Int get() { - return height - thumbPadding.top - thumbPadding.bottom - thumbHeight - } - - 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 scrollOffsetRange: Int - get() = scrollRange - height - - private val itemHeight: Int - get() { - if (childCount == 0) { - return 0 - } - - val itemView = getChildAt(0) - getDecoratedBoundsWithMargins(itemView, tRect) - return tRect.height() + return height - thumbPadding.top - thumbPadding.bottom - thumbSize } - private val itemCount: Int - get() = - when (val mgr = layoutManager) { - is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1 - is LinearLayoutManager -> mgr.itemCount - else -> 0 - } - /** An interface to provide text to use in the popup when fast-scrolling. */ interface PopupProvider { /** @@ -531,6 +331,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private companion object { - const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500 + const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 500 } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt index 1d889c5a6..43155aa8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Animations.kt @@ -200,3 +200,25 @@ class MaterialFlipper(context: Context) { return AnimatorSet().apply { playTogether(outAnimator, inAnimator) } } } + +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) + + fun jumpOut(view: View) { + view.translationX = x.toFloat() + } + + fun slideOut(view: View): Animator { + val animator = + outConfig.genericFloat(view.translationX, x.toFloat()) { view.translationX = it } + return animator + } + + fun slideIn(view: View): Animator { + val animator = inConfig.genericFloat(view.translationX, 0f) { view.translationX = it } + return animator + } +} diff --git a/app/src/main/res/drawable/ic_scroll_24.xml b/app/src/main/res/drawable/ic_scroll_24.xml new file mode 100644 index 000000000..9ab04ff6c --- /dev/null +++ b/app/src/main/res/drawable/ic_scroll_24.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ui_popup.xml b/app/src/main/res/drawable/ui_popup.xml new file mode 100644 index 000000000..af0c3a354 --- /dev/null +++ b/app/src/main/res/drawable/ui_popup.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ui_scroll_thumb.xml b/app/src/main/res/drawable/ui_scroll_thumb.xml index 5802c85a9..ed34a3c3f 100644 --- a/app/src/main/res/drawable/ui_scroll_thumb.xml +++ b/app/src/main/res/drawable/ui_scroll_thumb.xml @@ -3,14 +3,9 @@ android:shape="rectangle" android:tint="?attr/colorSecondary"> - - + + android:width="48dp" + android:height="48dp" /> \ 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 138fef24c..fd9720d57 100644 --- a/app/src/main/res/layout/fragment_home_list.xml +++ b/app/src/main/res/layout/fragment_home_list.xml @@ -1,5 +1,5 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 3a5892b30..f611b6fde 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -25,6 +25,9 @@ 40dp 48dp + 48dp + 48dp + 16dp 128dp