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.appcompat.widget.PopupMenu
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.adapters.AlbumDetailAdapter
|
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.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
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.createToast
|
||||||
|
import org.oxycblt.auxio.ui.isLandscape
|
||||||
import org.oxycblt.auxio.ui.setupAlbumSongActions
|
import org.oxycblt.auxio.ui.setupAlbumSongActions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,6 +91,16 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
setHasFixedSize(true)
|
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.
|
// 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
|
// Calculate where the item for the currently played song is, and scroll to there
|
||||||
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
|
val pos = detailModel.albumSortMode.value!!.getSortedSongList(
|
||||||
detailModel.currentAlbum.value!!.songs
|
detailModel.currentAlbum.value!!.songs
|
||||||
).indexOf(playbackModel.song.value)
|
).indexOf(playbackModel.song.value).inc()
|
||||||
|
|
||||||
if (pos != -1) {
|
if (pos != 0) {
|
||||||
// TODO: Re-add snap scrolling.
|
|
||||||
binding.detailRecycler.post {
|
binding.detailRecycler.post {
|
||||||
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
||||||
LinearCenterScroller(pos)
|
CenterSmoothScroller(requireContext(), pos)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,14 @@ import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter
|
import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.ui.isLandscape
|
||||||
import org.oxycblt.auxio.ui.setupAlbumActions
|
import org.oxycblt.auxio.ui.setupAlbumActions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,6 +89,16 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
adapter = detailAdapter
|
adapter = detailAdapter
|
||||||
setHasFixedSize(true)
|
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 ---
|
// --- 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