Skip to content

Commit

Permalink
Fix header not keeping the same anchor
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensousa committed Aug 21, 2024
1 parent 5bcdfd4 commit e99a31f
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -259,13 +277,13 @@ class DpadScrollableLayoutTest {

// when
fragmentScenario.onFragment { fragment ->
fragment.scrollableLayout?.removeViewAt(0)
fragment.scrollableLayout?.findViewById<ViewGroup>(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(
Expand All @@ -286,13 +304,13 @@ class DpadScrollableLayoutTest {

// when
fragmentScenario.onFragment { fragment ->
fragment.scrollableLayout?.removeViewAt(0)
fragment.scrollableLayout?.findViewById<ViewGroup>(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(
Expand All @@ -313,7 +331,7 @@ class DpadScrollableLayoutTest {

// when
fragmentScenario.onFragment { fragment ->
fragment.header1?.updateLayoutParams<DpadScrollableLayout.LayoutParams> {
fragment.header1?.updateLayoutParams {
height *= 2
}
}
Expand All @@ -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<DpadScrollableLayout.LayoutParams> {
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
Expand All @@ -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)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/header1"
<LinearLayout
android:id="@+id/headerLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/dpadrecyclerview_header_size"
android:background="@drawable/dpadrecyclerview_item_background"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:text="Header 1"
android:textColor="@color/dpadrecyclerview_item_text" />
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/header2"
android:layout_width="match_parent"
android:layout_height="@dimen/dpadrecyclerview_header_size"
android:background="@drawable/dpadrecyclerview_item_background"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:text="Header 2"
android:textColor="@color/dpadrecyclerview_item_text" />
<TextView
android:id="@+id/header1"
android:layout_width="match_parent"
android:layout_height="@dimen/dpadrecyclerview_header_size"
android:background="@drawable/dpadrecyclerview_item_background"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:text="Header 1"
android:textColor="@color/dpadrecyclerview_item_text" />

<TextView
android:id="@+id/header2"
android:layout_width="match_parent"
android:layout_height="@dimen/dpadrecyclerview_header_size"
android:background="@drawable/dpadrecyclerview_item_background"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:text="Header 2"
android:textColor="@color/dpadrecyclerview_item_text" />

</LinearLayout>

<com.rubensousa.dpadrecyclerview.DpadRecyclerView
android:id="@+id/recyclerView"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import kotlin.math.min
* To show it back again, use [showHeader]
*/
class DpadScrollableLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
context: Context, attrs: AttributeSet? = null,
) : LinearLayout(context, attrs) {

var headerHeight = 0
Expand All @@ -55,10 +55,14 @@ class DpadScrollableLayout @JvmOverloads constructor(
var isHeaderVisible = true
private set

var isHeaderCompletelyVisible = true
private set

private var currentOffset = 0
private var offsetInProgress: Int? = null
private var currentAnimator: ScrollAnimator? = null
private var headerHeightChanged = false
private var lastHeaderHeight = 0
private var scrollDurationConfig: ScrollDurationConfig = DefaultScrollDurationConfig()

// From RecyclerView
Expand Down Expand Up @@ -114,6 +118,7 @@ class DpadScrollableLayout @JvmOverloads constructor(
}
setMeasuredDimension(measuredWidth, childHeight)
headerHeightChanged = newHeaderHeight != headerHeight
lastHeaderHeight = headerHeight
headerHeight = newHeaderHeight
}

Expand All @@ -122,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(
Expand All @@ -149,13 +154,24 @@ class DpadScrollableLayout @JvmOverloads constructor(
}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentAnchor = if (currentOffset != -headerHeight && headerHeightChanged) {
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
Expand Down Expand Up @@ -254,6 +270,7 @@ class DpadScrollableLayout @JvmOverloads constructor(
}
currentOffset = getChildAt(0)?.top ?: 0
isHeaderVisible = currentOffset > -headerHeight
isHeaderCompletelyVisible = currentOffset == 0
}

private fun cancelOffsetAnimation() {
Expand Down

0 comments on commit e99a31f

Please sign in to comment.