Fix snapping after scroll actions in paging gallery

commit_hash:0bc9521306e4f743628b8bd796b4f15670d0e1b8
This commit is contained in:
grechka62
2026-04-23 22:30:56 +03:00
parent b029f9f5c5
commit e216f0bfa7
22 changed files with 59 additions and 17 deletions
@@ -3,6 +3,7 @@ package com.yandex.div.core.view2.items
import android.util.DisplayMetrics
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.core.view.doOnNextLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
@@ -10,6 +11,7 @@ import com.yandex.div.core.view2.divs.availableHeight
import com.yandex.div.core.view2.divs.availableWidth
import com.yandex.div.core.view2.divs.dpToPx
import com.yandex.div.core.view2.divs.gallery.DivGalleryAdapter
import com.yandex.div.core.view2.divs.gallery.PagerSnapStartHelper
import com.yandex.div.core.view2.divs.pager.DivPagerAdapter
import com.yandex.div.core.view2.divs.spToPx
import com.yandex.div.core.view2.divs.widgets.DivPagerView
@@ -73,9 +75,20 @@ internal sealed class DivViewWithItems {
*/
internal class PagingGallery(view: DivRecyclerView, direction: Direction) : Gallery(view, direction) {
override var currentItem: Int
get() = view.currentItem(direction)
set(value) = checkItem(value, itemCount) { view.smoothScrollToPosition(value) }
override fun createSmoothScroller(): RecyclerView.SmoothScroller {
return object : DivSmoothScroller(view) {
override fun calculateDtToFit(
viewStart: Int, viewEnd: Int,
boxStart: Int, boxEnd: Int,
snapPreference: Int
): Int {
val itemSpacing = view.pagerSnapStartHelper?.itemSpacing ?: return 0
val isHorizontal = (view.adapter as? DivGalleryAdapter)?.orientation == RecyclerView.HORIZONTAL
val size = if (isHorizontal) view.width else view.height
return (size - viewStart - viewEnd + itemSpacing) / 2
}
}
}
}
/**
@@ -91,19 +104,19 @@ internal sealed class DivViewWithItems {
get() = view.currentItem(direction)
set(value) {
checkItem(value, itemCount) {
val smoothScroller = object : LinearSmoothScroller(view.context) {
private val MILLISECONDS_PER_INCH = 50f // default is 25f, bigger - slower
override fun getHorizontalSnapPreference() = SNAP_TO_START
override fun getVerticalSnapPreference() = SNAP_TO_START
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
val smoothScroller = createSmoothScroller()
smoothScroller.targetPosition = value
view.layoutManager?.startSmoothScroll(smoothScroller)
}
}
protected open fun createSmoothScroller(): RecyclerView.SmoothScroller {
return object : DivSmoothScroller(view) {
override fun getHorizontalSnapPreference() = SNAP_TO_START
override fun getVerticalSnapPreference() = SNAP_TO_START
}
}
override val itemCount: Int
get() = view.itemCount
@@ -117,10 +130,27 @@ internal sealed class DivViewWithItems {
override fun setCurrentItemNoAnimation(index: Int) {
checkItem(index, itemCount) {
view.scrollToPosition(index)
view.pagerSnapStartHelper?.snapToPosition(index, waitForLayout = true)
?: view.scrollToPosition(index)
}
}
private fun PagerSnapStartHelper.snapToPosition(position: Int, waitForLayout: Boolean) {
val layoutManager = view.layoutManager ?: return
layoutManager.findViewByPosition(position)?.let { target ->
val offset = calculateDistanceToFinalSnap(layoutManager, target)
view.scrollBy(offset[0], offset[1])
return
}
if (!waitForLayout) return
view.doOnNextLayout {
snapToPosition(position, waitForLayout = false)
}
view.scrollToPosition(position)
}
override fun getIndicesOfItemWithId(id: String): List<Int> {
val adapter = view.adapter as? DivGalleryAdapter ?: return emptyList()
return adapter.visibleItems.getIndicesWithId(id) { div }
@@ -205,6 +235,16 @@ internal sealed class DivViewWithItems {
}
}
}
private open class DivSmoothScroller(view: RecyclerView): LinearSmoothScroller(view.context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics) =
MILLISECONDS_PER_INCH / displayMetrics.densityDpi
companion object {
private const val MILLISECONDS_PER_INCH = 50f // default is 25f, bigger - slower
}
}
}
/**
@@ -160,11 +160,13 @@ class GalleryItemsViewTest {
on { resources } doReturn mock()
}
whenever(view.layoutManager).thenReturn(layoutManager)
val scrollCaptor = argumentCaptor<LinearSmoothScroller>()
val underTest = createUnderTest(drv = view)
underTest.currentItem = 5
verify(view).smoothScrollToPosition(5)
verify(layoutManager).startSmoothScroll(scrollCaptor.capture())
Assert.assertEquals(5, scrollCaptor.firstValue.targetPosition)
}
@Test
@@ -84,7 +84,7 @@
{
"div_actions": [
{
"url": "div-action://set_next_item?id=gallery",
"url": "div-action://set_next_item?id=gallery&animated=false",
"log_id": "scroll_forward"
}
],
@@ -93,7 +93,7 @@
{
"div_actions": [
{
"url": "div-action://scroll_to_end?id=gallery",
"url": "div-action://scroll_to_end?id=gallery&animated=false",
"log_id": "scroll_to_end"
}
],
@@ -97,7 +97,7 @@
{
"div_actions": [
{
"url": "div-action://set_next_item?id=gallery",
"url": "div-action://set_next_item?id=gallery&animated=false",
"log_id": "scroll_forward"
}
],
@@ -106,7 +106,7 @@
{
"div_actions": [
{
"url": "div-action://scroll_to_end?id=gallery",
"url": "div-action://scroll_to_end?id=gallery&animated=false",
"log_id": "scroll_to_end"
}
],