From 98dde276d8014bdd000ff63375edd2aba5f93ef1 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 21 Aug 2024 16:14:44 +0200 Subject: [PATCH 1/6] Fix alignment issue when header height changes --- .../dpadrecyclerview/DpadScrollableLayout.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt index 6adf666e..6bc6f701 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt @@ -57,10 +57,8 @@ class DpadScrollableLayout @JvmOverloads constructor( private var currentOffset = 0 private var offsetInProgress: Int? = null - private var headerHeightChanged = false private var currentAnimator: ScrollAnimator? = null private var scrollDurationConfig: ScrollDurationConfig = DefaultScrollDurationConfig() - private var lastHeaderHeight = 0 // From RecyclerView private var scrollInterpolator = Interpolator { t -> @@ -114,11 +112,7 @@ class DpadScrollableLayout @JvmOverloads constructor( } } setMeasuredDimension(measuredWidth, childHeight) - if (newHeaderHeight != headerHeight) { - headerHeightChanged = newHeaderHeight != lastHeaderHeight - lastHeaderHeight = headerHeight - headerHeight = newHeaderHeight - } + headerHeight = newHeaderHeight } override fun measureChildWithMargins( @@ -153,7 +147,7 @@ class DpadScrollableLayout @JvmOverloads constructor( } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { - var currentAnchor = if (headerHeightChanged && lastHeaderHeight > 0) { + var currentAnchor = if (currentOffset != -headerHeight) { if (isHeaderVisible) { 0 } else { @@ -176,7 +170,6 @@ class DpadScrollableLayout @JvmOverloads constructor( child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight) currentAnchor += childHeight + layoutParams.bottomMargin } - headerHeightChanged = false } fun setScrollInterpolator(interpolator: Interpolator) { From da903423f59e76170986018c7d0881fef47d57b3 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 21 Aug 2024 16:14:50 +0200 Subject: [PATCH 2/6] Bump to next alpha --- docs/changelog.md | 8 ++++++++ gradle.properties | 2 +- mkdocs.yml | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) 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/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' From 49033cee1443f8c786483fa0399735529ae174b7 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 21 Aug 2024 16:16:11 +0200 Subject: [PATCH 3/6] Update current offset in the current layout pass --- .../java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt index 6bc6f701..3a3375ba 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt @@ -157,6 +157,7 @@ class DpadScrollableLayout @JvmOverloads constructor( currentOffset } val numberOfChildren = childCount + currentOffset = currentAnchor for (i in 0 until numberOfChildren) { val child = getChildAt(i) ?: continue if (child.visibility == View.GONE) { From 5bcdfd48c9928dd3ab37f1aa38c2e6589948998e Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 21 Aug 2024 16:35:01 +0200 Subject: [PATCH 4/6] Add new UI test for bug fix --- .../tests/layout/DpadScrollableLayoutTest.kt | 22 +++++++++++++++++++ .../dpadrecyclerview/DpadScrollableLayout.kt | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) 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..b5453ea3 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 @@ -366,6 +366,28 @@ class DpadScrollableLayoutTest { ) } + @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) + assertThat(header1Bounds).isEqualTo( + Rect(0, -headerHeight / 2, screenWidth, headerHeight / 2) + ) + } + @Test fun testRequestingLayoutDuringOffsetChangesDoesNotBreakLayout() { // given diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt index 3a3375ba..cc0b13bc 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadScrollableLayout.kt @@ -58,6 +58,7 @@ class DpadScrollableLayout @JvmOverloads constructor( private var currentOffset = 0 private var offsetInProgress: Int? = null private var currentAnimator: ScrollAnimator? = null + private var headerHeightChanged = false private var scrollDurationConfig: ScrollDurationConfig = DefaultScrollDurationConfig() // From RecyclerView @@ -112,6 +113,7 @@ class DpadScrollableLayout @JvmOverloads constructor( } } setMeasuredDimension(measuredWidth, childHeight) + headerHeightChanged = newHeaderHeight != headerHeight headerHeight = newHeaderHeight } @@ -147,7 +149,7 @@ class DpadScrollableLayout @JvmOverloads constructor( } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { - var currentAnchor = if (currentOffset != -headerHeight) { + var currentAnchor = if (currentOffset != -headerHeight && headerHeightChanged) { if (isHeaderVisible) { 0 } else { @@ -158,6 +160,7 @@ class DpadScrollableLayout @JvmOverloads constructor( } val numberOfChildren = childCount currentOffset = currentAnchor + headerHeightChanged = false for (i in 0 until numberOfChildren) { val child = getChildAt(i) ?: continue if (child.visibility == View.GONE) { From e99a31fdd3336e638ae3444fccc81c7340c8f932 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 21 Aug 2024 18:51:51 +0200 Subject: [PATCH 5/6] Fix header not keeping the same anchor --- .../tests/layout/DpadScrollableLayoutTest.kt | 55 +++++++++++-------- .../dpadrecyclerview_scrollable_container.xml | 46 +++++++++------- .../dpadrecyclerview/DpadScrollableLayout.kt | 25 +++++++-- 3 files changed, 79 insertions(+), 47 deletions(-) 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 b5453ea3..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,36 +352,25 @@ 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 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) - ) + val newBounds = getViewBounds(R.id.headerLayout) + assertThat(newBounds.bottom).isEqualTo(headerBounds.bottom) } @Test @@ -384,7 +391,7 @@ class DpadScrollableLayoutTest { // then val header1Bounds = getViewBounds(R.id.header1) assertThat(header1Bounds).isEqualTo( - Rect(0, -headerHeight / 2, screenWidth, headerHeight / 2) + 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"> - + + + + + -headerHeight + isHeaderCompletelyVisible = currentOffset == 0 } private fun cancelOffsetAnimation() { From 85c07b2ffb151bdf42f5aa2d70e325c75c93a1f4 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Wed, 21 Aug 2024 22:54:50 +0200 Subject: [PATCH 6/6] Update api file --- dpadrecyclerview/api/dpadrecyclerview.api | 1 + 1 file changed, 1 insertion(+) 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