Skip to content

Commit

Permalink
add Composable Table
Browse files Browse the repository at this point in the history
  • Loading branch information
sunny-chung committed Sep 2, 2023
1 parent bd28743 commit 5958e52
Show file tree
Hide file tree
Showing 40 changed files with 1,436 additions and 1 deletion.
15 changes: 15 additions & 0 deletions .gitignore
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
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 sunny-chung
Copyright (c) 2023 Sunny Chung

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
60 changes: 60 additions & 0 deletions README.md
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!
10 changes: 10 additions & 0 deletions build.gradle.kts
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
}
1 change: 1 addition & 0 deletions composable-table/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
47 changes: 47 additions & 0 deletions composable-table/build.gradle.kts
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])
}
}
}
}
}
1 change: 1 addition & 0 deletions demo-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
50 changes: 50 additions & 0 deletions demo-app/build.gradle.kts
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")
}
21 changes: 21 additions & 0 deletions demo-app/proguard-rules.pro
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
Loading

0 comments on commit 5958e52

Please sign in to comment.