diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/alignment/VerticalAlignmentTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/alignment/VerticalAlignmentTest.kt index 92db6323..baebb528 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/alignment/VerticalAlignmentTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/alignment/VerticalAlignmentTest.kt @@ -31,6 +31,7 @@ import com.rubensousa.dpadrecyclerview.test.helpers.selectLastPosition import com.rubensousa.dpadrecyclerview.test.helpers.selectPosition import com.rubensousa.dpadrecyclerview.test.helpers.updateChildAlignment import com.rubensousa.dpadrecyclerview.test.helpers.updateParentAlignment +import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R @@ -346,7 +347,7 @@ class VerticalAlignmentTest : DpadRecyclerViewTest() { } @Test - fun testLayoutAlignsToKeylineWhenThereAreNotManyItems() { + fun testLayoutAlignsToKeylineWhenThereAreNotManyItemsAndEdgeMinIsUsed() { val parentAlignment = ParentAlignment( edge = Edge.MIN, offset = 0, @@ -355,12 +356,61 @@ class VerticalAlignmentTest : DpadRecyclerViewTest() { ) launchFragment( getDefaultLayoutConfiguration().copy(parentAlignment = parentAlignment), - getDefaultAdapterConfiguration().copy(numberOfItems = 2) + getDefaultAdapterConfiguration().copy(numberOfItems = 3) + ) + val viewBounds = getItemViewBounds(position = 0) + assertThat(viewBounds.centerY()).isEqualTo(getRecyclerViewBounds().centerY()) + KeyEvents.pressDown() + waitForIdleScrollState() + KeyEvents.pressUp() + waitForIdleScrollState() + assertThat(getItemViewBounds(position = 0).centerY()) + .isEqualTo(getRecyclerViewBounds().centerY()) + } + + @Test + fun testLayoutAlignsToKeylineWhenThereAreNotManyItemsAndEdgeMaxIsUsed() { + val parentAlignment = ParentAlignment( + edge = Edge.MAX, + offset = 0, + fraction = 0.5f, + ) + launchFragment( + getDefaultLayoutConfiguration().copy(parentAlignment = parentAlignment), + getDefaultAdapterConfiguration().copy(numberOfItems = 3) ) val viewBounds = getItemViewBounds(position = 0) assertThat(viewBounds.centerY()).isEqualTo(getRecyclerViewBounds().centerY()) + KeyEvents.pressDown() + waitForIdleScrollState() + KeyEvents.pressUp() + waitForIdleScrollState() + assertThat(getItemViewBounds(position = 0).centerY()) + .isEqualTo(getRecyclerViewBounds().centerY()) } + @Test + fun testLayoutAlignsToKeylineWhenThereAreNotManyItemsAndEdgeNoneIsUsed() { + val parentAlignment = ParentAlignment( + edge = Edge.NONE, + offset = 0, + fraction = 0.5f, + ) + launchFragment( + getDefaultLayoutConfiguration().copy(parentAlignment = parentAlignment), + getDefaultAdapterConfiguration().copy(numberOfItems = 3) + ) + val viewBounds = getItemViewBounds(position = 0) + assertThat(viewBounds.centerY()).isEqualTo(getRecyclerViewBounds().centerY()) + KeyEvents.pressDown() + waitForIdleScrollState() + KeyEvents.pressUp() + waitForIdleScrollState() + assertThat(getItemViewBounds(position = 0).centerY()) + .isEqualTo(getRecyclerViewBounds().centerY()) + } + + @Test fun testLayoutAlignsToKeylineInsteadOfMaxEdgeWhenThereAreNotManyItems() { val parentAlignment = ParentAlignment( diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/alignment/ParentAlignmentCalculator.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/alignment/ParentAlignmentCalculator.kt index 278454b9..b0aef227 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/alignment/ParentAlignmentCalculator.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/alignment/ParentAlignmentCalculator.kt @@ -46,7 +46,7 @@ internal class ParentAlignmentCalculator { fun updateLayoutInfo( layoutManager: LayoutManager, isVertical: Boolean, - reverseLayout: Boolean + reverseLayout: Boolean, ) { size = if (isVertical) { layoutManager.height @@ -85,7 +85,7 @@ internal class ParentAlignmentCalculator { fun updateStartLimit( edge: Int, viewAnchor: Int, - alignment: ParentAlignment + alignment: ParentAlignment, ) { startEdge = edge if (isStartUnknown) { @@ -95,7 +95,10 @@ internal class ParentAlignmentCalculator { val keyLine = calculateKeyline(alignment) startScrollLimit = if (shouldAlignViewToStart(viewAnchor, keyLine, alignment)) { calculateScrollOffsetToStartEdge(edge) - } else if (!isLayoutIncomplete() || alignment.preferKeylineOverEdge) { + } else if (isLayoutComplete() + || alignment.preferKeylineOverEdge + || alignment.edge == Edge.NONE + ) { calculateScrollOffsetToKeyline(viewAnchor, keyLine) } else { 0 @@ -115,7 +118,10 @@ internal class ParentAlignmentCalculator { val keyline = calculateKeyline(alignment) endScrollLimit = if (shouldAlignViewToEnd(viewAnchor, keyline, alignment)) { calculateScrollOffsetToEndEdge(edge) - } else if (!isLayoutComplete() || alignment.preferKeylineOverEdge) { + } else if (isLayoutComplete() + || alignment.preferKeylineOverEdge + || alignment.edge == Edge.NONE + ) { calculateScrollOffsetToKeyline(viewAnchor, keyline) } else { 0 @@ -137,7 +143,7 @@ internal class ParentAlignmentCalculator { */ fun calculateScrollOffset( viewAnchor: Int, - alignment: ParentAlignment + alignment: ParentAlignment, ): Int { val keyline = calculateKeyline(alignment) val alignToStartEdge = shouldAlignViewToStart(viewAnchor, keyline, alignment) @@ -181,7 +187,7 @@ internal class ParentAlignmentCalculator { private fun shouldAlignViewToStart( viewAnchor: Int, keyline: Int, - alignment: ParentAlignment + alignment: ParentAlignment, ): Boolean { if (isStartUnknown || !shouldAlignToStartEdge(alignment.edge)) { return false @@ -195,7 +201,7 @@ internal class ParentAlignmentCalculator { private fun shouldAlignViewToEnd( viewAnchor: Int, keyline: Int, - alignment: ParentAlignment + alignment: ParentAlignment, ): Boolean { if (isEndUnknown || !shouldAlignToEndEdge(alignment.edge)) { return false @@ -219,6 +225,9 @@ internal class ParentAlignmentCalculator { } private fun isLayoutComplete(): Boolean { + if (isEndUnknown || isStartUnknown) { + return false + } return endEdge - startEdge >= size - paddingEnd - paddingStart && endEdge <= size - paddingEnd && startEdge >= paddingStart diff --git a/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/alignment/ParentAlignmentCalculatorTest.kt b/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/alignment/ParentAlignmentCalculatorTest.kt index 76bcaa4e..3e60c4e2 100644 --- a/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/alignment/ParentAlignmentCalculatorTest.kt +++ b/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/alignment/ParentAlignmentCalculatorTest.kt @@ -354,6 +354,34 @@ class ParentAlignmentCalculatorTest { assertThat(alignmentCalculator.endScrollLimit).isEqualTo(0) } + @Test + fun `start scroll limit should be distance to keyline when layout is incomplete and edge none is set`() { + setLayoutProperties(orientation = RecyclerView.VERTICAL, reverseLayout = false) + + val alignment = ParentAlignment( + edge = ParentAlignment.Edge.NONE, + offset = 0, + fraction = 0.5f, + ) + + val viewAnchor = verticalCenterKeyline + verticalViewHeight / 2 + alignmentCalculator.updateStartLimit( + edge = 0, + viewAnchor = viewAnchor, + alignment = alignment + ) + + alignmentCalculator.updateEndLimit( + edge = height - verticalViewHeight, + viewAnchor = height - verticalViewHeight / 2, + alignment = alignment + ) + + assertThat(alignmentCalculator.startScrollLimit).isEqualTo( + viewAnchor - verticalCenterKeyline, + ) + } + private fun setLayoutProperties(orientation: Int, reverseLayout: Boolean) { if (orientation == RecyclerView.VERTICAL) { alignmentCalculator.updateLayoutInfo( diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt new file mode 100644 index 00000000..450d1833 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt @@ -0,0 +1,99 @@ +package com.rubensousa.dpadrecyclerview.sample.ui.screen.list + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.dpadrecyclerview.DpadViewHolder +import com.rubensousa.dpadrecyclerview.ParentAlignment +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.databinding.MainAdapterItemFeatureBinding +import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenRecyclerviewBinding +import com.rubensousa.dpadrecyclerview.sample.ui.dpToPx +import com.rubensousa.dpadrecyclerview.sample.ui.viewBinding +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.common.ItemAnimator +import com.rubensousa.dpadrecyclerview.spacing.DpadLinearSpacingDecoration + +class ShortListFragment : Fragment(R.layout.screen_recyclerview) { + + private val binding by viewBinding(ScreenRecyclerviewBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.recyclerView.apply { + setParentAlignment( + ParentAlignment( + edge = ParentAlignment.Edge.NONE, + fraction = 0.5f + ) + ) + addItemDecoration( + DpadLinearSpacingDecoration.create( + itemSpacing = dpToPx(16.dp), + perpendicularEdgeSpacing = dpToPx(48.dp) + ) + ) + adapter = Adapter( + items = List(5) { i -> + "Item $i" + } + ) + requestFocus() + } + } + + internal class Adapter( + private val items: List, + ) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + MainAdapterItemFeatureBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun onViewRecycled(holder: ViewHolder) { + super.onViewRecycled(holder) + holder.recycle() + } + + class ViewHolder( + private val binding: MainAdapterItemFeatureBinding, + ) : RecyclerView.ViewHolder(binding.root), DpadViewHolder { + + private val animator = ItemAnimator(binding.root) + + init { + itemView.setOnFocusChangeListener { v, hasFocus -> + if (hasFocus) { + animator.startFocusGainAnimation() + } else { + animator.startFocusLossAnimation() + } + } + } + + fun bind(item: String) { + binding.textView.text = item + } + + fun recycle() { + animator.cancel() + } + + } + + } + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt index 0a3557b4..15516f98 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt @@ -37,7 +37,7 @@ class MainViewModel : ViewModel() { private fun buildFeatureLists(): List { return listOf( - buildNestedFeatureList(), + buildListFeatures(), buildGridFeatureList(), buildComposeFeatureList(), buildScrollingFeatureList(), @@ -45,7 +45,7 @@ class MainViewModel : ViewModel() { ) } - private fun buildNestedFeatureList(): FeatureList { + private fun buildListFeatures(): FeatureList { return FeatureList( title = "Lists", destinations = listOf( @@ -53,6 +53,10 @@ class MainViewModel : ViewModel() { direction = MainFragmentDirections.openList(), title = "Nested" ), + ScreenDestination( + direction = MainFragmentDirections.openShortList(), + title = "Short list" + ), ScreenDestination( direction = MainFragmentDirections.openList().apply { enableLooping = true diff --git a/sample/src/main/res/drawable/list_text_container_background.xml b/sample/src/main/res/drawable/list_text_container_background.xml new file mode 100644 index 00000000..6014f6ea --- /dev/null +++ b/sample/src/main/res/drawable/list_text_container_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/screen_text_scrolling.xml b/sample/src/main/res/layout/screen_text_scrolling.xml index 21654508..e228b383 100644 --- a/sample/src/main/res/layout/screen_text_scrolling.xml +++ b/sample/src/main/res/layout/screen_text_scrolling.xml @@ -25,7 +25,7 @@ android:layout_height="48dp" android:layout_marginStart="40dp" android:layout_marginTop="40dp" - android:background="@color/list_item_background" + android:background="@drawable/list_item_background" android:focusable="true" android:focusableInTouchMode="true" android:src="@drawable/ic_back" @@ -47,7 +47,7 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginStart="24dp" - android:background="@color/list_text_background" + android:background="@drawable/list_text_container_background" android:fadeScrollbars="false" android:scrollbarAlwaysDrawVerticalTrack="true" android:scrollbarSize="6dp" diff --git a/sample/src/main/res/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index 7ff31400..6dc83727 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -76,6 +76,10 @@ android:id="@+id/open_text_scrolling" app:destination="@id/text_scrolling_fragment" /> + + + + + diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml index 86ea7adf..d2694d25 100644 --- a/sample/src/main/res/values/colors.xml +++ b/sample/src/main/res/values/colors.xml @@ -9,4 +9,5 @@ #4B4B4B #FFFFFFFF #00000000 + #2B2B2B \ No newline at end of file