diff --git a/dpadrecyclerview/api/dpadrecyclerview.api b/dpadrecyclerview/api/dpadrecyclerview.api index f5c16e5b..44be0747 100644 --- a/dpadrecyclerview/api/dpadrecyclerview.api +++ b/dpadrecyclerview/api/dpadrecyclerview.api @@ -80,6 +80,7 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle public final fun isMinEdgeFadingEnabled ()Z public final fun isScrollEnabled ()Z protected final fun onFocusChanged (ZILandroid/graphics/Rect;)V + protected final fun onMeasure (II)V protected final fun onRequestFocusInDescendants (ILandroid/graphics/Rect;)Z public final fun onRtlPropertiesChanged (I)V public fun onScrollStateChanged (I)V diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/WrapContentTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/WrapContentTest.kt new file mode 100644 index 00000000..519cde34 --- /dev/null +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/WrapContentTest.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2023 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.layout + +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView +import com.google.common.truth.Truth.assertThat +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.tests.DpadRecyclerViewTest +import com.rubensousa.dpadrecyclerview.testing.R +import org.junit.Test + +class WrapContentTest : DpadRecyclerViewTest() { + + override fun getDefaultLayoutConfiguration(): TestLayoutConfiguration { + return TestLayoutConfiguration( + spans = 1, + orientation = RecyclerView.HORIZONTAL, + parentAlignment = ParentAlignment( + edge = ParentAlignment.Edge.NONE, + fraction = 0.5f + ), + childAlignment = ChildAlignment( + fraction = 0.5f + ) + ) + } + + @Test + fun testHorizontalWrapContentIsReplacedWithMatchParent() { + launchFragment( + getDefaultLayoutConfiguration(), getDefaultAdapterConfiguration() + .copy( + itemLayoutId = R.layout.dpadrecyclerview_test_item_horizontal, + numberOfItems = 1 + ) + ) + var rootWidth = 0 + executeOnFragment { fragment -> + rootWidth = fragment.requireView().width + } + onRecyclerView("Change layout params to WRAP_CONTENT") { recyclerView -> + recyclerView.updateLayoutParams { + width = ViewGroup.LayoutParams.WRAP_CONTENT + } + } + val bounds = getRecyclerViewBounds() + assertThat(bounds.width()).isEqualTo(rootWidth) + + val childBounds = getItemViewBounds(position = 0) + assertThat(childBounds.centerX()).isEqualTo(rootWidth / 2) + } + + @Test + fun testVerticalWrapContentIsReplacedWithMatchParent() { + launchFragment( + getDefaultLayoutConfiguration().copy(orientation = RecyclerView.VERTICAL), + getDefaultAdapterConfiguration().copy(numberOfItems = 1) + ) + var rootHeight = 0 + executeOnFragment { fragment -> + rootHeight = fragment.requireView().height + } + + onRecyclerView("Change layout params to WRAP_CONTENT") { recyclerView -> + recyclerView.updateLayoutParams { + height = ViewGroup.LayoutParams.WRAP_CONTENT + } + } + + val bounds = getRecyclerViewBounds() + assertThat(bounds.height()).isEqualTo(rootHeight) + + val childBounds = getItemViewBounds(position = 0) + assertThat(childBounds.centerY()).isEqualTo(rootHeight / 2) + } + + +} diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt index f9d76d52..b7d8ee0f 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt @@ -25,6 +25,7 @@ import android.view.Gravity import android.view.KeyEvent import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import android.view.animation.Interpolator import androidx.annotation.Px import androidx.core.view.ViewCompat @@ -45,6 +46,10 @@ import com.rubensousa.dpadrecyclerview.layoutmanager.PivotLayoutManager * and receives DPAD key events. * To scroll manually to any given item, * check [setSelectedPosition], [setSelectedPositionSmooth] and other related methods. + * + * When using wrap_content for the main scrolling direction, + * [DpadRecyclerView] will still measure itself to match its parent's size, + * but will layout all items at once without any recycling. */ open class DpadRecyclerView @JvmOverloads constructor( context: Context, @@ -211,6 +216,40 @@ open class DpadRecyclerView @JvmOverloads constructor( } } + // Overriding to prevent WRAP_CONTENT behavior by replacing it + // with the size defined by the parent. Leanback also doesn't support WRAP_CONTENT + final override fun onMeasure(widthSpec: Int, heightSpec: Int) { + val layout = layoutManager + if (layout == null) { + super.onMeasure(widthSpec, heightSpec) + return + } + val layoutParams = layoutParams + if (getOrientation() == VERTICAL + && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT + ) { + super.onMeasure( + widthSpec, MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(heightSpec), + MeasureSpec.EXACTLY + ) + ) + return + } else if (getOrientation() == HORIZONTAL + && layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT + ) { + super.onMeasure( + MeasureSpec.makeMeasureSpec( + MeasureSpec.getSize(widthSpec), + MeasureSpec.EXACTLY + ), + heightSpec, + ) + return + } + super.onMeasure(widthSpec, heightSpec) + } + final override fun setItemAnimator(animator: ItemAnimator?) { super.setItemAnimator(animator) /** diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt index 4c8384d4..7a8dd8eb 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt @@ -18,6 +18,7 @@ package com.rubensousa.dpadrecyclerview.layoutmanager.layout import android.graphics.Rect import android.view.View +import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.recyclerview.widget.OrientationHelper import androidx.recyclerview.widget.RecyclerView @@ -332,7 +333,11 @@ internal class LayoutInfo( } fun isInfinite(): Boolean { - return orientationHelper.mode == View.MeasureSpec.UNSPECIFIED && orientationHelper.end == 0 + val currentRecyclerView = recyclerView ?: return false + val layoutParams = currentRecyclerView.layoutParams + return if (isVertical() && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + true + } else isHorizontal() && layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT } fun isViewFocusable(view: View): Boolean { diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/ScreenDestination.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/ScreenDestination.kt index 753986c2..19f0b8c7 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/ScreenDestination.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/ScreenDestination.kt @@ -18,4 +18,17 @@ package com.rubensousa.dpadrecyclerview.sample.ui.screen.main import androidx.navigation.NavDirections -data class ScreenDestination(val direction: NavDirections, val title: String) \ No newline at end of file +data class ScreenDestination(val direction: NavDirections, val title: String) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ScreenDestination + if (title != other.title) return false + return true + } + + override fun hashCode(): Int { + return title.hashCode() + } +}