Skip to content

Commit

Permalink
Merge pull request #255 from rubensousa/1.4.0-alpha04
Browse files Browse the repository at this point in the history
1.4.0 alpha04
  • Loading branch information
rubensousa authored Aug 21, 2024
2 parents f24ba3c + 85c07b2 commit 6f94eb1
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 53 deletions.
8 changes: 8 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions dpadrecyclerview/api/dpadrecyclerview.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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,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<DpadScrollableLayout.LayoutParams> {
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)
)
}

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,12 +55,15 @@ 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 headerHeightChanged = false
private var currentAnimator: ScrollAnimator? = null
private var scrollDurationConfig: ScrollDurationConfig = DefaultScrollDurationConfig()
private var headerHeightChanged = false
private var lastHeaderHeight = 0
private var scrollDurationConfig: ScrollDurationConfig = DefaultScrollDurationConfig()

// From RecyclerView
private var scrollInterpolator = Interpolator { t ->
Expand Down Expand Up @@ -114,19 +117,17 @@ 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(
child: View?,
parentWidthMeasureSpec: Int,
widthUsed: Int,
parentHeightMeasureSpec: Int,
heightUsed: Int
heightUsed: Int,
) {
val lp = child!!.layoutParams as LayoutParams
val childWidthMeasureSpec = getChildMeasureSpec(
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -257,6 +270,7 @@ class DpadScrollableLayout @JvmOverloads constructor(
}
currentOffset = getChildAt(0)?.top ?: 0
isHeaderVisible = currentOffset > -headerHeight
isHeaderCompletelyVisible = currentOffset == 0
}

private fun cancelOffsetAnimation() {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
LIBRARY_VERSION=1.4.0-alpha04
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 6f94eb1

Please sign in to comment.