Skip to content

Commit

Permalink
Enforce scrolling to the keyline whenever AlignmentLookup is used
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed Aug 27, 2024
1 parent bcdd697 commit 0ed2c87
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2022 Rúben Sousa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.rubensousa.dpadrecyclerview.test.tests.alignment

import androidx.recyclerview.widget.RecyclerView
import com.google.common.truth.Truth.assertThat
import com.rubensousa.dpadrecyclerview.AlignmentLookup
import com.rubensousa.dpadrecyclerview.ChildAlignment
import com.rubensousa.dpadrecyclerview.ParentAlignment
import com.rubensousa.dpadrecyclerview.ParentAlignment.Edge
import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration
import com.rubensousa.dpadrecyclerview.test.helpers.getItemViewBounds
import com.rubensousa.dpadrecyclerview.test.helpers.getRecyclerViewBounds
import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView
import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState
import com.rubensousa.dpadrecyclerview.test.helpers.waitForLayout
import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest
import com.rubensousa.dpadrecyclerview.testing.KeyEvents
import com.rubensousa.dpadrecyclerview.testing.rules.DisableIdleTimeoutRule
import org.junit.Rule
import org.junit.Test

class AlignmentLookupTest : DpadRecyclerViewTest() {

@get:Rule
val idleTimeoutRule = DisableIdleTimeoutRule()

override fun getDefaultLayoutConfiguration(): TestLayoutConfiguration {
return TestLayoutConfiguration(
spans = 1,
orientation = RecyclerView.VERTICAL,
parentAlignment = ParentAlignment(
edge = Edge.MIN_MAX,
offset = 0,
fraction = 0f
),
childAlignment = ChildAlignment(
offset = 0,
fraction = 0f
)
)
}

@Test
fun testItemRespectsParentAlignmentLookup() {
// given
launchFragment()
val recyclerViewBounds = getRecyclerViewBounds()

// when
onRecyclerView("Set alignment") { recyclerView ->
recyclerView.setAlignmentLookup(object : AlignmentLookup {
override fun getParentAlignment(
viewHolder: RecyclerView.ViewHolder,
): ParentAlignment? {
if (viewHolder.layoutPosition == 0) {
return ParentAlignment(offset = 0, fraction = 0.5f)
}
return null
}
})
}

// then
waitForLayout()
val viewBounds = getItemViewBounds(position = 0)
assertThat(viewBounds.top).isEqualTo(recyclerViewBounds.height() / 2)
}

@Test
fun testItemRespectsChildAlignmentLookup() {
// given
launchFragment()

// when
onRecyclerView("Set alignment") { recyclerView ->
recyclerView.setAlignmentLookup(object : AlignmentLookup {
override fun getChildAlignment(viewHolder: RecyclerView.ViewHolder): ChildAlignment? {
if (viewHolder.layoutPosition == 0) {
return ChildAlignment(offset = 0, fraction = 0.5f)
}
return null
}
})
}

// then
waitForLayout()
val viewBounds = getItemViewBounds(position = 0)
assertThat(viewBounds.top).isEqualTo(-viewBounds.height() / 2)
}

@Test
fun testScrollingAlignsToLookup() {
// given
launchFragment()
val centerParentAlignment = ParentAlignment(fraction = 0.5f)
val centerChildAlignment = ChildAlignment(fraction = 0.5f)
val recyclerViewBounds = getRecyclerViewBounds()

// when
onRecyclerView("Set alignment") { recyclerView ->
recyclerView.setAlignmentLookup(object : AlignmentLookup {
override fun getParentAlignment(
viewHolder: RecyclerView.ViewHolder,
): ParentAlignment? {
if (viewHolder.layoutPosition % 2 == 0) {
return centerParentAlignment
}
return null
}

override fun getChildAlignment(viewHolder: RecyclerView.ViewHolder): ChildAlignment? {
if (viewHolder.layoutPosition % 2 == 0) {
return centerChildAlignment
}
return null
}
})
}
waitForLayout()

repeat(10) { index ->
val viewBounds = getItemViewBounds(position = index)
// then
if (index % 2 == 0) {
assertThat(viewBounds.centerY()).isEqualTo(recyclerViewBounds.height() / 2)
} else {
assertThat(viewBounds.top).isEqualTo(0)
}
KeyEvents.pressDown()
waitForIdleScrollState()
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder

/**
* Allows [DpadRecyclerView] to align differently for each ViewHolder.
* When this is used, the [ParentAlignment.Edge] preference has no effect
* and you're fully responsible to pick an anchor for all ViewHolders
*/
interface AlignmentLookup {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ internal class LayoutAlignment(
* If the returned value is 0, there is no need to scroll.
*/
fun calculateScrollToTarget(view: View): Int {
return parentAlignmentCalculator.calculateScrollOffset(
viewAnchor = getAnchor(view),
alignment = getParentAlignment(view)
)
val viewParentAlignment = getParentAlignment(view)
return if (alignmentLookup == null) {
parentAlignmentCalculator.calculateScrollOffset(
viewAnchor = getAnchor(view),
alignment = parentAlignment
)
} else {
parentAlignmentCalculator.calculateKeylineScrollOffset(
viewAnchor = getAnchor(view),
alignment = viewParentAlignment
)
}
}

fun getParentAlignment(view: View?): ParentAlignment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ internal class ParentAlignmentCalculator {
shouldAlignEndToKeyline(endAlignment) -> {
calculateScrollOffsetToKeyline(
anchor = endViewAnchor,
keyline = endKeyline)
keyline = endKeyline
)
}

else -> 0
Expand Down Expand Up @@ -155,22 +156,38 @@ internal class ParentAlignmentCalculator {
val alignToEndEdge = shouldAlignViewToEnd(viewAnchor, keyline, alignment)
if (!reverseLayout) {
if (alignToStartEdge) {
return min(startScrollLimit, calculateScrollOffsetToStartEdge(viewAnchor))
return calculateScrollToStartEdge(viewAnchor)
}
if (alignToEndEdge) {
return max(endScrollLimit, calculateScrollOffsetToEndEdge(viewAnchor))
return calculateScrollToEndEdge(viewAnchor)
}
} else {
if (alignToEndEdge) {
return max(endScrollLimit, calculateScrollOffsetToEndEdge(viewAnchor))
return calculateScrollToEndEdge(viewAnchor)
}
if (alignToStartEdge) {
return min(startScrollLimit, calculateScrollOffsetToStartEdge(viewAnchor))
return calculateScrollToStartEdge(viewAnchor)
}
}
return calculateScrollOffsetToKeyline(viewAnchor, keyline)
}

fun calculateKeylineScrollOffset(
viewAnchor: Int,
alignment: ParentAlignment,
): Int {
val keyline = calculateKeyline(alignment)
return calculateScrollOffsetToKeyline(viewAnchor, keyline)
}

private fun calculateScrollToStartEdge(anchor: Int): Int {
return min(startScrollLimit, calculateScrollOffsetToStartEdge(anchor))
}

private fun calculateScrollToEndEdge(anchor: Int): Int {
return max(endScrollLimit, calculateScrollOffsetToEndEdge(anchor))
}

fun calculateKeyline(alignment: ParentAlignment): Int {
var keyLine = 0
if (!reverseLayout) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import kotlin.math.min
internal abstract class StructureEngineer(
protected val layoutManager: RecyclerView.LayoutManager,
protected val layoutInfo: LayoutInfo,
protected val layoutAlignment: LayoutAlignment
protected val layoutAlignment: LayoutAlignment,
) {

companion object {
Expand Down Expand Up @@ -90,7 +90,7 @@ internal abstract class StructureEngineer(
layoutRequest: LayoutRequest,
viewProvider: ViewProvider,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): View

/**
Expand All @@ -103,7 +103,7 @@ internal abstract class StructureEngineer(
layoutRequest: LayoutRequest,
scrapViewProvider: ScrapViewProvider,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
)

/**
Expand All @@ -114,7 +114,7 @@ internal abstract class StructureEngineer(
viewProvider: ViewProvider,
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
layoutResult: LayoutResult
layoutResult: LayoutResult,
)

/**
Expand All @@ -125,7 +125,7 @@ internal abstract class StructureEngineer(
fun preLayoutChildren(
pivotPosition: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
recyclerViewProvider.updateRecycler(recycler)
val childCount = layoutInfo.getChildCount()
Expand Down Expand Up @@ -161,7 +161,7 @@ internal abstract class StructureEngineer(
preLayoutRequest: PreLayoutRequest,
layoutRequest: LayoutRequest,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
layoutManager.detachAndScrapAttachedViews(recycler)
val firstView = preLayoutRequest.firstView
Expand All @@ -185,7 +185,7 @@ internal abstract class StructureEngineer(
fun layoutChildren(
pivotPosition: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
recyclerViewProvider.updateRecycler(recycler)

Expand Down Expand Up @@ -231,7 +231,7 @@ internal abstract class StructureEngineer(
layoutRequest: LayoutRequest,
viewProvider: ViewProvider,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
val firstView = layoutInfo.getChildClosestToStart() ?: return
layoutRequest.prepend(layoutInfo.getLayoutPositionOf(firstView)) {
Expand All @@ -256,7 +256,7 @@ internal abstract class StructureEngineer(
layoutRequest: LayoutRequest,
viewProvider: ViewProvider,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): Boolean {
return false
}
Expand Down Expand Up @@ -291,7 +291,7 @@ internal abstract class StructureEngineer(
offset: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
recycleChildren: Boolean
recycleChildren: Boolean,
): Int {
if (recycleChildren) {
recyclerViewProvider.updateRecycler(recycler)
Expand Down Expand Up @@ -325,7 +325,7 @@ internal abstract class StructureEngineer(
layoutRequest: LayoutRequest,
state: RecyclerView.State,
scrollOffset: Int,
recycleChildren: Boolean
recycleChildren: Boolean,
) {
val scrollDistance = abs(scrollOffset)
layoutRequest.setRecyclingEnabled(recycleChildren)
Expand Down Expand Up @@ -460,7 +460,7 @@ internal abstract class StructureEngineer(
private fun alignPivot(
pivotView: View,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
var remainingScroll = if (layoutRequest.isVertical) {
state.remainingScrollVertical
Expand All @@ -482,7 +482,8 @@ internal abstract class StructureEngineer(
}

val parentAlignment = layoutAlignment.getParentAlignment(pivotView)
if (parentAlignment.edge != ParentAlignment.Edge.NONE
if (layoutAlignment.alignmentLookup == null
&& parentAlignment.edge != ParentAlignment.Edge.NONE
&& alignToEdge(parentAlignment, recycler, state, remainingScroll)
) {
layoutAlignment.updateScrollLimits()
Expand All @@ -509,7 +510,7 @@ internal abstract class StructureEngineer(
alignment: ParentAlignment,
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
remainingScroll: Int
remainingScroll: Int,
): Boolean {
val startView = layoutInfo.getChildClosestToStart() ?: return false
val endView = layoutInfo.getChildClosestToEnd() ?: return false
Expand Down Expand Up @@ -626,7 +627,7 @@ internal abstract class StructureEngineer(
startView: View,
remainingScroll: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
val distanceToEndEdge = max(0, layoutInfo.getEndAfterPadding() - endEdge)
layoutRequest.prepend(layoutInfo.getLayoutPositionOf(startView)) {
Expand All @@ -645,7 +646,7 @@ internal abstract class StructureEngineer(
endView: View,
remainingScroll: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
) {
val distanceToStart = max(0, startEdge - layoutInfo.getStartAfterPadding())
layoutRequest.append(layoutInfo.getLayoutPositionOf(endView)) {
Expand Down Expand Up @@ -695,7 +696,7 @@ internal abstract class StructureEngineer(
remainingSpace: Int,
viewProvider: ViewProvider,
layoutRequest: LayoutRequest,
state: RecyclerView.State
state: RecyclerView.State,
): Boolean {
return viewProvider.hasNext(layoutRequest, state)
&& (remainingSpace > 0 || layoutRequest.isInfinite)
Expand Down
Loading

0 comments on commit 0ed2c87

Please sign in to comment.