From 98320fcd8d4a569c515a55d3d69ab7008512122d Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Tue, 29 Dec 2020 10:59:01 -0700 Subject: [PATCH] Update scrolling Remove the viscous interpolator and replace it with a variant of the default smooth scroller, dont like it as much but its less buggy. --- .../auxio/detail/AlbumDetailFragment.kt | 21 ++- .../auxio/detail/ArtistDetailFragment.kt | 12 ++ .../auxio/recycler/CenterSmoothScroller.kt | 20 +++ .../auxio/recycler/LinearCenterScroller.kt | 164 ------------------ 4 files changed, 48 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/recycler/CenterSmoothScroller.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/recycler/LinearCenterScroller.kt diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 64ceee535..33baf231c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.GridLayoutManager import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.adapters.AlbumDetailAdapter @@ -16,8 +17,9 @@ import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode -import org.oxycblt.auxio.recycler.LinearCenterScroller +import org.oxycblt.auxio.recycler.CenterSmoothScroller import org.oxycblt.auxio.ui.createToast +import org.oxycblt.auxio.ui.isLandscape import org.oxycblt.auxio.ui.setupAlbumSongActions /** @@ -89,6 +91,16 @@ class AlbumDetailFragment : DetailFragment() { binding.detailRecycler.apply { adapter = detailAdapter setHasFixedSize(true) + + if (isLandscape(resources)) { + layoutManager = GridLayoutManager(requireContext(), 2).also { + it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (position == 0) 2 else 1 + } + } + } + } } // If this fragment was created in order to nav to an item, then snap scroll to that item. @@ -152,13 +164,12 @@ class AlbumDetailFragment : DetailFragment() { // Calculate where the item for the currently played song is, and scroll to there val pos = detailModel.albumSortMode.value!!.getSortedSongList( detailModel.currentAlbum.value!!.songs - ).indexOf(playbackModel.song.value) + ).indexOf(playbackModel.song.value).inc() - if (pos != -1) { - // TODO: Re-add snap scrolling. + if (pos != 0) { binding.detailRecycler.post { binding.detailRecycler.layoutManager?.startSmoothScroll( - LinearCenterScroller(pos) + CenterSmoothScroller(requireContext(), pos) ) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 9b1cff939..65e44b434 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -7,12 +7,14 @@ import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.GridLayoutManager import org.oxycblt.auxio.R import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.ui.isLandscape import org.oxycblt.auxio.ui.setupAlbumActions /** @@ -87,6 +89,16 @@ class ArtistDetailFragment : DetailFragment() { binding.detailRecycler.apply { adapter = detailAdapter setHasFixedSize(true) + + if (isLandscape(resources)) { + layoutManager = GridLayoutManager(requireContext(), 2).also { + it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (position == 0) 2 else 1 + } + } + } + } } // --- VIEWMODEL SETUP --- diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/CenterSmoothScroller.kt b/app/src/main/java/org/oxycblt/auxio/recycler/CenterSmoothScroller.kt new file mode 100644 index 000000000..419bbc829 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/recycler/CenterSmoothScroller.kt @@ -0,0 +1,20 @@ +package org.oxycblt.auxio.recycler + +import android.content.Context +import androidx.recyclerview.widget.LinearSmoothScroller + +class CenterSmoothScroller(context: Context, target: Int) : LinearSmoothScroller(context) { + init { + targetPosition = target + } + + override fun calculateDtToFit( + viewStart: Int, + viewEnd: Int, + boxStart: Int, + boxEnd: Int, + snapPreference: Int + ): Int { + return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/LinearCenterScroller.kt b/app/src/main/java/org/oxycblt/auxio/recycler/LinearCenterScroller.kt deleted file mode 100644 index e2870d9d6..000000000 --- a/app/src/main/java/org/oxycblt/auxio/recycler/LinearCenterScroller.kt +++ /dev/null @@ -1,164 +0,0 @@ -package org.oxycblt.auxio.recycler - -import android.graphics.PointF -import android.view.View -import android.view.animation.Interpolator -import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.BuildConfig -import kotlin.math.exp - -/** - * A custom [RecyclerView.SmoothScroller] partially copied from [androidx.recyclerview.widget.LinearSmoothScroller] that has a scroll effect similar - * to [androidx.core.widget.NestedScrollView]. - * - * I don't know what half of this code does but it works and looks better than the default scroller so I use it - */ -class LinearCenterScroller(target: Int) : RecyclerView.SmoothScroller() { - private val viscousInterpolator = ViscousFluidInterpolator() - private var targetVec: PointF? = null - - // Temporary variables to keep track of the interim scroll target. These values do not - // point to a real item position, rather point to an estimated location pixels. - private var interimTargetDx = 0 - private var interimTargetDy = 0 - - init { - targetPosition = target - } - - // Not used - override fun onStart() {} - - override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) { - val dx = calcDxToMakeVisible(targetView) - val dy = calcDyToMakeVisible(targetView) - - action.update(-dx, -dy, DEFAULT_TIME, viscousInterpolator) - } - - override fun onSeekTargetStep(dx: Int, dy: Int, state: RecyclerView.State, action: Action) { - if (childCount == 0) { - stop() - return - } - - if (BuildConfig.DEBUG && targetVec != null && ((targetVec!!.x * dx < 0 || targetVec!!.y * dy < 0))) { - error("Scroll happened in the opposite direction of the target. Some calculations are wrong") - } - - interimTargetDx = clampApplyScroll(interimTargetDx, dx) - interimTargetDy = clampApplyScroll(interimTargetDy, dy) - - if (interimTargetDx == 0 && interimTargetDy == 0) { - updateActionForInterimTarget(action) - } - } - - override fun onStop() { - interimTargetDx = 0 - interimTargetDy = 0 - targetVec = null - } - - private fun calcDxToMakeVisible(view: View): Int { - val manager = layoutManager ?: return 0 - - if (!manager.canScrollHorizontally()) return 0 - - val params = view.layoutParams as RecyclerView.LayoutParams - val top = manager.getDecoratedTop(view) - params.topMargin - val bottom = manager.getDecoratedBottom(view) + params.bottomMargin - val start = manager.paddingTop - val end = manager.height - manager.paddingBottom - - return calculateDeltaToFit(top, bottom, start, end) - } - - fun calcDyToMakeVisible(view: View): Int { - val manager = layoutManager ?: return 0 - - if (!manager.canScrollVertically()) return 0 - - val params = view.layoutParams as RecyclerView.LayoutParams - val top = manager.getDecoratedTop(view) - params.topMargin - val bottom = manager.getDecoratedBottom(view) + params.bottomMargin - val start = manager.paddingTop - val end = manager.height - manager.paddingBottom - - return calculateDeltaToFit(top, bottom, start, end) - } - - private fun calculateDeltaToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int): Int { - // Center the view instead of making it sit at the top or bottom. - return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) - } - - private fun clampApplyScroll(argTmpDt: Int, dt: Int): Int { - var tmpDt = argTmpDt - tmpDt -= dt - if (argTmpDt * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset - return 0 - } - return tmpDt - } - - private fun updateActionForInterimTarget(action: Action) { - val scrollVector = computeScrollVectorForPosition(targetPosition) - if (scrollVector == null || (scrollVector.x == 0.0f && scrollVector.y == 0.0f)) { - val target = targetPosition - action.jumpTo(target) - stop() - return - } - normalize(scrollVector) - - targetVec = scrollVector - - interimTargetDx = (TARGET_SEEK_SCROLL_DIST * scrollVector.x).toInt() - interimTargetDy = (TARGET_SEEK_SCROLL_DIST * scrollVector.y).toInt() - - action.update( - (interimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO).toInt(), - (interimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO).toInt(), - DEFAULT_TIME, viscousInterpolator - ) - } - - /** - * A nice-looking interpolator that is similar to the [androidx.core.widget.NestedScrollView] interpolator. - */ - private inner class ViscousFluidInterpolator : Interpolator { - private val viscousNormalize = 1.0f / viscousFluid(1.0f) - private val viscousOffset = 1.0f - viscousNormalize * viscousFluid(1.0f) - - fun viscousFluid(argX: Float): Float { - var x = argX - - x *= VISCOUS_FLUID_SCALE - - if (x < 1.0f) { - x -= (1.0f - exp(-x)) - } else { - val start = 0.36787944117f; // 1/e == exp(-1) - x = 1.0f - exp(1.0f - x) - x = start + x * (1.0f - start) - } - return x - } - - override fun getInterpolation(input: Float): Float { - val interpolated = viscousNormalize * viscousFluid(input) - if (interpolated > 0) { - return interpolated + viscousOffset - } - return interpolated - } - } - - companion object { - private const val VISCOUS_FLUID_SCALE = 12.0f - private const val TARGET_SEEK_SCROLL_DIST = 10000 - private const val TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f - private const val DEFAULT_TIME = 500 - } -}