From 924fe94a8d99af26d42bd9bd7ead430210abd2d7 Mon Sep 17 00:00:00 2001 From: Flaringapp Date: Sat, 4 Apr 2026 00:02:05 +0300 Subject: [PATCH 1/4] Implement align modifier for collapsing column --- .../api/ComposeCollapsingTopBar.klib.api | 1 + .../nestedcollapse/CollapsingTopBarColumn.kt | 48 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api b/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api index f6ff324..ddcb179 100644 --- a/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api +++ b/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api @@ -19,6 +19,7 @@ abstract interface <#A: kotlin/Any?> com.flaringapp.compose.topbar.nestedscroll/ } abstract interface com.flaringapp.compose.topbar.nestedcollapse/CollapsingTopBarColumnScope { // com.flaringapp.compose.topbar.nestedcollapse/CollapsingTopBarColumnScope|null[0] + abstract fun (androidx.compose.ui/Modifier).align(androidx.compose.ui/Alignment.Horizontal): androidx.compose.ui/Modifier // com.flaringapp.compose.topbar.nestedcollapse/CollapsingTopBarColumnScope.align|align@androidx.compose.ui.Modifier(androidx.compose.ui.Alignment.Horizontal){}[0] abstract fun (androidx.compose.ui/Modifier).clipToCollapse(): androidx.compose.ui/Modifier // com.flaringapp.compose.topbar.nestedcollapse/CollapsingTopBarColumnScope.clipToCollapse|clipToCollapse@androidx.compose.ui.Modifier(){}[0] abstract fun (androidx.compose.ui/Modifier).columnProgress(com.flaringapp.compose.topbar/CollapsingTopBarProgressListener): androidx.compose.ui/Modifier // com.flaringapp.compose.topbar.nestedcollapse/CollapsingTopBarColumnScope.columnProgress|columnProgress@androidx.compose.ui.Modifier(com.flaringapp.compose.topbar.CollapsingTopBarProgressListener){}[0] abstract fun (androidx.compose.ui/Modifier).notCollapsible(): androidx.compose.ui/Modifier // com.flaringapp.compose.topbar.nestedcollapse/CollapsingTopBarColumnScope.notCollapsible|notCollapsible@androidx.compose.ui.Modifier(){}[0] diff --git a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt index a6fbb1e..6a8bdc9 100644 --- a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt +++ b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.toRect @@ -143,6 +144,8 @@ private class CollapsingTopBarColumnMeasurePolicy( with(placer) { place( placeables = placeables, + layoutDirection = layoutDirection, + parentWidth = width, topBarHeight = topBarHeightState, totalHeight = totalHeight, collapsibleHeight = collapsibleHeight, @@ -155,6 +158,8 @@ private class CollapsingTopBarColumnMeasurePolicy( fun Placeable.PlacementScope.place( placeables: List, + layoutDirection: LayoutDirection, + parentWidth: Int, topBarHeight: Float, totalHeight: Int, collapsibleHeight: Int, @@ -165,6 +170,8 @@ private class CollapsingTopBarColumnMeasurePolicy( override fun Placeable.PlacementScope.place( placeables: List, + layoutDirection: LayoutDirection, + parentWidth: Int, topBarHeight: Float, totalHeight: Int, collapsibleHeight: Int, @@ -184,6 +191,11 @@ private class CollapsingTopBarColumnMeasurePolicy( placementOffset = itemOffset val parentData = placeable.columnParentData + val xPosition = (parentData?.alignment ?: Alignment.Start).align( + size = placeable.width, + space = parentWidth, + layoutDirection = layoutDirection, + ) // Pinned element if (parentData?.isNotCollapsible == true) { @@ -193,7 +205,7 @@ private class CollapsingTopBarColumnMeasurePolicy( totalProgress = expandFraction, itemProgress = 1f, ) - placeable.place(0, itemOffset - unhandledCollapseOffset, 1f) + placeable.place(xPosition, itemOffset - unhandledCollapseOffset, 1f) return@forEach } @@ -209,7 +221,7 @@ private class CollapsingTopBarColumnMeasurePolicy( if (parentData?.isNotCollapsible != true) { parentData?.clipToCollapseHeightListener?.invoke(0) } - placeable.place(0, itemOffset) + placeable.place(xPosition, itemOffset) return@forEach } @@ -235,7 +247,7 @@ private class CollapsingTopBarColumnMeasurePolicy( it - unhandledCollapseOffset } - placeable.place(0, itemPositionWithOptionalPin) + placeable.place(xPosition, itemPositionWithOptionalPin) } } } @@ -244,6 +256,8 @@ private class CollapsingTopBarColumnMeasurePolicy( override fun Placeable.PlacementScope.place( placeables: List, + layoutDirection: LayoutDirection, + parentWidth: Int, topBarHeight: Float, totalHeight: Int, collapsibleHeight: Int, @@ -296,8 +310,13 @@ private class CollapsingTopBarColumnMeasurePolicy( for (i in placeables.indices.reversed()) { val placeable = placeables[i] val yPosition = yPositions[i] + val xPosition = (placeable.columnParentData?.alignment ?: Alignment.Start).align( + size = placeable.width, + space = parentWidth, + layoutDirection = layoutDirection, + ) - placeable.place(0, yPosition) + placeable.place(xPosition, yPosition) } } } @@ -315,6 +334,14 @@ public sealed class CollapsingTopBarColumnDirection { @Immutable public interface CollapsingTopBarColumnScope { + /** + * Align the element horizontally within the width of [CollapsingTopBarColumn]. + * Only the last modifier in chain takes effect. + * + * @param alignment the horizontal alignment of the element inside the column. + */ + public fun Modifier.align(alignment: Alignment.Horizontal): Modifier + /** * Registers a progress listener to be notified every time top bar column collapse height * changes. Only the last modifier in chain takes effect. @@ -350,6 +377,10 @@ public interface CollapsingTopBarColumnScope { private object CollapsingTopBarColumnScopeInstance : CollapsingTopBarColumnScope { + override fun Modifier.align(alignment: Alignment.Horizontal): Modifier { + return then(AlignmentModifier(alignment)) + } + override fun Modifier.columnProgress(listener: CollapsingTopBarProgressListener): Modifier { return then(ProgressListenerModifier(listener)) } @@ -367,6 +398,14 @@ private object CollapsingTopBarColumnScopeInstance : CollapsingTopBarColumnScope } } +private class AlignmentModifier( + private val alignment: Alignment.Horizontal, +) : CollapsingTopBarColumnParentDataModifier() { + override fun modifyParentData(parentData: CollapsingTopBarColumnParentData) { + parentData.alignment = alignment + } +} + private class ProgressListenerModifier( private val listener: CollapsingTopBarProgressListener, ) : CollapsingTopBarColumnParentDataModifier() { @@ -455,6 +494,7 @@ private abstract class CollapsingTopBarColumnParentDataModifier : ParentDataModi } private data class CollapsingTopBarColumnParentData( + var alignment: Alignment.Horizontal? = null, var progressListener: CollapsingTopBarProgressListener? = null, var isNotCollapsible: Boolean = false, var pinWhenCollapsed: Boolean = false, From 6487c0d988c1e2201ad826241c3b80fc8e1068df Mon Sep 17 00:00:00 2001 From: Flaringapp Date: Sat, 4 Apr 2026 00:25:13 +0300 Subject: [PATCH 2/4] Rearrange collapsing column modifiers --- .../nestedcollapse/CollapsingTopBarColumn.kt | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt index 6a8bdc9..534b1cd 100644 --- a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt +++ b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/nestedcollapse/CollapsingTopBarColumn.kt @@ -342,22 +342,21 @@ public interface CollapsingTopBarColumnScope { */ public fun Modifier.align(alignment: Alignment.Horizontal): Modifier - /** - * Registers a progress listener to be notified every time top bar column collapse height - * changes. Only the last modifier in chain takes effect. - * - * @param listener the listener that gets notified of every collapse progress update. - * - * @see CollapsingTopBarProgressListener - */ - public fun Modifier.columnProgress(listener: CollapsingTopBarProgressListener): Modifier - /** * Prevent the element from collapsing and make it pin to the bottom of column as it collapses. * The height of all not collapsible elements form a total minimum (collapsed) height of column. */ public fun Modifier.notCollapsible(): Modifier + /** + * Move element further in the collapse direction after it has collapsed, like 'pinning' to the + * bottom of column under the next element. + * + * Has no effect if combined with [notCollapsible], and makes no sense to use in combination + * with [clipToCollapse]. + */ + public fun Modifier.pinWhenCollapsed(): Modifier + /** * Clip element draw area as it collapses so that it does not draw underneath the element above. * @@ -366,13 +365,14 @@ public interface CollapsingTopBarColumnScope { public fun Modifier.clipToCollapse(): Modifier /** - * Move element further in the collapse direction after it has collapsed, like 'pinning' to the - * bottom of column under the next element. + * Registers a progress listener to be notified every time top bar column collapse height + * changes. Only the last modifier in chain takes effect. * - * Has no effect if combined with [notCollapsible], and makes no sense to use in combination - * with [clipToCollapse]. + * @param listener the listener that gets notified of every collapse progress update. + * + * @see CollapsingTopBarProgressListener */ - public fun Modifier.pinWhenCollapsed(): Modifier + public fun Modifier.columnProgress(listener: CollapsingTopBarProgressListener): Modifier } private object CollapsingTopBarColumnScopeInstance : CollapsingTopBarColumnScope { @@ -381,20 +381,20 @@ private object CollapsingTopBarColumnScopeInstance : CollapsingTopBarColumnScope return then(AlignmentModifier(alignment)) } - override fun Modifier.columnProgress(listener: CollapsingTopBarProgressListener): Modifier { - return then(ProgressListenerModifier(listener)) - } - override fun Modifier.notCollapsible(): Modifier { return then(NotCollapsibleModifier()) } + override fun Modifier.pinWhenCollapsed(): Modifier { + return then(PinWhenCollapsedElement) + } + override fun Modifier.clipToCollapse(): Modifier { return then(ClipToCollapseElement) } - override fun Modifier.pinWhenCollapsed(): Modifier { - return then(PinWhenCollapsedElement) + override fun Modifier.columnProgress(listener: CollapsingTopBarProgressListener): Modifier { + return then(ProgressListenerModifier(listener)) } } @@ -406,17 +406,15 @@ private class AlignmentModifier( } } -private class ProgressListenerModifier( - private val listener: CollapsingTopBarProgressListener, -) : CollapsingTopBarColumnParentDataModifier() { +private class NotCollapsibleModifier : CollapsingTopBarColumnParentDataModifier() { override fun modifyParentData(parentData: CollapsingTopBarColumnParentData) { - parentData.progressListener = listener + parentData.isNotCollapsible = true } } -private class NotCollapsibleModifier : CollapsingTopBarColumnParentDataModifier() { +private data object PinWhenCollapsedElement : CollapsingTopBarColumnParentDataModifier() { override fun modifyParentData(parentData: CollapsingTopBarColumnParentData) { - parentData.isNotCollapsible = true + parentData.pinWhenCollapsed = true } } @@ -427,9 +425,11 @@ private data object ClipToCollapseElement : ModifierNodeElement Unit)? = null, + var progressListener: CollapsingTopBarProgressListener? = null, ) private val Placeable.columnParentData: CollapsingTopBarColumnParentData? From 9e6d56f22b6b4036ad05fa4de2d99305c319dc1c Mon Sep 17 00:00:00 2001 From: Flaringapp Date: Sat, 4 Apr 2026 01:06:19 +0300 Subject: [PATCH 3/4] Add column alignment sample --- .../samples/CollapsingTopBarSampleGroups.kt | 2 + .../samples/column/ColumnAlignmentSample.kt | 118 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/column/ColumnAlignmentSample.kt diff --git a/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/CollapsingTopBarSampleGroups.kt b/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/CollapsingTopBarSampleGroups.kt index 54f25b5..9c40f24 100644 --- a/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/CollapsingTopBarSampleGroups.kt +++ b/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/CollapsingTopBarSampleGroups.kt @@ -28,6 +28,7 @@ import com.flaringapp.compose.topbar.sample.shared.ui.samples.basic.CollapsingEx import com.flaringapp.compose.topbar.sample.shared.ui.samples.basic.CollapsingExpandAtTopSample import com.flaringapp.compose.topbar.sample.shared.ui.samples.basic.EnterAlwaysCollapsedSample import com.flaringapp.compose.topbar.sample.shared.ui.samples.column.AlternatelyCollapsibleColumnSample +import com.flaringapp.compose.topbar.sample.shared.ui.samples.column.ColumnAlignmentSample import com.flaringapp.compose.topbar.sample.shared.ui.samples.column.ColumnInStackSample import com.flaringapp.compose.topbar.sample.shared.ui.samples.column.ColumnMovingElementSample import com.flaringapp.compose.topbar.sample.shared.ui.samples.column.ColumnPinnedElementsSample @@ -54,6 +55,7 @@ object CollapsingTopBarSampleGroups { ReverseCollapsibleColumnSample, AlternatelyCollapsibleColumnSample, ColumnInStackSample, + ColumnAlignmentSample, ColumnPinnedElementsSample, ColumnMovingElementSample, ) diff --git a/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/column/ColumnAlignmentSample.kt b/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/column/ColumnAlignmentSample.kt new file mode 100644 index 0000000..775ef03 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/flaringapp/compose/topbar/sample/shared/ui/samples/column/ColumnAlignmentSample.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Flaringapp + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.flaringapp.compose.topbar.sample.shared.ui.samples.column + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flaringapp.compose.topbar.nestedcollapse.CollapsingTopBarColumn +import com.flaringapp.compose.topbar.sample.shared.ui.samples.CollapsingTopBarSample +import com.flaringapp.compose.topbar.sample.shared.ui.samples.common.SampleContent +import com.flaringapp.compose.topbar.sample.shared.ui.samples.common.SampleTopAppBar +import com.flaringapp.compose.topbar.sample.shared.ui.theme.ComposeCollapsingTopBarTheme +import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffold +import com.flaringapp.compose.topbar.scaffold.CollapsingTopBarScaffoldScrollMode + +object ColumnAlignmentSample : CollapsingTopBarSample { + + override val name: String = "Column Alignment" + + @Composable + override fun Content(onBack: () -> Unit) { + ColumnAlignmentSampleContent(onBack = onBack) + } +} + +@Composable +fun ColumnAlignmentSampleContent( + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier.fillMaxSize(), + ) { + CollapsingContent( + onBack = onBack, + ) + } +} + +@Composable +private fun CollapsingContent( + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + CollapsingTopBarScaffold( + modifier = modifier, + scrollMode = CollapsingTopBarScaffoldScrollMode.collapse(expandAlways = false), + topBar = { topBarState -> + CollapsingTopBarColumn(topBarState) { + SampleTopAppBar( + modifier = Modifier.notCollapsible(), + title = "Column Alignment", + onBack = onBack, + containerColor = Color.Transparent, + ) + + AlignmentElement( + modifier = Modifier.align(Alignment.Start), + ) + + AlignmentElement( + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + + AlignmentElement( + modifier = Modifier.align(Alignment.End), + ) + } + }, + body = { + SampleContent() + }, + ) +} + +@Composable +private fun AlignmentElement( + modifier: Modifier = Modifier, +) { + Spacer( + modifier = modifier + .size(width = 64.dp, height = 40.dp) + .background(MaterialTheme.colorScheme.secondaryContainer), + ) +} + +@Preview +@Composable +private fun Preview() { + ComposeCollapsingTopBarTheme { + ColumnAlignmentSampleContent( + onBack = {}, + ) + } +} From 2f90240760d7cb0615f412758635c2cacb19bcd4 Mon Sep 17 00:00:00 2001 From: Flaringapp Date: Sat, 4 Apr 2026 01:14:37 +0300 Subject: [PATCH 4/4] Update readme with new align modifier --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index a7978a3..d14e732 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,40 @@ CollapsingTopBarColumn( See all supported placement customization Modifiers: +
+Align + +#### Align + +```kotlin +Modifier.align(Alignment.Horizontal) +``` + +Aligns an element horizontally within the width of `CollapsingTopBarColumn`. + +```kotlin +CollapsingTopBarScaffold( + scrollMode = CollapsingTopBarScaffoldScrollMode.collapse(expandAlways = false), + topBar = { topBarState -> + CollapsingTopBarColumn(topBarState) { + SampleTopAppBar( + modifier = Modifier.notCollapsible(), + ) + AlignmentElement( + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + }, + body = { + SampleContent() + }, +) +``` + +> In this example the element is aligned to the center of the column. + +
+
Not collapsible