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.
This commit is contained in:
parent
966adf7704
commit
98320fcd8d
4 changed files with 48 additions and 169 deletions
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ---
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue