diff --git a/README.md b/README.md
index e112b23..0a3a03c 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,14 @@
# Lazy Sticky Headers
-Kotlin Multiplatform library for adding sticky items to lazy lists.
+Compose Multiplatform library for adding sticky headers to lazy lists.
+
+## Preview
+
+
+
+
+
## Getting started
diff --git a/asset/preview_calendar.gif b/asset/preview_calendar.gif
new file mode 100644
index 0000000..5b0a4e2
Binary files /dev/null and b/asset/preview_calendar.gif differ
diff --git a/asset/preview_contacts.gif b/asset/preview_contacts.gif
new file mode 100644
index 0000000..7419e00
Binary files /dev/null and b/asset/preview_contacts.gif differ
diff --git a/demo/composeApp/build.gradle.kts b/demo/composeApp/build.gradle.kts
index 5211095..baa053b 100644
--- a/demo/composeApp/build.gradle.kts
+++ b/demo/composeApp/build.gradle.kts
@@ -106,6 +106,10 @@ android {
getByName("release") {
isMinifyEnabled = false
}
+ create("composeRelease") {
+ signingConfig = signingConfigs.getByName("debug")
+ isMinifyEnabled = false
+ }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/demo/composeApp/src/commonMain/composeResources/drawable/palette.xml b/demo/composeApp/src/commonMain/composeResources/drawable/palette.xml
new file mode 100644
index 0000000..71325ab
--- /dev/null
+++ b/demo/composeApp/src/commonMain/composeResources/drawable/palette.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/App.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/App.kt
index 35dfc65..58574cb 100644
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/App.kt
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/App.kt
@@ -19,37 +19,24 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.DropdownMenu
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
-import me.gingerninja.lazy.sample.list.FINITE_ITEM_COUNT
-import me.gingerninja.lazy.sample.list.calendarList
-import me.gingerninja.lazy.sample.list.sampleList
+import me.gingerninja.lazy.sample.grid.gridScreens
+import me.gingerninja.lazy.sample.home.homeScreen
+import me.gingerninja.lazy.sample.list.listScreens
import me.gingerninja.lazy.sample.ui.theme.LazySampleTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -76,56 +63,54 @@ fun App(onSettingsUpdate: (DemoSettings) -> Unit = {}) {
Theme.DARK -> true
}
- LazySampleTheme(
- darkTheme = darkTheme,
- ) {
- if (showSettings) {
- SettingsDialog(
- settings = settings,
- onUpdate = {
- settings = it
- },
- onDismiss = {
- showSettings = false
+ val layoutDirection = settings.layoutDirection ?: LocalLayoutDirection.current
+
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+ LazySampleTheme(
+ darkTheme = darkTheme,
+ ) {
+ if (showSettings) {
+ SettingsDialog(
+ settings = settings,
+ onUpdate = {
+ settings = it
+ },
+ onDismiss = {
+ showSettings = false
+ },
+ )
+ }
+
+ AppContent(
+ showSettings = {
+ showSettings = true
},
)
}
-
- AppContent(
- settings = settings,
- showSettings = {
- showSettings = true
- },
- )
}
}
@Composable
-private fun AppContent(settings: DemoSettings, showSettings: () -> Unit) {
+private fun AppContent(showSettings: () -> Unit) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
AppNavHost(
modifier = Modifier.fillMaxSize(),
- settings = settings,
showSettings = showSettings,
)
}
}
@Composable
-private fun AppNavHost(
- settings: DemoSettings,
- showSettings: () -> Unit,
- modifier: Modifier = Modifier,
-) {
+private fun AppNavHost(showSettings: () -> Unit, modifier: Modifier = Modifier) {
val navController = rememberNavController()
NavHost(
modifier = modifier,
navController = navController,
- startDestination = Destination.Home.route,
+ startDestination = Home.route,
enterTransition = {
fadeIn() + slideInHorizontally { it / 2 }
},
@@ -145,125 +130,20 @@ private fun AppNavHost(
modifier = Modifier.fillMaxSize(),
)
- sampleList(
+ listScreens(
modifier = Modifier.fillMaxSize(),
+ onScreenClick = { navController.navigate(it.route) },
onBack = {
navController.popBackStack()
},
)
- calendarList(
+ gridScreens(
modifier = Modifier.fillMaxSize(),
+ onScreenClick = { navController.navigate(it.route) },
onBack = {
navController.popBackStack()
},
)
}
}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun SettingsDialog(
- settings: DemoSettings,
- onUpdate: (DemoSettings) -> Unit,
- onDismiss: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- ModalBottomSheet(
- modifier = modifier,
- onDismissRequest = onDismiss,
- ) {
- var themeSelectorOpen by remember { mutableStateOf(false) }
- Column(
- modifier = Modifier.verticalScroll(rememberScrollState()),
- ) {
- ListItem(
- modifier = Modifier.clickable {
- themeSelectorOpen = true
- },
- headlineContent = {
- Text("Theme")
- },
- supportingContent = {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .wrapContentSize(Alignment.TopStart),
- ) {
- Text(settings.theme.text)
-
- DropdownMenu(
- expanded = themeSelectorOpen,
- onDismissRequest = {
- themeSelectorOpen = false
- },
- ) {
- Theme.entries.forEach {
- DropdownMenuItem(
- text = {
- Text(text = it.text)
- },
- onClick = {
- onUpdate(settings.copy(theme = it))
- themeSelectorOpen = false
- },
- )
- }
- }
- }
- },
- )
- ListItem(
- modifier = Modifier.clickable {
- onUpdate(settings.copy(isVertical = !settings.isVertical))
- },
- headlineContent = {
- Text("Vertical layout")
- },
- supportingContent = {
- val countText = if (settings.isVertical) {
- "column"
- } else {
- "row"
- }
-
- Text("Showing items in a $countText")
- },
- trailingContent = {
- Switch(
- checked = settings.isVertical,
- onCheckedChange = {
- onUpdate(settings.copy(isVertical = !settings.isVertical))
- },
- )
- },
- )
-
- ListItem(
- modifier = Modifier.clickable {
- onUpdate(settings.copy(isInfinite = !settings.isInfinite))
- },
- headlineContent = {
- Text("Infinite items")
- },
- supportingContent = {
- val countText = if (settings.isInfinite) {
- "infinite"
- } else {
- FINITE_ITEM_COUNT.toString()
- }
-
- Text("Displaying $countText items")
- },
- trailingContent = {
- Switch(
- checked = settings.isInfinite,
- onCheckedChange = {
- onUpdate(settings.copy(isInfinite = !settings.isInfinite))
- },
- )
- },
- )
- }
- }
-}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/DemoSettings.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/DemoSettings.kt
index cbe55ae..8f252ca 100644
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/DemoSettings.kt
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/DemoSettings.kt
@@ -18,10 +18,12 @@ package me.gingerninja.lazy.sample
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
+import androidx.compose.ui.unit.LayoutDirection
@Immutable
data class DemoSettings(
val theme: Theme = Theme.AUTO,
+ val layoutDirection: LayoutDirection? = null,
val isVertical: Boolean = true,
val isInfinite: Boolean = true,
) {
@@ -31,20 +33,27 @@ data class DemoSettings(
*/
val Saver = run {
val themeKey = "theme"
+ val layoutDirKey = "layoutDir"
val isVerticalKey = "isVertical"
val isInfiniteKey = "isInfinite"
mapSaver(
save = {
mapOf(
- themeKey to it.theme.text,
+ themeKey to it.theme.ordinal,
+ layoutDirKey to it.layoutDirection?.ordinal,
isVerticalKey to it.isVertical,
isInfiniteKey to it.isInfinite,
)
},
restore = {
DemoSettings(
- theme = it[themeKey] as Theme,
+ theme = (it[themeKey] as Int).let { ordinal ->
+ Theme.entries[ordinal]
+ },
+ layoutDirection = (it[layoutDirKey] as? Int)?.let { ordinal ->
+ LayoutDirection.entries[ordinal]
+ },
isVertical = it[isVerticalKey] as Boolean,
isInfinite = it[isInfiniteKey] as Boolean,
)
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/Destination.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/Destination.kt
index 97c3ab2..78ccb25 100644
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/Destination.kt
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/Destination.kt
@@ -15,37 +15,22 @@
*/
package me.gingerninja.lazy.sample
-sealed class Destination(
+import me.gingerninja.lazy.sample.grid.GridDestinations
+import me.gingerninja.lazy.sample.list.ListDestinations
+
+data class Destination(
val route: String,
val title: String,
val description: String? = null,
-) {
- data object Home : Destination(
- route = "home",
- title = "Lazy Sticky Headers",
- )
-
- data object ListVertical : Destination(
- route = "list/vertical",
- title = "LazyColumn",
- description = "Items are placed in a LazyColumn",
- )
-
- data object ListHorizontal : Destination(
- route = "list/horizontal",
- title = "LazyRow",
- description = "Items are placed in a LazyRow",
- )
+ val enabled: Boolean = true,
+)
- data object ListCalendar : Destination(
- route = "list/calendar",
- title = "Calendar",
- description = "Sample calendar schedule view",
- )
-}
+val Home = Destination(
+ route = "home",
+ title = "Lazy Sticky Headers",
+)
val topDestinations = listOf(
- Destination.ListVertical,
- Destination.ListHorizontal,
- Destination.ListCalendar,
+ ListDestinations.root,
+ GridDestinations.root,
)
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/HomeScreen.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/HomeScreen.kt
deleted file mode 100644
index a3c4929..0000000
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/HomeScreen.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2024 Gergely Kőrössy
- *
- * 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 me.gingerninja.lazy.sample
-
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import me.gingerninja.lazy.sample.ui.component.NavCard
-
-fun NavGraphBuilder.homeScreen(
- onSettingsClick: () -> Unit,
- onScreenClick: (destination: Destination) -> Unit,
- modifier: Modifier = Modifier,
-) {
- composable(Destination.Home.route) {
- HomeScreen(
- onSettingsClick = onSettingsClick,
- onScreenClick = onScreenClick,
- modifier = modifier,
- )
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun HomeScreen(
- onSettingsClick: () -> Unit,
- onScreenClick: (destination: Destination) -> Unit,
- modifier: Modifier = Modifier,
-) {
- Scaffold(
- modifier = modifier,
- topBar = {
- CenterAlignedTopAppBar(
- title = {
- Text(Destination.Home.title)
- },
- actions = {
- IconButton(
- onClick = onSettingsClick,
- ) {
- Icon(
- imageVector = Icons.Default.Settings,
- contentDescription = "Settings",
- )
- }
- },
- )
- },
- ) {
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(it),
- ) {
- items(topDestinations) { destination ->
- NavCard(
- title = destination.title,
- description = destination.description,
- onClick = {
- onScreenClick(destination)
- },
- modifier = Modifier
- .fillParentMaxWidth()
- .padding(horizontal = 20.dp, vertical = 10.dp),
- )
- }
- }
- }
-}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/SettingsDialog.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/SettingsDialog.kt
new file mode 100644
index 0000000..5615023
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/SettingsDialog.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.LayoutDirection
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun SettingsDialog(
+ settings: DemoSettings,
+ onUpdate: (DemoSettings) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ModalBottomSheet(
+ modifier = modifier,
+ onDismissRequest = onDismiss,
+ ) {
+ var themeSelectorOpen by remember { mutableStateOf(false) }
+ var layoutDirSelectorOpen by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = Modifier.verticalScroll(rememberScrollState()),
+ ) {
+ ListItem(
+ modifier = Modifier.clickable {
+ themeSelectorOpen = true
+ },
+ headlineContent = {
+ Text("Theme")
+ },
+ supportingContent = {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.TopStart),
+ ) {
+ Text(settings.theme.text)
+
+ DropdownMenu(
+ expanded = themeSelectorOpen,
+ onDismissRequest = {
+ themeSelectorOpen = false
+ },
+ ) {
+ Theme.entries.forEach {
+ DropdownMenuItem(
+ text = {
+ Text(text = it.text)
+ },
+ onClick = {
+ onUpdate(settings.copy(theme = it))
+ themeSelectorOpen = false
+ },
+ )
+ }
+ }
+ }
+ },
+ )
+
+ ListItem(
+ modifier = Modifier.clickable {
+ layoutDirSelectorOpen = true
+ },
+ headlineContent = {
+ Text("Layout direction")
+ },
+ supportingContent = {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .wrapContentSize(Alignment.TopStart),
+ ) {
+ Text(
+ text = when (settings.layoutDirection) {
+ LayoutDirection.Ltr -> "LTR"
+ LayoutDirection.Rtl -> "RTL"
+ null -> "System"
+ },
+ )
+
+ DropdownMenu(
+ expanded = layoutDirSelectorOpen,
+ onDismissRequest = {
+ layoutDirSelectorOpen = false
+ },
+ ) {
+ DropdownMenuItem(
+ text = {
+ Text(text = "System")
+ },
+ onClick = {
+ onUpdate(settings.copy(layoutDirection = null))
+ layoutDirSelectorOpen = false
+ },
+ )
+ DropdownMenuItem(
+ text = {
+ Text(text = "LTR")
+ },
+ onClick = {
+ onUpdate(settings.copy(layoutDirection = LayoutDirection.Ltr))
+ layoutDirSelectorOpen = false
+ },
+ )
+ DropdownMenuItem(
+ text = {
+ Text(text = "RTL")
+ },
+ onClick = {
+ onUpdate(settings.copy(layoutDirection = LayoutDirection.Rtl))
+ layoutDirSelectorOpen = false
+ },
+ )
+ }
+ }
+ },
+ )
+ }
+ }
+}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/grid/GridScreen.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/grid/GridScreen.kt
new file mode 100644
index 0000000..b6ebc7d
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/grid/GridScreen.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample.grid
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigation
+import me.gingerninja.lazy.sample.Destination
+import me.gingerninja.lazy.sample.ui.component.NavScreen
+
+fun NavGraphBuilder.gridScreens(
+ onBack: () -> Unit,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ navigation(
+ startDestination = "grid/root",
+ route = GridDestinations.root.route,
+ ) {
+ composable("grid/root") {
+ GridScreen(
+ onBack = onBack,
+ onScreenClick = onScreenClick,
+ modifier = modifier,
+ )
+ }
+ }
+}
+
+object GridDestinations {
+ val root = Destination(
+ route = "grid",
+ title = "Grids",
+ description = "Coming soon...",
+ // description = "LazyVerticalGrid and LazyHorizontalGrid examples",
+ enabled = false,
+ )
+}
+
+private val destinations = listOf()
+
+@Composable
+private fun GridScreen(
+ onBack: () -> Unit,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ NavScreen(
+ title = GridDestinations.root.title,
+ subtitle = GridDestinations.root.description,
+ destinations = destinations,
+ onScreenClick = onScreenClick,
+ modifier = modifier,
+ onBack = onBack,
+ )
+}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/home/HomeScreen.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/home/HomeScreen.kt
new file mode 100644
index 0000000..6fd417d
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/home/HomeScreen.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample.home
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import me.gingerninja.lazy.composeapp.generated.resources.Res
+import me.gingerninja.lazy.composeapp.generated.resources.palette
+import me.gingerninja.lazy.sample.Destination
+import me.gingerninja.lazy.sample.Home
+import me.gingerninja.lazy.sample.topDestinations
+import me.gingerninja.lazy.sample.ui.component.NavScreen
+import org.jetbrains.compose.resources.vectorResource
+
+fun NavGraphBuilder.homeScreen(
+ onSettingsClick: () -> Unit,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ composable(Home.route) {
+ HomeScreen(
+ onSettingsClick = onSettingsClick,
+ onScreenClick = onScreenClick,
+ modifier = modifier,
+ )
+ }
+}
+
+@Composable
+private fun HomeScreen(
+ onSettingsClick: () -> Unit,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ NavScreen(
+ title = Home.title,
+ destinations = topDestinations,
+ onScreenClick = onScreenClick,
+ modifier = modifier,
+ actions = {
+ IconButton(
+ onClick = onSettingsClick,
+ ) {
+ Icon(
+ imageVector = vectorResource(Res.drawable.palette),
+ contentDescription = "Settings",
+ )
+ }
+ },
+ )
+}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarList.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarList.kt
index 22426c0..5b01446 100644
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarList.kt
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarList.kt
@@ -28,6 +28,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
@@ -41,57 +44,47 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
+import kotlinx.datetime.daysUntil
import kotlinx.datetime.format
import kotlinx.datetime.format.DayOfWeekNames
import kotlinx.datetime.format.MonthNames
import kotlinx.datetime.format.Padding
import kotlinx.datetime.format.char
import kotlinx.datetime.isoDayNumber
-import kotlinx.datetime.minus
import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime
import me.gingerninja.lazy.StickyHeaders
-import me.gingerninja.lazy.sample.DemoSettings
-import me.gingerninja.lazy.sample.Destination
import kotlin.math.absoluteValue
-internal fun NavGraphBuilder.calendarList(onBack: () -> Unit, modifier: Modifier = Modifier) {
- composable(Destination.ListCalendar.route) {
- CalendarListScreen(
- onBack = onBack,
- settings = DemoSettings(),
- modifier = modifier,
- )
- }
-}
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun CalendarListScreen(
- onBack: () -> Unit,
- settings: DemoSettings,
- modifier: Modifier = Modifier,
-) {
+fun CalendarListScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = {
Column {
- Text(Destination.ListCalendar.title)
+ Text("Lazy Sticky Headers")
+ Text(
+ ListDestinations.Calendar.title,
+ style = MaterialTheme.typography.bodyMedium,
+ )
}
},
navigationIcon = {
@@ -104,24 +97,10 @@ private fun CalendarListScreen(
)
}
},
-
- /*actions = {
- IconButton(
- onClick = {
- showSettings()
- }
- ) {
- Icon(
- imageVector = Icons.Default.Settings,
- contentDescription = "Settings"
- )
- }
- }*/
)
},
) {
CalendarList(
- settings = settings,
modifier = Modifier
.fillMaxSize()
.padding(it),
@@ -130,35 +109,145 @@ private fun CalendarListScreen(
}
@Composable
-private fun CalendarList(settings: DemoSettings, modifier: Modifier = Modifier) {
- ScheduleView(
- modifier = modifier,
- isInfinite = settings.isInfinite,
- )
-}
-
-@Composable
-private fun ScheduleView(modifier: Modifier, isInfinite: Boolean) {
- val startIndex = remember(isInfinite) {
- if (isInfinite) Int.MAX_VALUE / 2 else 0
+private fun CalendarList(modifier: Modifier) {
+ val startIndex = remember {
+ Int.MAX_VALUE / 2
}
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = startIndex,
)
+ val horizontalListState = rememberLazyListState(
+ initialFirstVisibleItemIndex = startIndex,
+ )
+
val today = remember {
Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
}
- val startDate = remember(isInfinite, today) {
- if (isInfinite) {
- today
- } else {
- today.minus(today.dayOfMonth - 1, DateTimeUnit.DAY)
+ val startDate = remember(today) {
+ today
+ }
+
+ LaunchedEffect(listState, horizontalListState) {
+ launch {
+ snapshotFlow { listState.firstVisibleItemIndex }
+ .collectLatest {
+ horizontalListState.animateScrollToItem(
+ scheduleIndexToMonthIndex(it, startIndex, startDate),
+ )
+ }
}
}
+ Column(
+ modifier = modifier,
+ ) {
+ MonthView(
+ modifier = Modifier.padding(bottom = 12.dp),
+ listState = horizontalListState,
+ startIndex = startIndex,
+ startDate = startDate,
+ )
+
+ ScheduleView(
+ modifier = Modifier.weight(1f),
+ listState = listState,
+ startIndex = startIndex,
+ startDate = startDate,
+ today = today,
+ )
+ }
+}
+
+@Composable
+private fun MonthView(
+ listState: LazyListState,
+ startIndex: Int,
+ startDate: LocalDate,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ StickyHeaders(
+ modifier = Modifier.fillMaxWidth(),
+ state = listState,
+ key = { item ->
+ val date = startDate.plus(item.index - startIndex, DateTimeUnit.DAY)
+
+ LocalDate(date.year, date.month, 1)
+ },
+ ) {
+ val formatter = LocalDate.Format {
+ monthName(MonthNames.ENGLISH_FULL)
+ chars(" ")
+ year()
+ }
+
+ Text(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 2.dp),
+ text = it.key.format(formatter),
+ style = MaterialTheme.typography.labelSmall,
+ )
+ }
+
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ state = listState,
+ ) {
+ monthViewDayItems(startIndex, startDate)
+ }
+ }
+}
+
+private fun LazyListScope.monthViewDayItems(startIndex: Int, startDate: LocalDate) {
+ items(
+ count = Int.MAX_VALUE,
+ key = { it },
+ ) {
+ val date = startDate.plus(it - startIndex, DateTimeUnit.DAY)
+
+ val formatter = LocalDate.Format {
+ dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED)
+ }
+
+ val dateHeader = formatter.format(date).let { day ->
+ day.firstOrNull()?.toString() ?: day
+ }
+
+ Column(
+ // modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .width(50.dp),
+ // .padding(horizontal = 10.dp, vertical = 10.dp)
+ // .fillParentMaxWidth(1 / 7f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(bottom = 10.dp),
+ text = dateHeader,
+ style = MaterialTheme.typography.labelSmall,
+ )
+
+ Text(
+ modifier = Modifier,
+ text = "${date.dayOfMonth}",
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ScheduleView(
+ listState: LazyListState,
+ startIndex: Int,
+ startDate: LocalDate,
+ today: LocalDate,
+ modifier: Modifier = Modifier,
+) {
fun getDataByIndex(index: Int) = calculateDataByIndex(index, startIndex, startDate)
fun getItemTypeByIndex(index: Int): ScheduleItemType {
@@ -171,14 +260,13 @@ private fun ScheduleView(modifier: Modifier, isInfinite: Boolean) {
state = listState,
) {
items(
- count = if (isInfinite) Int.MAX_VALUE else FINITE_ITEM_COUNT,
+ count = Int.MAX_VALUE,
key = { it },
contentType = ::getItemTypeByIndex,
) {
val data = getDataByIndex(it)
when (data.type) {
- ScheduleItemType.MONTH -> TODO()
ScheduleItemType.WEEK -> {
ScheduleWeek(
startDate = data.date,
@@ -207,7 +295,7 @@ private fun ScheduleView(modifier: Modifier, isInfinite: Boolean) {
.padding(start = 20.dp)
.fillMaxHeight(),
state = listState,
- stickyKeyFactory = { item ->
+ key = { item ->
val itemKey = getDataByIndex(item.index)
val itemType = item.contentType as ScheduleItemType
@@ -232,9 +320,9 @@ private fun ScheduleView(modifier: Modifier, isInfinite: Boolean) {
dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED)
}
Text(
- text = it.format(formatter),
+ text = it.key.format(formatter),
style = MaterialTheme.typography.labelSmall,
- color = if (today == it) {
+ color = if (today == it.key) {
MaterialTheme.colorScheme.secondary
} else {
Color.Unspecified
@@ -244,7 +332,7 @@ private fun ScheduleView(modifier: Modifier, isInfinite: Boolean) {
modifier = Modifier
.aspectRatio(1f)
.run {
- if (today == it) {
+ if (today == it.key) {
background(
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape,
@@ -258,8 +346,8 @@ private fun ScheduleView(modifier: Modifier, isInfinite: Boolean) {
Text(
modifier = Modifier.padding(bottom = 2.dp),
textAlign = TextAlign.Center,
- text = "${it.dayOfMonth}",
- color = if (today == it) {
+ text = "${it.key.dayOfMonth}",
+ color = if (today == it.key) {
MaterialTheme.colorScheme.onSecondary
} else {
MaterialTheme.colorScheme.onSurface
@@ -324,10 +412,11 @@ private fun ScheduleItem(title: String, subtitle: String, modifier: Modifier = M
}
}
-private class CalculatedDate(
- val date: LocalDate,
- val type: ScheduleItemType,
-)
+private fun scheduleIndexToMonthIndex(index: Int, startIndex: Int, startDate: LocalDate): Int {
+ val original = calculateDataByIndex(index, startIndex, startDate)
+
+ return startIndex + startDate.daysUntil(original.date)
+}
private fun calculateDataByIndex(
index: Int,
@@ -409,8 +498,12 @@ private object DateFormatters {
}
}
+private class CalculatedDate(
+ val date: LocalDate,
+ val type: ScheduleItemType,
+)
+
private enum class ScheduleItemType {
- MONTH,
WEEK,
ITEM,
}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarRowList.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarRowList.kt
new file mode 100644
index 0000000..09500a3
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/CalendarRowList.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample.list
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.datetime.Clock
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.format
+import kotlinx.datetime.format.DayOfWeekNames
+import kotlinx.datetime.format.MonthNames
+import kotlinx.datetime.minus
+import kotlinx.datetime.plus
+import kotlinx.datetime.toLocalDateTime
+import me.gingerninja.lazy.StickyHeaders
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CalendarRowListScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {
+ Column {
+ Text("Lazy Sticky Headers")
+ Text("Calendar row", style = MaterialTheme.typography.bodyMedium)
+ }
+ },
+ navigationIcon = {
+ IconButton(
+ onClick = onBack,
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = "Back",
+ )
+ }
+ },
+ )
+ },
+ ) {
+ CalendarRowList(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ )
+ }
+}
+
+@Composable
+private fun CalendarRowList(modifier: Modifier = Modifier) {
+ val listState = rememberLazyListState()
+
+ val startDate = remember {
+ val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
+
+ today.minus(today.dayOfMonth - 1, DateTimeUnit.DAY)
+ }
+
+ val formatter = LocalDate.Format {
+ monthName(MonthNames.ENGLISH_FULL)
+ chars(" ")
+ year()
+ }
+
+ Column(
+ modifier = modifier,
+ ) {
+ StickyHeaders(
+ modifier = Modifier
+ .fillMaxWidth(),
+ state = listState,
+ key = { item ->
+ val date = startDate.plus(item.index, DateTimeUnit.DAY)
+
+ LocalDate(date.year, date.month, 1)
+ },
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 2.dp),
+ text = it.key.format(formatter),
+ style = MaterialTheme.typography.labelSmall,
+ )
+ }
+
+ LazyRow(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ state = listState,
+ ) {
+ horizontalListItems(startDate)
+ }
+ }
+}
+
+private fun LazyListScope.horizontalListItems(startDate: LocalDate) {
+ items(
+ count = Int.MAX_VALUE,
+ key = { it },
+ ) {
+ val date = startDate.plus(it, DateTimeUnit.DAY)
+
+ val formatter = LocalDate.Format {
+ dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED)
+ }
+
+ val dateHeader = formatter.format(date).let { day ->
+ day.firstOrNull()?.toString() ?: day
+ }
+
+ Column(
+ modifier = Modifier.width(50.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier.padding(bottom = 10.dp),
+ text = dateHeader,
+ style = MaterialTheme.typography.labelSmall,
+ )
+
+ Text(
+ modifier = Modifier,
+ text = "${date.dayOfMonth}",
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/ContactList.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/ContactList.kt
new file mode 100644
index 0000000..981e7ef
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/ContactList.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample.list
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import me.gingerninja.lazy.StickyHeaders
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ContactListScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = {
+ Column {
+ Text("Lazy Sticky Headers")
+ Text("Contacts", style = MaterialTheme.typography.bodyMedium)
+ }
+ },
+ navigationIcon = {
+ IconButton(
+ onClick = onBack,
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = "Back",
+ )
+ }
+ },
+ )
+ },
+ ) {
+ ContactList(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ )
+ }
+}
+
+@Composable
+private fun ContactList(modifier: Modifier = Modifier) {
+ val listState = rememberLazyListState()
+
+ Box(modifier = modifier) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ state = listState,
+ ) {
+ items(
+ items = names,
+ key = { it },
+ ) {
+ Box(
+ modifier = Modifier
+ .padding(
+ start = 20.dp,
+ end = 20.dp,
+ top = 10.dp,
+ bottom = 10.dp,
+ )
+ .padding(start = 50.dp)
+ .fillMaxWidth(),
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ ) {
+ Text(text = it, style = MaterialTheme.typography.bodyLarge)
+ }
+ }
+ }
+ }
+
+ StickyHeaders(
+ modifier = Modifier
+ .padding(start = 10.dp)
+ .fillMaxHeight(),
+ state = listState,
+ key = { item ->
+ names[item.index].first()
+ },
+ ) {
+ Box(
+ modifier = Modifier
+ .padding(vertical = 10.dp)
+ .width(50.dp)
+ // .padding(vertical = 20.dp)
+ // .border(1.dp, Color.Gray, MaterialTheme.shapes.medium)
+ .padding(horizontal = 10.dp, vertical = 10.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ "${it.key}",
+ style = MaterialTheme.typography.headlineMedium,
+ color = MaterialTheme.colorScheme.tertiary,
+ )
+ }
+ }
+
+ /*LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ state = listState
+ ) {
+ verticalListItems(startDate, settings)
+ }*/
+ }
+}
+
+private val names = listOf(
+ "Aaryan Blake",
+ "Abby Blackburn",
+ "Adil Stanley",
+ "Aimee Salazar",
+ "Aishah Escobar",
+ "Aiza Bonner",
+ "Alexa Mcmahon",
+ "Alison Franklin",
+ "Alissa Yates",
+ "Amanda Potts",
+ "Ameer Zimmerman",
+ "Andre Russell",
+ "Angel Mcdaniel",
+ "Antonia Frederick",
+ "Antony Villarreal",
+ "Ariana Joyce",
+ "Athena Fields",
+ "Austin Gutierrez",
+ "Autumn Mclaughlin",
+ "Ayla Mejia",
+ "Aysha Blevins",
+ "Bailey Park",
+ "Bartosz Lambert",
+ "Beatrix Huffman",
+ "Beth Meyers",
+ "Bethany Guerra",
+ "Betsy Jensen",
+ "Brooke Rivers",
+ "Brooklyn Gilbert",
+ "Bruce Downs",
+ "Callie Pierce",
+ "Candice Petty",
+ "Carl Stevenson",
+ "Carol Byrd",
+ "Caroline Whitney",
+ "Catherine Reyes",
+ "Chad Lozano",
+ "Christian Yoder",
+ "Clementine Mccann",
+ "Connor Clarke",
+ "Connor Estrada",
+ "Daisy Mayer",
+ "Dalton Shaw",
+ "Daniel Osborne",
+ "Daniela Graham",
+ "Daniela Mata",
+ "Darcey Gregory",
+ "Darren Nixon",
+ "Deborah Frye",
+ "Delores Casey",
+ "Demi-Leigh Vega",
+ "Eddie Baxter",
+ "Edmund Ware",
+ "Eesa Simpson",
+ "Elena Mcgowan",
+ "Eleni Howe",
+ "Eliot Cortez",
+ "Eliza Pugh",
+ "Elsa Gates",
+ "Elspeth Moss",
+ "Emre Parsons",
+ "Esha Acosta",
+ "Evie Porter",
+ "Fabian Wallace",
+ "Fahad Reese",
+ "Faiza Hubbard",
+ "Farhan Proctor",
+ "Farhan Rodgers",
+ "Fatimah Doherty",
+ "Felix Myers",
+ "Findlay Bowen",
+ "Fletcher Robles",
+ "Floyd Ortega",
+ "Franciszek Garza",
+ "Fred Ali",
+ "Freyja Holden",
+ "Garfield Torres",
+ "Gavin Jenkins",
+ "Gerald Crane",
+ "Haaris Austin",
+ "Habiba Lloyd",
+ "Hafsa Gilmore",
+ "Hari O'Reilly",
+ "Hasan Gonzalez",
+ "Hashim Bullock",
+ "Hayden Obrien",
+ "Hayley Olsen",
+ "Henrietta Hurst",
+ "Hollie Mercado",
+ "Hussain Blankenship",
+ "Ida Sandoval",
+ "India Mcdonald",
+ "Ines Rasmussen",
+ "Ishaan Perkins",
+ "Jackson Mccann",
+ "Jade Blackwell",
+ "Jan Hunter",
+ "Jan Walters",
+ "Jared Malone",
+ "Jaya Gallegos",
+ "Jennie Lang",
+ "Joan Price",
+ "Joanna Morrow",
+ "Johnathan Ferguson",
+ "Johnny Gallagher",
+ "Jonathan Knowles",
+ "Jude Cameron",
+ "Judy Espinoza",
+ "Karol Horne",
+ "Katie Nixon",
+ "Keiran Cordova",
+ "Kenneth Lamb",
+ "Keyaan Holt",
+ "Kieran Velez",
+ "Kimberly Hickman",
+ "Kirsten Connor",
+ "Kitty Everett",
+ "Kobi Finley",
+ "Kye Norton",
+ "Lena Nielsen",
+ "Leslie Stanley",
+ "Lorcan David",
+ "Lydia Lynch",
+ "Lyra David",
+ "Mahdi Lynch",
+ "Marco Sullivan",
+ "Maria Landry",
+ "Mariya Brady",
+ "Markus Herman",
+ "Markus Watts",
+ "Martin Macias",
+ "Mason Horne",
+ "Matilda Bowman",
+ "Maximilian Buchanan",
+ "May Mcintyre",
+ "Miah Winters",
+ "Micheal Stephens",
+ "Mohamad Mckay",
+ "Mohamed Le",
+ "Muhammed Escobar",
+ "Murray Ashley",
+ "Nadine Webster",
+ "Nate Combs",
+ "Nathanael Holder",
+ "Nelson Welch",
+ "Nieve Mahoney",
+ "Nikolas Contreras",
+ "Noel Strickland",
+ "Norman Cruz",
+ "Omari Richard",
+ "Oscar Koch",
+ "Patricia Simon",
+ "Peter Cain",
+ "Rachel Hendrix",
+ "Rafael Kemp",
+ "Rahim Walsh",
+ "Raihan Gray",
+ "Raymond Winters",
+ "Riley Beasley",
+ "Rosalie Mathis",
+ "Rosanna Barry",
+ "Russell Velazquez",
+ "Sadia Duffy",
+ "Sadie Beard",
+ "Safiya Gibbs",
+ "Safiyyah Franco",
+ "Saif Mayer",
+ "Samir Lawson",
+ "Samuel Crosby",
+ "Sana Clements",
+ "Scott Davenport",
+ "Serena Noble",
+ "Shreya Gill",
+ "Sonny Marshall",
+ "Sulaiman Bates",
+ "Sulayman Cain",
+ "Summer Hart",
+ "Tanya Sims",
+ "Tariq Luna",
+ "Terry Bullock",
+ "Tessa Diaz",
+ "Tim Butler",
+ "Tina Lozano",
+ "Tommy Wilcox",
+ "Tommy-Lee Carter",
+ "Tommy-Lee Hutchinson",
+ "Tomos Martin",
+ "Tony Hardin",
+ "Vanessa Snow",
+ "Veronica Hodges",
+ "Victor Kelley",
+ "Wiktor Holmes",
+ "Wilfred Terry",
+ "Willie Hoffman",
+ "Wojciech Ray",
+ "Yasir Lynn",
+ "Yousef Glover",
+ "Zane West",
+ "Zarah Stephenson",
+ "Zaynah Petty",
+)
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/ListScreen.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/ListScreen.kt
new file mode 100644
index 0000000..ea8a7be
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/ListScreen.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navigation
+import me.gingerninja.lazy.sample.Destination
+import me.gingerninja.lazy.sample.ui.component.NavScreen
+
+fun NavGraphBuilder.listScreens(
+ onBack: () -> Unit,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ navigation(
+ startDestination = "list/root",
+ route = ListDestinations.root.route,
+ ) {
+ composable("list/root") {
+ ListScreen(
+ onBack = onBack,
+ onScreenClick = onScreenClick,
+ modifier = modifier,
+ )
+ }
+
+ composable(ListDestinations.Contact.route) {
+ ContactListScreen(
+ modifier = modifier,
+ onBack = onBack,
+ )
+ }
+
+ composable(ListDestinations.CalendarRow.route) {
+ CalendarRowListScreen(
+ modifier = modifier,
+ onBack = onBack,
+ )
+ }
+
+ composable(ListDestinations.Calendar.route) {
+ CalendarListScreen(
+ onBack = onBack,
+ modifier = modifier,
+ )
+ }
+ }
+}
+
+object ListDestinations {
+ val root = Destination(
+ route = "list",
+ title = "Lists",
+ description = "LazyColumn and LazyRow examples",
+ )
+ internal val Contact = Destination(
+ route = "list/contact",
+ title = "Simple LazyColumn",
+ description = "A vertical list showcasing a simple contact list",
+ )
+
+ internal val CalendarRow = Destination(
+ route = "list/calendar-row",
+ title = "Simple LazyRow",
+ description = "A horizontal list showcasing a calendar row",
+ )
+
+ internal val Calendar = Destination(
+ route = "list/calendar",
+ title = "Complex calendar",
+ description = "Sample calendar schedule view",
+ )
+}
+
+private val destinations = listOf(
+ ListDestinations.Contact,
+ ListDestinations.CalendarRow,
+ ListDestinations.Calendar,
+)
+
+@Composable
+private fun ListScreen(
+ onBack: () -> Unit,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ NavScreen(
+ title = ListDestinations.root.title,
+ subtitle = ListDestinations.root.description,
+ destinations = destinations,
+ onScreenClick = onScreenClick,
+ modifier = modifier,
+ onBack = onBack,
+ )
+}
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/SampleList.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/SampleList.kt
deleted file mode 100644
index 2c2ae29..0000000
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/list/SampleList.kt
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- * Copyright 2024 Gergely Kőrössy
- *
- * 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 me.gingerninja.lazy.sample.list
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material3.Card
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import kotlinx.datetime.Clock
-import kotlinx.datetime.DateTimeUnit
-import kotlinx.datetime.DayOfWeek
-import kotlinx.datetime.LocalDate
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.format
-import kotlinx.datetime.format.DayOfWeekNames
-import kotlinx.datetime.format.MonthNames
-import kotlinx.datetime.isoDayNumber
-import kotlinx.datetime.minus
-import kotlinx.datetime.plus
-import kotlinx.datetime.toLocalDateTime
-import me.gingerninja.lazy.StickyHeaders
-import me.gingerninja.lazy.sample.DemoSettings
-import me.gingerninja.lazy.sample.Destination
-
-internal fun NavGraphBuilder.sampleList(onBack: () -> Unit, modifier: Modifier = Modifier) {
- composable(Destination.ListVertical.route) {
- SampleListScreen(
- onBack = onBack,
- settings = DemoSettings(isVertical = true),
- title = Destination.ListVertical.title,
- modifier = modifier,
- )
- }
-
- composable(Destination.ListHorizontal.route) {
- SampleListScreen(
- onBack = onBack,
- settings = DemoSettings(isVertical = false),
- title = Destination.ListHorizontal.title,
- modifier = modifier,
- )
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun SampleListScreen(
- onBack: () -> Unit,
- settings: DemoSettings,
- title: String,
- modifier: Modifier = Modifier,
-) {
- Scaffold(
- modifier = modifier,
- topBar = {
- TopAppBar(
- title = {
- Column {
- Text(title)
- }
- },
- navigationIcon = {
- IconButton(
- onClick = onBack,
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Default.ArrowBack,
- contentDescription = "Back",
- )
- }
- },
-
- /*actions = {
- IconButton(
- onClick = {
- showSettings()
- }
- ) {
- Icon(
- imageVector = Icons.Default.Settings,
- contentDescription = "Settings"
- )
- }
- }*/
- )
- },
- ) {
- SampleList(
- settings = settings,
- modifier = Modifier
- .fillMaxSize()
- .padding(it),
- )
- }
-}
-
-@Composable
-private fun SampleList(settings: DemoSettings, modifier: Modifier = Modifier) {
- val listState = rememberLazyListState()
-
- val startDate = remember {
- val today =
- Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
-
- /*val diff = today.dayOfWeek.isoDayNumber - DayOfWeek.MONDAY.isoDayNumber
-
- today.minus(diff, DateTimeUnit.DAY)*/
- today.minus(today.dayOfMonth - 1, DateTimeUnit.DAY)
- }
-
- val formatter = LocalDate.Format {
- monthName(MonthNames.ENGLISH_ABBREVIATED)
- }
-
- if (settings.isVertical) {
- Box(modifier = modifier) {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- state = listState,
- ) {
- verticalListItems(startDate, settings)
- }
-
- StickyHeaders(
- modifier = Modifier
- .padding(start = 20.dp)
- .fillMaxHeight(),
- state = listState,
- stickyKeyFactory = { item ->
- val date = startDate.plus(item.index, DateTimeUnit.DAY).let {
- it.minus(
- value = it.dayOfWeek.isoDayNumber - DayOfWeek.MONDAY.isoDayNumber,
- unit = DateTimeUnit.DAY,
- )
- }
-
- date
- },
- ) {
- Column(
- modifier = Modifier
- // .fillMaxWidth()
- .width(50.dp)
- .padding(vertical = 10.dp)
- .clip(MaterialTheme.shapes.medium)
- .border(1.dp, Color.Gray, MaterialTheme.shapes.medium)
- .background(MaterialTheme.colorScheme.surface)
- .padding(horizontal = 10.dp, vertical = 4.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- ) {
- val endOfWeek = it.plus(6, DateTimeUnit.DAY)
-
- Text(
- text = it.format(formatter),
- style = MaterialTheme.typography.labelSmall,
- )
- Text("${it.dayOfMonth}")
-
- Box(
- Modifier
- .padding(vertical = 4.dp)
- .size(20.dp, 4.dp)
- .background(
- color = MaterialTheme.colorScheme.tertiary,
- shape = CircleShape,
- ),
- )
-
- if (endOfWeek.month != it.month) {
- Text(
- text = endOfWeek.format(formatter),
- style = MaterialTheme.typography.labelSmall,
- )
- }
- Text("${endOfWeek.dayOfMonth}")
- }
- }
-
- /*LazyColumn(
- modifier = Modifier
- .weight(1f)
- .fillMaxHeight(),
- state = listState
- ) {
- verticalListItems(startDate, settings)
- }*/
- }
- } else {
- Column(
- modifier = modifier,
- ) {
- StickyHeaders(
- modifier = Modifier
- .fillMaxWidth(),
- state = listState,
- stickyKeyFactory = { item ->
- val date = startDate.plus(item.index, DateTimeUnit.DAY)
-
- LocalDate(date.year, date.month, 1)
- },
- ) {
- val formatter = LocalDate.Format {
- monthName(MonthNames.ENGLISH_FULL)
- chars(" ")
- year()
- }
-
- Text(
- modifier = Modifier.padding(horizontal = 10.dp, vertical = 2.dp),
- text = it.format(formatter),
- style = MaterialTheme.typography.labelSmall,
- )
- }
-
- LazyRow(
- modifier = Modifier
- .weight(1f)
- .fillMaxWidth(),
- state = listState,
- ) {
- horizontalListItems(startDate, settings)
- }
- }
- }
-}
-
-private fun LazyListScope.verticalListItems(startDate: LocalDate, settings: DemoSettings) {
- items(
- count = if (settings.isInfinite) Int.MAX_VALUE else FINITE_ITEM_COUNT,
- key = { it },
- ) {
- val date = startDate.plus(it, DateTimeUnit.DAY)
-
- if (date.dayOfMonth % 3 != 0) {
- Card(
- onClick = {},
- modifier = Modifier
- .padding(
- start = 20.dp,
- end = 20.dp,
- top = 10.dp,
- bottom = 10.dp,
- )
- .padding(start = 70.dp)
- .fillMaxWidth(),
- ) {
- Column(
- modifier = Modifier.padding(20.dp),
- ) {
- Text(
- "Day card",
- style = MaterialTheme.typography.titleMedium,
- )
- Text(
- "$date",
- )
- }
- }
- } else {
- Box(
- Modifier
- .fillMaxWidth()
- .background(MaterialTheme.colorScheme.tertiaryContainer)
- .padding(40.dp),
- ) {
- Text("Full-size item at $date")
- }
- }
- }
-}
-
-private fun LazyListScope.horizontalListItems(startDate: LocalDate, settings: DemoSettings) {
- items(
- count = if (settings.isInfinite) Int.MAX_VALUE else FINITE_ITEM_COUNT,
- key = { it },
- ) {
- val date = startDate.plus(it, DateTimeUnit.DAY)
-
- val formatter = LocalDate.Format {
- dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED)
- }
-
- val dateHeader = formatter.format(date).let { day ->
- day.firstOrNull()?.toString() ?: day
- }
-
- Column(
- // modifier = Modifier.fillMaxWidth(),
- modifier = Modifier
- .width(50.dp),
- // .padding(horizontal = 10.dp, vertical = 10.dp)
- // .fillParentMaxWidth(1 / 7f),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- modifier = Modifier.padding(bottom = 10.dp),
- text = dateHeader,
- style = MaterialTheme.typography.labelSmall,
- )
-
- Text(
- modifier = Modifier,
- text = "${date.dayOfMonth}",
- style = MaterialTheme.typography.bodyMedium,
- )
- }
- }
-}
-
-const val FINITE_ITEM_COUNT = 100
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavCard.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavCard.kt
index 190fd02..984f3e6 100644
--- a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavCard.kt
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavCard.kt
@@ -32,9 +32,11 @@ fun NavCard(
description: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
+ enabled: Boolean = true,
) {
Card(
onClick = onClick,
+ enabled = enabled,
colors = CardDefaults.outlinedCardColors(),
border = CardDefaults.outlinedCardBorder(),
modifier = modifier,
diff --git a/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavScreen.kt b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavScreen.kt
new file mode 100644
index 0000000..6639bde
--- /dev/null
+++ b/demo/composeApp/src/commonMain/kotlin/me/gingerninja/lazy/sample/ui/component/NavScreen.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 Gergely Kőrössy
+ *
+ * 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 me.gingerninja.lazy.sample.ui.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import me.gingerninja.lazy.sample.Destination
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun NavScreen(
+ title: String,
+ destinations: List,
+ onScreenClick: (destination: Destination) -> Unit,
+ modifier: Modifier = Modifier,
+ subtitle: String? = null,
+ actions: @Composable RowScope.() -> Unit = {},
+ onBack: (() -> Unit)? = null,
+) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ if (subtitle == null) {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(title)
+ },
+ actions = actions,
+ navigationIcon = {
+ onBack?.let {
+ IconButton(
+ onClick = it,
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = "Back",
+ )
+ }
+ }
+ },
+ )
+ } else {
+ TopAppBar(
+ title = {
+ Column {
+ Text(title)
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ },
+ navigationIcon = {
+ onBack?.let {
+ IconButton(
+ onClick = it,
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = "Back",
+ )
+ }
+ }
+ },
+ )
+ }
+ },
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ ) {
+ items(destinations) { destination ->
+ NavCard(
+ title = destination.title,
+ description = destination.description,
+ enabled = destination.enabled,
+ onClick = {
+ onScreenClick(destination)
+ },
+ modifier = Modifier
+ .fillParentMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 10.dp),
+ )
+ }
+ }
+ }
+}
diff --git a/sticky-headers/api/android/sticky-headers.api b/sticky-headers/api/android/sticky-headers.api
index 278c3cb..9558e78 100644
--- a/sticky-headers/api/android/sticky-headers.api
+++ b/sticky-headers/api/android/sticky-headers.api
@@ -2,3 +2,19 @@ public final class me/gingerninja/lazy/StickyHeadersKt {
public static final fun StickyHeaders (Landroidx/compose/foundation/lazy/LazyListState;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}
+public final class me/gingerninja/lazy/StickyInterval {
+ public static final field $stable I
+ public fun (Ljava/lang/Object;II)V
+ public final fun component1 ()Ljava/lang/Object;
+ public final fun component2 ()I
+ public final fun component3 ()I
+ public final fun copy (Ljava/lang/Object;II)Lme/gingerninja/lazy/StickyInterval;
+ public static synthetic fun copy$default (Lme/gingerninja/lazy/StickyInterval;Ljava/lang/Object;IIILjava/lang/Object;)Lme/gingerninja/lazy/StickyInterval;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getEndIndex ()I
+ public final fun getKey ()Ljava/lang/Object;
+ public final fun getStartIndex ()I
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
diff --git a/sticky-headers/api/desktop/sticky-headers.api b/sticky-headers/api/desktop/sticky-headers.api
index 278c3cb..9558e78 100644
--- a/sticky-headers/api/desktop/sticky-headers.api
+++ b/sticky-headers/api/desktop/sticky-headers.api
@@ -2,3 +2,19 @@ public final class me/gingerninja/lazy/StickyHeadersKt {
public static final fun StickyHeaders (Landroidx/compose/foundation/lazy/LazyListState;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}
+public final class me/gingerninja/lazy/StickyInterval {
+ public static final field $stable I
+ public fun (Ljava/lang/Object;II)V
+ public final fun component1 ()Ljava/lang/Object;
+ public final fun component2 ()I
+ public final fun component3 ()I
+ public final fun copy (Ljava/lang/Object;II)Lme/gingerninja/lazy/StickyInterval;
+ public static synthetic fun copy$default (Lme/gingerninja/lazy/StickyInterval;Ljava/lang/Object;IIILjava/lang/Object;)Lme/gingerninja/lazy/StickyInterval;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getEndIndex ()I
+ public final fun getKey ()Ljava/lang/Object;
+ public final fun getStartIndex ()I
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
diff --git a/sticky-headers/build.gradle.kts b/sticky-headers/build.gradle.kts
index 96b4b64..ef6878d 100644
--- a/sticky-headers/build.gradle.kts
+++ b/sticky-headers/build.gradle.kts
@@ -13,7 +13,7 @@ plugins {
alias(libs.plugins.nexusPlugin)
}
-version = "0.1.0-alpha01"
+version = "0.1.0-alpha02"
kotlin {
jvm(name = "desktop")
diff --git a/sticky-headers/src/commonMain/kotlin/me/gingerninja/lazy/StickyHeaders.kt b/sticky-headers/src/commonMain/kotlin/me/gingerninja/lazy/StickyHeaders.kt
index 24c445c..bc90c3e 100644
--- a/sticky-headers/src/commonMain/kotlin/me/gingerninja/lazy/StickyHeaders.kt
+++ b/sticky-headers/src/commonMain/kotlin/me/gingerninja/lazy/StickyHeaders.kt
@@ -33,10 +33,22 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
+/**
+ * The sticky item interval container. This represents a single sticky item.
+ */
@Immutable
-private data class StickyInterval(
+data class StickyInterval(
+ /**
+ * Key of the interval that was returned by `key` in [StickyHeaders].
+ */
val key: T,
+ /**
+ * Start index of the interval (inclusive).
+ */
val startIndex: Int,
+ /**
+ * End index of the interval (exclusive).
+ */
val endIndex: Int,
)
@@ -49,27 +61,29 @@ private data class StickyInterval(
* [LazyColumn][androidx.compose.foundation.lazy.LazyColumn] or a
* [LazyRow][androidx.compose.foundation.lazy.LazyRow] with [state].
*
- * The items are grouped by the value returned by [stickyKeyFactory]. This grouping only occurs in
+ * The items are grouped by the value returned by [key]. This grouping only occurs in
* a consecutive order, meaning that if the function returns the same value for two non-consecutive
* items, two sticky headers will be created, thus this is generally discouraged.
- * When the [stickyKeyFactory] returns `null`, it acts as a boundary for the sticky items before /
+ * When the [key] returns `null`, it acts as a boundary for the sticky items before /
* after.
*
* @param state the [LazyListState] of the list
- * @param stickyKeyFactory key factory function for the sticky items
+ * @param key key factory function for the sticky items
* @param modifier [Modifier] applied to the container of the sticky items
* @param content sticky item content
*/
@Composable
fun StickyHeaders(
state: LazyListState,
- stickyKeyFactory: (item: LazyListItemInfo) -> T?,
+ key: (item: LazyListItemInfo) -> T?,
+ // contentType: (item: LazyListItemInfo) -> Any? = { null },
modifier: Modifier = Modifier,
- content: @Composable (stickyKey: T) -> Unit,
+ // TODO use the StickyInterval instead of T?
+ content: @Composable (stickyKey: StickyInterval) -> Unit,
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- val keyFactory = rememberUpdatedState(stickyKeyFactory)
+ val keyFactory = rememberUpdatedState(key)
val orientation by remember(state) {
derivedStateOf {
@@ -94,21 +108,21 @@ fun StickyHeaders(
buildList {
items.forEach { item ->
- val key = keyFactory.value(item)
+ val currentKey = keyFactory.value(item)
if (!initKeySet) {
initKeySet = true
- lastKey = key
+ lastKey = currentKey
}
- if (lastKey != key) {
+ if (lastKey != currentKey) {
lastKey?.also {
add(
StickyInterval(it, lastIndex, item.index),
)
}
- lastKey = key
+ lastKey = currentKey
lastIndex = item.index
}
}
@@ -126,8 +140,8 @@ fun StickyHeaders(
Box(
modifier = modifier.clipToBounds(),
) {
- keys.forEach { (key, start, end) ->
- key(key) { // TODO ReusableContentHost { }, see LazyLayoutItemContentFactory
+ keys.forEach { interval ->
+ key(interval.key) { // TODO ReusableContentHost { }, see LazyLayoutItemContentFactory
Box(
modifier = Modifier
.run {
@@ -140,10 +154,11 @@ fun StickyHeaders(
}
}
.graphicsLayer {
- val next =
- state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == end }
- val item =
- state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == start }
+ val next = state.layoutInfo.visibleItemsInfo
+ .firstOrNull { it.index == interval.endIndex }
+
+ val item = state.layoutInfo.visibleItemsInfo
+ .firstOrNull { it.index == interval.startIndex }
val nextOffset = next?.offset ?: Int.MAX_VALUE
@@ -173,7 +188,7 @@ fun StickyHeaders(
}
},
) {
- content(key)
+ content(interval)
}
}
}