From 56d3db1fb10f3ac9bc666e1b4dacc3cf5f03c483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Fri, 22 Dec 2023 00:30:23 +0000 Subject: [PATCH 1/3] Fix edge alignment when a small number of items is visible in the screen --- .../tests/alignment/VerticalAlignmentTest.kt | 54 +++++++++- .../alignment/ParentAlignmentCalculator.kt | 23 +++-- .../ParentAlignmentCalculatorTest.kt | 28 ++++++ .../ui/screen/list/ShortListFragment.kt | 99 +++++++++++++++++++ .../sample/ui/screen/main/MainViewModel.kt | 8 +- sample/src/main/res/navigation/nav_graph.xml | 9 ++ 6 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ShortListFragment.kt 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..3c699778 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 0a4fcfd2..32fefeec 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(), buildFocusFeatureList(), @@ -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/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index 29b982ad..ec4382d0 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -72,6 +72,10 @@ android:id="@+id/open_item_animations" app:destination="@id/item_animations_fragment" /> + + + + + From e2f99691641dbfd3d60f6e62510b1959befc5607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Fri, 22 Dec 2023 00:33:32 +0000 Subject: [PATCH 2/3] Fix formatting --- .../layoutmanager/alignment/ParentAlignmentCalculator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3c699778..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 @@ -97,7 +97,7 @@ internal class ParentAlignmentCalculator { calculateScrollOffsetToStartEdge(edge) } else if (isLayoutComplete() || alignment.preferKeylineOverEdge - || alignment.edge == Edge.NONE + || alignment.edge == Edge.NONE ) { calculateScrollOffsetToKeyline(viewAnchor, keyLine) } else { From a5e14e54d2fd0132fb9d494fbd114c1f40b403e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Fri, 22 Dec 2023 11:03:03 +0000 Subject: [PATCH 3/3] Use drawable instead of color --- .../src/main/res/drawable/list_text_container_background.xml | 5 +++++ sample/src/main/res/layout/screen_text_scrolling.xml | 4 ++-- sample/src/main/res/values/colors.xml | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 sample/src/main/res/drawable/list_text_container_background.xml 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/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