-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
*.iml | ||
.gradle | ||
/local.properties | ||
/.idea/caches | ||
/.idea/libraries | ||
/.idea/modules.xml | ||
/.idea/workspace.xml | ||
/.idea/navEditor.xml | ||
/.idea/assetWizardSettings.xml | ||
.DS_Store | ||
/build | ||
/captures | ||
.externalNativeBuild | ||
.cxx | ||
local.properties |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Composable Table | ||
|
||
An Android Jetpack Compose library that provides a `@Composable` table | ||
with automatic layouts. | ||
|
||
![Demo Video](media/composable-table-demo.gif) | ||
|
||
## Features | ||
- 2-dimension scrolling | ||
- Automatic cell width and height calculation using the largest one | ||
- Maximum cell width/height could be optionally specified | ||
- Allowing custom composable and action listeners (e.g. clickable) for each cell | ||
- Infinite table width and height | ||
- Straight-forward to use | ||
|
||
## What is NOT included | ||
- Lazy cells | ||
- Grid lines (have to be implemented by users themselves inside cells) | ||
|
||
## Setup | ||
|
||
1. Add the JitPack maven repository. | ||
```kotlin | ||
repositories { | ||
// ... | ||
maven(url = "https://jitpack.io") | ||
} | ||
``` | ||
|
||
2. Add this library as a dependency. | ||
```kotlin | ||
dependencies { | ||
// ... | ||
implementation("com.github.sunny-chung:composable-table:1.0.0") | ||
} | ||
``` | ||
|
||
## Usage | ||
Below shows an example of minimal usage. | ||
```kotlin | ||
Table( | ||
columnCount = 10, | ||
rowCount = 100 | ||
) { columnIndex, rowIndex -> | ||
Text("($rowIndex, $columnIndex)") | ||
} | ||
``` | ||
|
||
Please read the [demo app](demo-app/src/main/java/com/sunnychung/lib/android/composabletable/ux/AppView.kt) for a practical usage example. | ||
|
||
## Troubleshooting | ||
|
||
If `maxCellWidthDp` or `maxCellHeightDp` is specified, please do not use `fillMaxXXX` modifiers | ||
in the root composable of cells, or the cell would occupy maximum size unconditionally. | ||
This behaviour is [documented](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#(androidx.compose.ui.Modifier).fillMaxWidth(kotlin.Float)) in the `fillMaxXXX` methods of the Compose Foundation library. | ||
|
||
## Contribution | ||
Contributions are welcomed! | ||
|
||
Please also raise an issue if you think a must-have feature is missing! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// Top-level build file where you can add configuration options common to all sub-projects/modules. | ||
plugins { | ||
val androidGradlePluginVersion = "8.1.0" | ||
val kotlinVersion = "1.8.21" | ||
|
||
id("com.android.application") version androidGradlePluginVersion apply false | ||
id("com.android.library") version androidGradlePluginVersion apply false | ||
kotlin("android") version kotlinVersion apply false | ||
kotlin("plugin.serialization") version kotlinVersion apply false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
plugins { | ||
id("com.android.library") | ||
kotlin("android") | ||
id("maven-publish") | ||
} | ||
|
||
android { | ||
namespace = "com.sunnychung.lib.android.composabletable" | ||
compileSdk = 34 | ||
|
||
defaultConfig { | ||
minSdk = 23 | ||
} | ||
buildTypes { | ||
release { | ||
isMinifyEnabled = false | ||
} | ||
} | ||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 | ||
} | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
buildFeatures { | ||
compose = true | ||
} | ||
composeOptions { | ||
kotlinCompilerExtensionVersion = "1.4.7" | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation("androidx.compose.ui:ui:${libs.versions.jetpack.compose.get()}") | ||
implementation("androidx.compose.foundation:foundation:${libs.versions.jetpack.compose.get()}") | ||
} | ||
|
||
publishing { | ||
publications { | ||
create<MavenPublication>("maven") { | ||
groupId = "com.github.sunny-chung" | ||
artifactId = "composable-table" | ||
version = "1.0.0" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package com.sunnychung.lib.android.composabletable.ux | ||
|
||
import androidx.compose.foundation.ScrollState | ||
import androidx.compose.foundation.horizontalScroll | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.rememberScrollState | ||
import androidx.compose.foundation.verticalScroll | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.mutableStateMapOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.layout.Layout | ||
import androidx.compose.ui.platform.LocalDensity | ||
import androidx.compose.ui.unit.Constraints | ||
import androidx.compose.ui.unit.Dp | ||
|
||
@Composable | ||
fun Table( | ||
modifier: Modifier = Modifier, | ||
columnCount: Int, | ||
rowCount: Int, | ||
maxCellWidthDp: Dp = Dp.Infinity, | ||
maxCellHeightDp: Dp = Dp.Infinity, | ||
verticalScrollState: ScrollState = rememberScrollState(), | ||
horizontalScrollState: ScrollState = rememberScrollState(), | ||
cellContent: @Composable (columnIndex: Int, rowIndex: Int) -> Unit | ||
) { | ||
val columnWidths = remember { mutableStateMapOf<Int, Int>() } | ||
val rowHeights = remember { mutableStateMapOf<Int, Int>() } | ||
|
||
val maxCellWidth = if (listOf(Dp.Infinity, Dp.Unspecified).contains(maxCellWidthDp)) { | ||
Constraints.Infinity | ||
} else { | ||
with(LocalDensity.current) { maxCellWidthDp.toPx() }.toInt() | ||
} | ||
val maxCellHeight = if (listOf(Dp.Infinity, Dp.Unspecified).contains(maxCellHeightDp)) { | ||
Constraints.Infinity | ||
} else { | ||
with(LocalDensity.current) { maxCellHeightDp.toPx() }.toInt() | ||
} | ||
|
||
Box( | ||
modifier = modifier | ||
.then(Modifier.horizontalScroll(horizontalScrollState)) | ||
.then(Modifier.verticalScroll(verticalScrollState)) | ||
) { | ||
Layout( | ||
content = { | ||
|
||
(0 until rowCount).forEach { rowIndex -> | ||
(0 until columnCount).forEach { columnIndex -> | ||
cellContent(columnIndex, rowIndex) | ||
} | ||
} | ||
}, | ||
) { measurables, constraints -> | ||
val placeables = measurables.mapIndexed { index, it -> | ||
val columnIndex = index % columnCount | ||
val rowIndex = index / columnCount | ||
it.measure(Constraints( | ||
minWidth = columnWidths[columnIndex] ?: 0, | ||
minHeight = rowHeights[rowIndex] ?: 0, | ||
maxWidth = maxCellWidth, | ||
maxHeight = maxCellHeight | ||
)) | ||
} | ||
|
||
placeables.forEachIndexed { index, placeable -> | ||
val columnIndex = index % columnCount | ||
val rowIndex = index / columnCount | ||
|
||
val existingWidth = columnWidths[columnIndex] ?: 0 | ||
val maxWidth = maxOf(existingWidth, placeable.width) | ||
if (maxWidth > existingWidth) { | ||
columnWidths[columnIndex] = maxWidth | ||
} | ||
|
||
val existingHeight = rowHeights[rowIndex] ?: 0 | ||
val maxHeight = maxOf(existingHeight, placeable.height) | ||
if (maxHeight > existingHeight) { | ||
rowHeights[rowIndex] = maxHeight | ||
} | ||
} | ||
|
||
val accumWidths = mutableListOf(0) | ||
(1..columnWidths.size).forEach { i -> | ||
accumWidths += accumWidths.last() + columnWidths[i-1]!! | ||
} | ||
val accumHeights = mutableListOf(0) | ||
(1..rowHeights.size).forEach { i -> | ||
accumHeights += accumHeights.last() + rowHeights[i-1]!! | ||
} | ||
|
||
val totalWidth = accumWidths.last() | ||
val totalHeight = accumHeights.last() | ||
|
||
layout(width = totalWidth, height = totalHeight) { | ||
placeables.forEachIndexed { index, placeable -> | ||
val columnIndex = index % columnCount | ||
val rowIndex = index / columnCount | ||
|
||
placeable.placeRelative(accumWidths[columnIndex], accumHeights[rowIndex]) | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
plugins { | ||
id("com.android.application") | ||
kotlin("android") | ||
kotlin("plugin.serialization") | ||
} | ||
|
||
android { | ||
namespace = "com.sunnychung.lib.android.composabletable" | ||
compileSdk = 34 | ||
|
||
defaultConfig { | ||
applicationId = "com.sunnychung.lib.android.composabletable" | ||
minSdk = 23 | ||
targetSdk = 34 | ||
versionCode = 1 | ||
} | ||
buildTypes { | ||
release { | ||
isMinifyEnabled = false | ||
proguardFiles( | ||
getDefaultProguardFile("proguard-android-optimize.txt"), | ||
"proguard-rules.pro" | ||
) | ||
} | ||
} | ||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 | ||
} | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
buildFeatures { | ||
compose = true | ||
} | ||
composeOptions { | ||
kotlinCompilerExtensionVersion = "1.4.7" | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation(project(":composable-table")) | ||
implementation("androidx.compose.ui:ui:${libs.versions.jetpack.compose.get()}") | ||
implementation("androidx.compose.foundation:foundation:${libs.versions.jetpack.compose.get()}") | ||
implementation("androidx.compose.material:material:${libs.versions.jetpack.compose.get()}") | ||
implementation("androidx.activity:activity-compose:1.7.2") | ||
|
||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") | ||
api("io.ktor:ktor-serialization-kotlinx-json:2.2.4") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |