diff --git a/docs/changelog.md b/docs/changelog.md index b860e4d2..50f88922 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,14 @@ ## Version 1.4.0 +### 1.4.0-alpha04 + +2024-08-21 + +#### Bug fixes + +- Fix header alignment when `DpadScrollableLayout` triggers a new layout ([#254](https://github.com/rubensousa/DpadRecyclerView/pull/254)) + ### 1.4.0-alpha03 2024-08-14 diff --git a/dpadrecyclerview/api/dpadrecyclerview.api b/dpadrecyclerview/api/dpadrecyclerview.api index 361be4b8..104760de 100644 --- a/dpadrecyclerview/api/dpadrecyclerview.api +++ b/dpadrecyclerview/api/dpadrecyclerview.api @@ -220,6 +220,7 @@ public final class com/rubensousa/dpadrecyclerview/DpadScrollableLayout : androi public final fun getHeaderHeight ()I public final fun hideHeader (Z)V public static synthetic fun hideHeader$default (Lcom/rubensousa/dpadrecyclerview/DpadScrollableLayout;ZILjava/lang/Object;)V + public final fun isHeaderCompletelyVisible ()Z public final fun isHeaderVisible ()Z public final fun scrollHeaderTo (IZ)V public static synthetic fun scrollHeaderTo$default (Lcom/rubensousa/dpadrecyclerview/DpadScrollableLayout;IZILjava/lang/Object;)V diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/DpadScrollableLayoutTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/DpadScrollableLayoutTest.kt index 38296db7..e5b8146d 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/DpadScrollableLayoutTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/layout/DpadScrollableLayoutTest.kt @@ -19,6 +19,7 @@ package com.rubensousa.dpadrecyclerview.test.tests.layout import android.graphics.Rect import android.os.Bundle import android.view.View +import android.view.ViewGroup import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.testing.FragmentScenario @@ -87,6 +88,23 @@ class DpadScrollableLayoutTest { ) } + @Test + fun testHeaderStartsCompletelyVisible() { + val headerBounds = getViewBounds(R.id.headerLayout) + assertThat(headerBounds.top).isEqualTo(0) + + var completelyVisible = false + var visible = false + + fragmentScenario.onFragment { fragment -> + completelyVisible = fragment.scrollableLayout!!.isHeaderCompletelyVisible + visible = fragment.scrollableLayout!!.isHeaderVisible + } + + assertThat(completelyVisible).isTrue() + assertThat(visible).isTrue() + } + @Test fun testHidingHeaderWithoutAnimation() { // given @@ -259,13 +277,13 @@ class DpadScrollableLayoutTest { // when fragmentScenario.onFragment { fragment -> - fragment.scrollableLayout?.removeViewAt(0) + fragment.scrollableLayout?.findViewById(R.id.headerLayout)?.removeViewAt(0) } // then - val header2Bounds = getViewBounds(R.id.header2) + val headerBounds = getViewBounds(R.id.headerLayout) val recyclerViewBounds = getViewBounds(R.id.recyclerView) - assertThat(header2Bounds).isEqualTo( + assertThat(headerBounds).isEqualTo( Rect(0, -headerHeight, screenWidth, 0) ) assertThat(recyclerViewBounds).isEqualTo( @@ -286,13 +304,13 @@ class DpadScrollableLayoutTest { // when fragmentScenario.onFragment { fragment -> - fragment.scrollableLayout?.removeViewAt(0) + fragment.scrollableLayout?.findViewById(R.id.headerLayout)?.removeViewAt(0) } // then - val header2Bounds = getViewBounds(R.id.header2) + val headerBounds = getViewBounds(R.id.headerLayout) val recyclerViewBounds = getViewBounds(R.id.recyclerView) - assertThat(header2Bounds).isEqualTo( + assertThat(headerBounds).isEqualTo( Rect(0, 0, screenWidth, headerHeight) ) assertThat(recyclerViewBounds).isEqualTo( @@ -313,7 +331,7 @@ class DpadScrollableLayoutTest { // when fragmentScenario.onFragment { fragment -> - fragment.header1?.updateLayoutParams { + fragment.header1?.updateLayoutParams { height *= 2 } } @@ -334,35 +352,46 @@ class DpadScrollableLayoutTest { } @Test - fun testOffsetIsAdjustedWhenLayoutGetsBiggerWhileHeaderIsVisible() { + fun testBottomStaysAlignedWhenHeaderGrowsWhileVisible() { // given val headerHeight = getHeaderHeight() - val screenWidth = getWidth() - val screenHeight = getHeight() fragmentScenario.onFragment { fragment -> fragment.scrollableLayout?.scrollHeaderTo(topOffset = -headerHeight / 2) } waitViewAtCoordinates(R.id.header1, top = -headerHeight / 2, bottom = headerHeight / 2) + val headerBounds = getViewBounds(R.id.headerLayout) // when fragmentScenario.onFragment { fragment -> - fragment.header1?.updateLayoutParams { + fragment.header1?.updateLayoutParams { height *= 2 } } + // then + val newBounds = getViewBounds(R.id.headerLayout) + assertThat(newBounds.bottom).isEqualTo(headerBounds.bottom) + } + + @Test + fun testHeaderStaysPartiallyVisibleAfterLayoutRequest() { + // given + val headerHeight = getHeaderHeight() + val screenWidth = getWidth() + fragmentScenario.onFragment { fragment -> + fragment.scrollableLayout?.scrollHeaderTo(topOffset = -headerHeight / 2) + } + waitViewAtCoordinates(R.id.header1, top = -headerHeight / 2, bottom = headerHeight / 2) + + // when + fragmentScenario.onFragment { fragment -> + fragment.scrollableLayout?.requestLayout() + } + // then val header1Bounds = getViewBounds(R.id.header1) - val header2Bounds = getViewBounds(R.id.header2) - val recyclerViewBounds = getViewBounds(R.id.recyclerView) assertThat(header1Bounds).isEqualTo( - Rect(0, 0, screenWidth, headerHeight * 2) - ) - assertThat(header2Bounds).isEqualTo( - Rect(0, headerHeight * 2, screenWidth, headerHeight * 3) - ) - assertThat(recyclerViewBounds).isEqualTo( - Rect(0, headerHeight * 3, screenWidth, screenHeight + headerHeight * 3) + Rect(0, -headerHeight / 2, screenWidth, headerHeight / 2) ) } diff --git a/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_scrollable_container.xml b/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_scrollable_container.xml index 490eddac..9cd3a381 100644 --- a/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_scrollable_container.xml +++ b/dpadrecyclerview/src/androidTest/res/layout/dpadrecyclerview_scrollable_container.xml @@ -5,27 +5,35 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="wrap_content" + android:orientation="vertical"> - + + + + + @@ -114,11 +117,9 @@ class DpadScrollableLayout @JvmOverloads constructor( } } setMeasuredDimension(measuredWidth, childHeight) - if (newHeaderHeight != headerHeight) { - headerHeightChanged = newHeaderHeight != lastHeaderHeight - lastHeaderHeight = headerHeight - headerHeight = newHeaderHeight - } + headerHeightChanged = newHeaderHeight != headerHeight + lastHeaderHeight = headerHeight + headerHeight = newHeaderHeight } override fun measureChildWithMargins( @@ -126,7 +127,7 @@ class DpadScrollableLayout @JvmOverloads constructor( parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, - heightUsed: Int + heightUsed: Int, ) { val lp = child!!.layoutParams as LayoutParams val childWidthMeasureSpec = getChildMeasureSpec( @@ -153,16 +154,29 @@ class DpadScrollableLayout @JvmOverloads constructor( } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { - var currentAnchor = if (headerHeightChanged && lastHeaderHeight > 0) { - if (isHeaderVisible) { + var currentAnchor = if (headerHeightChanged) { + if (isHeaderCompletelyVisible) { + // If the header was completely visible before, keep it that way 0 + } else if (isHeaderVisible) { + /** + * The header was partially visible, so keep the anchor consistent if possible, + * and limit it to the top of the layout + */ + val currentHeaderBottom = currentOffset + lastHeaderHeight + val newTop = currentHeaderBottom - headerHeight + min(0, newTop) } else { + // The header was completely invisible, so ensure it stays that way -headerHeight } } else { + // The height didn't change, so keep the previous offset currentOffset } val numberOfChildren = childCount + currentOffset = currentAnchor + headerHeightChanged = false for (i in 0 until numberOfChildren) { val child = getChildAt(i) ?: continue if (child.visibility == View.GONE) { @@ -176,7 +190,6 @@ class DpadScrollableLayout @JvmOverloads constructor( child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight) currentAnchor += childHeight + layoutParams.bottomMargin } - headerHeightChanged = false } fun setScrollInterpolator(interpolator: Interpolator) { @@ -257,6 +270,7 @@ class DpadScrollableLayout @JvmOverloads constructor( } currentOffset = getChildAt(0)?.top ?: 0 isHeaderVisible = currentOffset > -headerHeight + isHeaderCompletelyVisible = currentOffset == 0 } private fun cancelOffsetAnimation() { diff --git a/gradle.properties b/gradle.properties index e6ee9e4c..14219a8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,4 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.enableR8.fullMode=true -LIBRARY_VERSION=1.4.0-alpha03 \ No newline at end of file +LIBRARY_VERSION=1.4.0-alpha04 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 997a5e05..5df18dbc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,7 +24,7 @@ theme: extra: dpadrecyclerview: - version: '1.4.0-alpha03' + version: '1.4.0-alpha04' social: - icon: 'fontawesome/brands/github' link: 'https://github.com/rubensousa/DpadRecyclerView'