Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.4.0 alpha04 #255

Merged
merged 6 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading