Skip to content

Commit

Permalink
Merge pull request #259 from rubensousa/alignment_lookup
Browse files Browse the repository at this point in the history
Add AlignmentLookup for customizing individual alignment config per item
  • Loading branch information
rubensousa authored Aug 28, 2024
2 parents 310a980 + 17c184f commit 18ed50a
Show file tree
Hide file tree
Showing 15 changed files with 558 additions and 221 deletions.
13 changes: 13 additions & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
public abstract interface class com/rubensousa/dpadrecyclerview/AlignmentLookup {
public abstract fun getChildAlignment (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)Lcom/rubensousa/dpadrecyclerview/ChildAlignment;
public abstract fun getParentAlignment (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)Lcom/rubensousa/dpadrecyclerview/ParentAlignment;
}

public final class com/rubensousa/dpadrecyclerview/AlignmentLookup$DefaultImpls {
public static fun getChildAlignment (Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)Lcom/rubensousa/dpadrecyclerview/ChildAlignment;
public static fun getParentAlignment (Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;Landroidx/recyclerview/widget/RecyclerView$ViewHolder;)Lcom/rubensousa/dpadrecyclerview/ParentAlignment;
}

public final class com/rubensousa/dpadrecyclerview/ChildAlignment : android/os/Parcelable, com/rubensousa/dpadrecyclerview/ViewAlignment {
public static final field CREATOR Lcom/rubensousa/dpadrecyclerview/ChildAlignment$CREATOR;
public fun <init> ()V
Expand Down Expand Up @@ -128,6 +138,8 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle
public final fun removeView (Landroid/view/View;)V
public final fun removeViewAt (I)V
public final fun requestLayout ()V
public final fun setAlignmentLookup (Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;Z)V
public static synthetic fun setAlignmentLookup$default (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;ZILjava/lang/Object;)V
public final fun setAlignments (Lcom/rubensousa/dpadrecyclerview/ParentAlignment;Lcom/rubensousa/dpadrecyclerview/ChildAlignment;Z)V
public final fun setChildAlignment (Lcom/rubensousa/dpadrecyclerview/ChildAlignment;Z)V
public static synthetic fun setChildAlignment$default (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Lcom/rubensousa/dpadrecyclerview/ChildAlignment;ZILjava/lang/Object;)V
Expand Down Expand Up @@ -538,6 +550,7 @@ public final class com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutMana
public fun scrollVerticallyBy (ILandroidx/recyclerview/widget/RecyclerView$Recycler;Landroidx/recyclerview/widget/RecyclerView$State;)I
public final fun selectPosition (IIZ)V
public final fun selectSubPosition (IZ)V
public final fun setAlignmentLookup (Lcom/rubensousa/dpadrecyclerview/AlignmentLookup;Z)V
public final fun setAlignments (Lcom/rubensousa/dpadrecyclerview/ParentAlignment;Lcom/rubensousa/dpadrecyclerview/ChildAlignment;Z)V
public final fun setChildAlignment (Lcom/rubensousa/dpadrecyclerview/ChildAlignment;Z)V
public final fun setChildrenDrawingOrderEnabled (Z)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* 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.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 = ParentAlignment.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()
}
}

@Test
fun testScrollIsStillAppliedAfterFastScrolling() {
// given
launchFragment()
val bottomParentAlignment = ParentAlignment(fraction = 1f)
val bottomChildAlignment = ChildAlignment(fraction = 1f)
val recyclerViewBounds = getRecyclerViewBounds()

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

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

// when
KeyEvents.pressDown(times = 10)
waitForIdleScrollState()
KeyEvents.pressUp(times = 10)
waitForIdleScrollState()

// then
val viewBounds = getItemViewBounds(position = 0)
assertThat(viewBounds.bottom).isEqualTo(recyclerViewBounds.bottom)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024 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

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 {

/**
* @return the [ParentAlignment] configuration to be used for [viewHolder]
* or null to fallback to the default one set via [DpadRecyclerView.setParentAlignment]
*/
fun getParentAlignment(viewHolder: ViewHolder): ParentAlignment? = null

/**
* @return the [ChildAlignment] configuration to be used for [viewHolder]
* or null to fallback to the default one set via [DpadRecyclerView.setChildAlignment]
*/
fun getChildAlignment(viewHolder: ViewHolder): ChildAlignment? = null

}
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,17 @@ open class DpadRecyclerView @JvmOverloads constructor(
requireLayout().setAlignments(parent, child, smooth)
}

/**
* Set a custom alignment configuration for each ViewHolder. Check [AlignmentLookup].
* This is only supported in linear layouts
*
* @param lookup custom alignment configuration specific to some ViewHolders
* @param smooth true if the layout should be smooth scrolled to the new position
*/
fun setAlignmentLookup(lookup: AlignmentLookup?, smooth: Boolean = false) {
requireLayout().setAlignmentLookup(lookup, smooth)
}

/**
* Enable or disable smooth scrolling to new focused position. By default, this is set to true.
* When set to false, RecyclerView will scroll immediately to the focused view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import android.view.ViewGroup
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.AlignmentLookup
import com.rubensousa.dpadrecyclerview.ChildAlignment
import com.rubensousa.dpadrecyclerview.DpadLoopDirection
import com.rubensousa.dpadrecyclerview.DpadRecyclerView
Expand Down Expand Up @@ -535,6 +536,9 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),

fun setSpanCount(spanCount: Int) {
if (configuration.spanCount != spanCount) {
if (spanCount > 1) {
layoutAlignment.alignmentLookup = null
}
configuration.setSpanCount(spanCount)
spanFocusFinder.clearSpanCache()
pivotLayout.updateStructure()
Expand Down Expand Up @@ -621,24 +625,32 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
fun isFocusSearchDisabled(): Boolean = configuration.isFocusSearchDisabled

fun setAlignments(parent: ParentAlignment, child: ChildAlignment, smooth: Boolean) {
layoutAlignment.setParentAlignment(parent)
layoutAlignment.setChildAlignment(child)
layoutAlignment.parentAlignment = parent
layoutAlignment.childAlignment = child
scrollToSelectedPositionOrRequestLayout(smooth)
}

fun setParentAlignment(alignment: ParentAlignment, smooth: Boolean) {
layoutAlignment.setParentAlignment(alignment)
layoutAlignment.parentAlignment = alignment
scrollToSelectedPositionOrRequestLayout(smooth)
}

fun getParentAlignment(): ParentAlignment = layoutAlignment.getParentAlignment()
fun getParentAlignment(): ParentAlignment = layoutAlignment.parentAlignment

fun setChildAlignment(alignment: ChildAlignment, smooth: Boolean) {
layoutAlignment.setChildAlignment(alignment)
layoutAlignment.childAlignment = alignment
scrollToSelectedPositionOrRequestLayout(smooth)
}

fun getChildAlignment(): ChildAlignment = layoutAlignment.getChildAlignment()
fun getChildAlignment(): ChildAlignment = layoutAlignment.childAlignment

fun setAlignmentLookup(lookup: AlignmentLookup?, smooth: Boolean) {
if (lookup === layoutAlignment.alignmentLookup || configuration.spanCount != 1) {
return
}
layoutAlignment.alignmentLookup = lookup
scrollToSelectedPositionOrRequestLayout(smooth)
}

fun addOnViewHolderSelectedListener(listener: OnViewHolderSelectedListener) {
pivotSelector.addOnViewHolderSelectedListener(listener)
Expand Down
Loading

0 comments on commit 18ed50a

Please sign in to comment.