Skip to content

Commit

Permalink
Merge pull request #173 from rubensousa/fix_edge_alignment
Browse files Browse the repository at this point in the history
Fix edge alignment when a small number of items is visible in the screen
  • Loading branch information
rubensousa authored Dec 22, 2023
2 parents 4221bfc + a5e14e5 commit 9f2354d
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -346,7 +347,7 @@ class VerticalAlignmentTest : DpadRecyclerViewTest() {
}

@Test
fun testLayoutAlignsToKeylineWhenThereAreNotManyItems() {
fun testLayoutAlignsToKeylineWhenThereAreNotManyItemsAndEdgeMinIsUsed() {
val parentAlignment = ParentAlignment(
edge = Edge.MIN,
offset = 0,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal class ParentAlignmentCalculator {
fun updateLayoutInfo(
layoutManager: LayoutManager,
isVertical: Boolean,
reverseLayout: Boolean
reverseLayout: Boolean,
) {
size = if (isVertical) {
layoutManager.height
Expand Down Expand Up @@ -85,7 +85,7 @@ internal class ParentAlignmentCalculator {
fun updateStartLimit(
edge: Int,
viewAnchor: Int,
alignment: ParentAlignment
alignment: ParentAlignment,
) {
startEdge = edge
if (isStartUnknown) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>,
) : RecyclerView.Adapter<Adapter.ViewHolder>() {

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()
}

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,26 @@ class MainViewModel : ViewModel() {

private fun buildFeatureLists(): List<FeatureList> {
return listOf(
buildNestedFeatureList(),
buildListFeatures(),
buildGridFeatureList(),
buildComposeFeatureList(),
buildScrollingFeatureList(),
buildAnimationsFeatureList(),
)
}

private fun buildNestedFeatureList(): FeatureList {
private fun buildListFeatures(): FeatureList {
return FeatureList(
title = "Lists",
destinations = listOf(
ScreenDestination(
direction = MainFragmentDirections.openList(),
title = "Nested"
),
ScreenDestination(
direction = MainFragmentDirections.openShortList(),
title = "Short list"
),
ScreenDestination(
direction = MainFragmentDirections.openList().apply {
enableLooping = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/focused_text" android:state_focused="true" />
<item android:drawable="@color/transparent" />
</selector>
4 changes: 2 additions & 2 deletions sample/src/main/res/layout/screen_text_scrolling.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions sample/src/main/res/navigation/nav_graph.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
android:id="@+id/open_text_scrolling"
app:destination="@id/text_scrolling_fragment" />

<action
android:id="@+id/open_short_list"
app:destination="@id/short_list_fragment" />

</fragment>

<fragment
Expand Down Expand Up @@ -185,4 +189,9 @@
<fragment
android:id="@+id/text_scrolling_fragment"
android:name="com.rubensousa.dpadrecyclerview.sample.ui.screen.text.TextScrollingFragment" />

<fragment
android:id="@+id/short_list_fragment"
android:name="com.rubensousa.dpadrecyclerview.sample.ui.screen.list.ShortListFragment" />

</navigation>
1 change: 1 addition & 0 deletions sample/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
<color name="placeholder">#4B4B4B</color>
<color name="white">#FFFFFFFF</color>
<color name="transparent">#00000000</color>
<color name="focused_text">#2B2B2B</color>
</resources>

0 comments on commit 9f2354d

Please sign in to comment.