I’d like to achieve this behaviour with compose.
So basically it’s a nested scroll, when the list isn’t scrolled the header is expanded at maximum height (e.g. 264.dp), and when the list starts to scroll, it scrolls over the header but respect to a minimum height of header to be still visible (e.g. 115.dp).
I have tried so many things but still couldn’t achieve this behaviour. The last thing I tried is a complex solution with Modifier.swipeable
and nestedScroll
in a motion layout
which works almost fine but first swipeable is deprecated, and second, it’s really complex. Isn’t a better way of doing this?
Box(
modifier = Modifier.fillMaxSize(),
) {
val swipingState = rememberSwipeableState(initialValue = SwipingStates.Expanded)
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
val heightInPx = with(LocalDensity.current) { maxHeight.toPx() }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
return if (delta < 0) {
swipingState.performDrag(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
return swipingState.performDrag(delta).toOffset()
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
swipingState.performFling(available.y)
return super.onPostFling(consumed, available)
}
private fun Float.toOffset() = Offset(0f, this)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.swipeable(
state = swipingState,
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Vertical,
anchors = mapOf(
0f to SwipingStates.Collapsed,
heightInPx to SwipingStates.Expanded
)
)
.nestedScroll(nestedScrollConnection)
) {
val computedProgress by remember {
derivedStateOf {
if (swipingState.progress.to == SwipingStates.Collapsed) {
swipingState.progress.fraction
} else {
1f - swipingState.progress.fraction
}
}
}
val startHeightNum = 264.dp
MotionLayout(
modifier = Modifier.fillMaxSize(),
start = ConstraintSet {
val header = createRefFor("header")
val column = createRefFor("body")
constrain(header) {
this.width = Dimension.matchParent
this.height = Dimension.value(startHeightNum)
}
constrain(column) {
this.width = Dimension.matchParent
this.height = Dimension.fillToConstraints
this.top.linkTo(header.bottom, 0.dp)
this.bottom.linkTo(parent.bottom, 0.dp)
}
},
end = ConstraintSet {
val header = createRefFor("header")
val column = createRefFor("body")
constrain(header) {
this.height = Dimension.value(115.dp)
}
constrain(column) {
this.width = Dimension.matchParent
this.height = Dimension.fillToConstraints
this.top.linkTo(header.bottom, 0.dp)
this.bottom.linkTo(parent.bottom, 0.dp)
}
},
progress = computedProgress,
) {
Box(
modifier = Modifier
.layoutId("body")
.fillMaxWidth()
.background(bgColor)
) {
Column(
modifier = Modifier.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
// list items
}
}
Box(
modifier = Modifier
.layoutId("header")
.fillMaxWidth()
.height(startHeightNum)
) {
// header item
}
}
}
}
}