├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── ksnd
│ │ │ │ └── autosizetable
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── SampleScreen.kt
│ │ └── AndroidManifest.xml
│ └── test
│ │ └── java
│ │ └── ksnd
│ │ └── autosizetable
│ │ └── ExampleUnitTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── autosizetable
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── ksnd
│ │ └── autosizetable
│ │ ├── DragScroll2DState.kt
│ │ └── AutoSizeTable.kt
└── build.gradle.kts
├── jitpack.yml
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .editorconfig
├── Gemfile
├── .idea
└── dictionaries
│ └── dictionary.xml
├── renovate.json5
├── .gitignore
├── settings.gradle.kts
├── Dangerfile
├── LICENSE
├── .circleci
└── config.yml
├── gradle.properties
├── README.md
├── Gemfile.lock
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/autosizetable/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AutoSizeTable
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{kt,kts}]
4 | ktlint_code_style = intellij_idea
5 | ktlint_standard_function-naming = disabled
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kosenda/AutoSizeTable/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/autosizetable/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo_name| "https://github.com/kosenda/AutoSizeTable" }
3 | gem 'danger', '~> 9.5.0'
4 | gem "danger-checkstyle_format"
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/dictionaries/dictionary.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | autosizetable
5 | ksnd
6 | ktlint
7 |
8 |
9 |
--------------------------------------------------------------------------------
/renovate.json5:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | ],
5 | "dependencyDashboard": false,
6 | "timezone": "Asia/Tokyo",
7 | "automergeSchedule": [
8 | "every 4 hour after 00:00 and before 23:59 every day",
9 | ],
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .externalNativeBuild
8 | .cxx
9 | local.properties
10 | *.log
11 | *.apk
12 | output.json
13 | /.idea/*
14 | !/.idea/codeStyles/
15 | !/.idea/dictionaries/
16 | *.jks
17 | *.keystore
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/app/src/main/java/ksnd/autosizetable/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/test/java/ksnd/autosizetable/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable
2 |
3 | import junit.framework.TestCase.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url = uri("https://jitpack.io") }
14 | }
15 | }
16 |
17 | rootProject.name = "AutoSizeTable"
18 | include(":app")
19 | include(":autosizetable")
20 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/ksnd/autosizetable/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import ksnd.autosizetable.ui.theme.AutoSizeTableTheme
7 |
8 | class MainActivity : ComponentActivity() {
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContent {
12 | AutoSizeTableTheme(
13 | darkTheme = false,
14 | ) {
15 | SampleScreen()
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/Dangerfile:
--------------------------------------------------------------------------------
1 | # Sometimes it's a README fix, or something like that - which isn't relevant for
2 | # including in a project's CHANGELOG for example
3 | declared_trivial = github.pr_title.include? "#trivial"
4 |
5 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet
6 | warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]"
7 |
8 | # Warn when there is a big PR
9 | warn("Big PR") if git.lines_of_code > 500
10 |
11 | # Don't let testing shortcuts get into master by accident
12 | fail("fdescribe left in tests") if `grep -r fdescribe specs/ `.length > 1
13 | fail("fit left in tests") if `grep -r fit specs/ `.length > 1
14 |
15 | # Do not show out of range issues, not caused by the current PR
16 | github.dismiss_out_of_range_messages
17 |
18 | checkstyle_format.base_path = Dir.pwd
19 | checkstyle_format.report 'app/build/reports/ktlint/ktlint-result.xml'
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 KSND
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | jobs:
4 | build-and-test:
5 | docker:
6 | - image: cimg/android:2025.10
7 | steps:
8 | - checkout
9 |
10 | - restore_cache:
11 | key: jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }}
12 |
13 | - run: ./gradlew androidDependencies
14 |
15 | - save_cache:
16 | paths:
17 | - ~/.gradle
18 | key: jars-{{ checksum "build.gradle.kts" }}-{{ checksum "app/build.gradle.kts" }}
19 |
20 | - run:
21 | command: sudo gem install bundler:2.4.12
22 |
23 | - run:
24 | command: bundle install --path vendor/bundle
25 |
26 | - run:
27 | name: ktlint-check
28 | command: ./gradlew --continue ktlintCheck
29 |
30 | - run:
31 | name: test
32 | command: ./gradlew testDebug --stacktrace
33 |
34 | - run:
35 | name: run-danger-file
36 | command: bundle exec danger --verbose
37 |
38 | workflows:
39 | test:
40 | jobs:
41 | - build-and-test
42 |
43 | # ■ memo ■
44 | # check command: `circleci config validate`
45 |
--------------------------------------------------------------------------------
/app/src/main/java/ksnd/autosizetable/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography =
11 | Typography(
12 | bodyLarge =
13 | TextStyle(
14 | fontFamily = FontFamily.Default,
15 | fontWeight = FontWeight.Normal,
16 | fontSize = 16.sp,
17 | lineHeight = 24.sp,
18 | letterSpacing = 0.5.sp,
19 | ),
20 | /* Other default text styles to override
21 | titleLarge = TextStyle(
22 | fontFamily = FontFamily.Default,
23 | fontWeight = FontWeight.Normal,
24 | fontSize = 22.sp,
25 | lineHeight = 28.sp,
26 | letterSpacing = 0.sp
27 | ),
28 | labelSmall = TextStyle(
29 | fontFamily = FontFamily.Default,
30 | fontWeight = FontWeight.Medium,
31 | fontSize = 11.sp,
32 | lineHeight = 16.sp,
33 | letterSpacing = 0.5.sp
34 | )
35 | */
36 | )
37 |
--------------------------------------------------------------------------------
/autosizetable/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.library)
3 | alias(libs.plugins.kotlin.android)
4 | id("maven-publish")
5 | }
6 |
7 | android {
8 | namespace = "ksnd.autosizetable"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | minSdk = 24
13 |
14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_17
18 | targetCompatibility = JavaVersion.VERSION_17
19 | }
20 | kotlinOptions {
21 | jvmTarget = JavaVersion.VERSION_17.toString()
22 | }
23 | buildFeatures {
24 | compose = true
25 | }
26 | composeOptions {
27 | kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation(libs.androidx.compose.foundation)
33 | }
34 |
35 | publishing {
36 | publications {
37 | register("release") {
38 | groupId = "ksnd.autosizetable"
39 | artifactId = "autosizetable"
40 | version = libs.versions.autoSizeTable.get()
41 |
42 | afterEvaluate {
43 | from(components["release"])
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | activity = "1.9.3"
3 | androidGradlePlugin = "8.13.0"
4 | androidxCompose = "1.7.6"
5 | androidxComposeMaterial3 = "1.3.1"
6 | junit4 = "4.13.2"
7 | kotlin = "1.9.23"
8 | ktlint = "1.7.1"
9 |
10 | # I'm using it, so no deletions allowed
11 | androidxComposeCompiler = "1.5.13"
12 | autoSizeTable = "1.2.0"
13 |
14 | [libraries]
15 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
16 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxCompose" }
17 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" }
18 | androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" }
19 | androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" }
20 | androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidxCompose" }
21 | junit = { group = "junit", name = "junit", version.ref = "junit4" }
22 |
23 | # Ktlint
24 | ktlint = { group = "com.pinterest.ktlint", name = "ktlint-cli", version.ref = "ktlint" }
25 |
26 | [plugins]
27 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
28 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
29 | android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/kosenda/AutoSizeTable) [](https://renovatebot.com) [](https://jitpack.io/#kosenda/AutoSizeTable)
2 |
3 | # AutoSizeTable
4 |
5 | Jetpack Compose library to easily create tables with each item resized。
6 |
7 |
8 | > [!WARNING]
9 | > **Poor performance**
10 |
11 | ## ▪ Features
12 |
13 | - 📊 **Auto-sized columns and rows** - Automatically adjust each item's size based on content
14 | - 🔄 **2D scrolling** - Support for horizontal and vertical scrolling
15 | - ↗️ **Diagonal scrolling** - Available from v1.2.0, support for simultaneous diagonal scrolling
16 |
17 | ## ▪ Sample
18 |
19 | https://github.com/user-attachments/assets/0fa91f31-424a-46ad-a224-83201041e2a0
20 |
21 | https://github.com/kosenda/AutoSizeTable/blob/dc28402d64bf1f349f3834aae9658b89ab9d4283/app/src/main/java/ksnd/autosizetable/SampleScreen.kt#L31-L120
22 |
23 | ## ▪ How to setup
24 |
25 | ### Step 1. Add the JitPack repository to your build file
26 |
27 | Add it in your root build.gradle at the end of repositories:
28 |
29 | ```
30 | dependencyResolutionManagement {
31 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
32 | repositories {
33 | mavenCentral()
34 | maven { url = uri("https://jitpack.io") }
35 | }
36 | }
37 | ```
38 |
39 | ### Step 2. Add the dependency
40 |
41 | See [release page](https://github.com/kosenda/AutoSizeTable/releases) for ``
42 |
43 | ```
44 | dependencies {
45 | implementation("com.github.kosenda:AutoSizeTable:")
46 | }
47 | ```
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.7)
5 | public_suffix (>= 2.0.2, < 7.0)
6 | base64 (0.2.0)
7 | bigdecimal (3.1.9)
8 | claide (1.1.0)
9 | claide-plugins (0.9.2)
10 | cork
11 | nap
12 | open4 (~> 1.3)
13 | colored2 (3.1.2)
14 | cork (0.3.0)
15 | colored2 (~> 3.1)
16 | danger (9.5.3)
17 | base64 (~> 0.2)
18 | claide (~> 1.0)
19 | claide-plugins (>= 0.9.2)
20 | colored2 (>= 3.1, < 5)
21 | cork (~> 0.1)
22 | faraday (>= 0.9.0, < 3.0)
23 | faraday-http-cache (~> 2.0)
24 | git (>= 1.13, < 3.0)
25 | kramdown (>= 2.5.1, < 3.0)
26 | kramdown-parser-gfm (~> 1.0)
27 | octokit (>= 4.0)
28 | pstore (~> 0.1)
29 | terminal-table (>= 1, < 5)
30 | danger-checkstyle_format (0.1.1)
31 | danger-plugin-api (~> 1.0)
32 | ox (~> 2.0)
33 | danger-plugin-api (1.0.0)
34 | danger (> 2.0)
35 | faraday (2.12.3)
36 | faraday-net_http (>= 2.0, < 3.5)
37 | json
38 | logger
39 | faraday-http-cache (2.5.1)
40 | faraday (>= 0.8)
41 | faraday-net_http (3.4.1)
42 | net-http (>= 0.5.0)
43 | git (1.19.1)
44 | addressable (~> 2.8)
45 | rchardet (~> 1.8)
46 | json (2.9.1)
47 | kramdown (2.5.1)
48 | rexml (>= 3.3.9)
49 | kramdown-parser-gfm (1.1.0)
50 | kramdown (~> 2.0)
51 | logger (1.6.6)
52 | nap (1.1.0)
53 | net-http (0.6.0)
54 | uri
55 | octokit (9.2.0)
56 | faraday (>= 1, < 3)
57 | sawyer (~> 0.9)
58 | open4 (1.3.4)
59 | ox (2.14.19)
60 | bigdecimal (>= 3.0)
61 | pstore (0.1.4)
62 | public_suffix (6.0.2)
63 | rchardet (1.8.0)
64 | rexml (3.4.2)
65 | sawyer (0.9.2)
66 | addressable (>= 2.3.5)
67 | faraday (>= 0.17.3, < 3)
68 | terminal-table (3.0.2)
69 | unicode-display_width (>= 1.1.1, < 3)
70 | unicode-display_width (2.6.0)
71 | uri (1.0.3)
72 |
73 | PLATFORMS
74 | arm64-darwin-22
75 | x86_64-linux
76 |
77 | DEPENDENCIES
78 | danger (~> 9.5.0)
79 | danger-checkstyle_format
80 |
81 | BUNDLED WITH
82 | 2.4.13
83 |
--------------------------------------------------------------------------------
/app/src/main/java/ksnd/autosizetable/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme =
19 | darkColorScheme(
20 | primary = Purple80,
21 | secondary = PurpleGrey80,
22 | tertiary = Pink80,
23 | )
24 |
25 | private val LightColorScheme =
26 | lightColorScheme(
27 | primary = Purple40,
28 | secondary = PurpleGrey40,
29 | tertiary = Pink40,
30 | /* Other default colors to override
31 | background = Color(0xFFFFFBFE),
32 | surface = Color(0xFFFFFBFE),
33 | onPrimary = Color.White,
34 | onSecondary = Color.White,
35 | onTertiary = Color.White,
36 | onBackground = Color(0xFF1C1B1F),
37 | onSurface = Color(0xFF1C1B1F),
38 | */
39 | )
40 |
41 | @Composable
42 | fun AutoSizeTableTheme(
43 | darkTheme: Boolean = isSystemInDarkTheme(),
44 | // Dynamic color is available on Android 12+
45 | dynamicColor: Boolean = true,
46 | content: @Composable () -> Unit,
47 | ) {
48 | val colorScheme =
49 | when {
50 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
51 | val context = LocalContext.current
52 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
53 | }
54 |
55 | darkTheme -> DarkColorScheme
56 | else -> LightColorScheme
57 | }
58 | val view = LocalView.current
59 | if (!view.isInEditMode) {
60 | SideEffect {
61 | val window = (view.context as Activity).window
62 | window.statusBarColor = colorScheme.primary.toArgb()
63 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
64 | }
65 | }
66 |
67 | MaterialTheme(
68 | colorScheme = colorScheme,
69 | typography = Typography,
70 | content = content,
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | val ktlint: Configuration by configurations.creating
2 |
3 | plugins {
4 | alias(libs.plugins.android.application)
5 | alias(libs.plugins.kotlin.android)
6 | }
7 |
8 | android {
9 | namespace = "ksnd.autosizetable"
10 | compileSdk = 34
11 |
12 | defaultConfig {
13 | applicationId = "ksnd.autosizetable"
14 | minSdk = 24
15 | targetSdk = 34
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary = true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | isMinifyEnabled = false
28 | proguardFiles(
29 | getDefaultProguardFile("proguard-android-optimize.txt"),
30 | "proguard-rules.pro"
31 | )
32 | }
33 | }
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_17
36 | targetCompatibility = JavaVersion.VERSION_17
37 | }
38 | kotlinOptions {
39 | jvmTarget = JavaVersion.VERSION_17.toString()
40 | }
41 | buildFeatures {
42 | compose = true
43 | }
44 | composeOptions {
45 | kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
46 | }
47 | packaging {
48 | resources {
49 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
50 | }
51 | }
52 | }
53 |
54 | dependencies {
55 | implementation(project(":autosizetable"))
56 |
57 | implementation(libs.androidx.activity.compose)
58 | implementation(libs.androidx.compose.material3)
59 | implementation(libs.androidx.compose.ui)
60 | implementation(libs.androidx.compose.ui.tooling)
61 | implementation(libs.androidx.compose.ui.tooling.preview)
62 | testImplementation(libs.junit)
63 |
64 | // ktlint
65 | ktlint(libs.ktlint) {
66 | attributes {
67 | attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
68 | }
69 | }
70 | }
71 |
72 | tasks.create("ktlintCheck") {
73 | description = "Check Kotlin code style."
74 | classpath = ktlint
75 | mainClass.set("com.pinterest.ktlint.Main")
76 | args = listOf(
77 | "src/**/*.kt",
78 | "--reporter=checkstyle,output=${layout.buildDirectory.get()}/reports/ktlint/ktlint-result.xml",
79 | )
80 | isIgnoreExitValue = true
81 | }
82 |
83 | tasks.create("ktlintFormatting") {
84 | description = "Fix Kotlin code style deviations."
85 | classpath = ktlint
86 | mainClass.set("com.pinterest.ktlint.Main")
87 | args("-F", "src/**/*.kt")
88 | // https://github.com/pinterest/ktlint/issues/1391#issuecomment-1331954612
89 | jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
90 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 |
74 |
75 | @rem Execute Gradle
76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
77 |
78 | :end
79 | @rem End local scope for the variables with windows NT shell
80 | if %ERRORLEVEL% equ 0 goto mainEnd
81 |
82 | :fail
83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
84 | rem the _cmd.exe /c_ return code!
85 | set EXIT_CODE=%ERRORLEVEL%
86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
88 | exit /b %EXIT_CODE%
89 |
90 | :mainEnd
91 | if "%OS%"=="Windows_NT" endlocal
92 |
93 | :omega
94 |
--------------------------------------------------------------------------------
/app/src/main/java/ksnd/autosizetable/SampleScreen.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable
2 |
3 | import androidx.compose.foundation.horizontalScroll
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.rememberScrollState
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.MailOutline
11 | import androidx.compose.material3.Button
12 | import androidx.compose.material3.Icon
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Surface
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.runtime.mutableIntStateOf
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.runtime.setValue
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.tooling.preview.Preview
24 | import androidx.compose.ui.unit.dp
25 | import ksnd.autosizetable.ui.theme.AutoSizeTableTheme
26 |
27 | private const val NUM_OF_ITEMS_IN_EACH_COLUMN = 20
28 | private const val NUM_OF_ITEMS_IN_EACH_ROW = 30
29 | private const val MAX_NUM_OF_MAIL_ICONS = 5
30 |
31 | @Composable
32 | fun SampleScreen() {
33 | val colorScheme = MaterialTheme.colorScheme
34 | var typeIndex by remember { mutableIntStateOf(0) }
35 | var numMailRowIcons by remember { mutableIntStateOf(1) }
36 | var numMailColumnIcons by remember { mutableIntStateOf(1) }
37 |
38 | // fixedTopSize to fixedStartSize (Row to Column)
39 | val type = listOf(
40 | 1 to 1,
41 | 1 to 0,
42 | 0 to 1,
43 | 0 to 0,
44 | 1 to 2,
45 | 2 to 2,
46 | )
47 |
48 | Surface(
49 | color = MaterialTheme.colorScheme.background,
50 | ) {
51 | Column {
52 | Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
53 | Button(
54 | onClick = { typeIndex = (typeIndex + 1) % type.size },
55 | modifier = Modifier.padding(start = 8.dp, top = 8.dp),
56 | ) {
57 | Text("Switch fixed size")
58 | }
59 |
60 | Button(
61 | onClick = { numMailRowIcons = (numMailRowIcons) % MAX_NUM_OF_MAIL_ICONS + 1 },
62 | modifier = Modifier.padding(start = 8.dp, top = 8.dp),
63 | ) {
64 | Text("Switch num of row mail icons")
65 | }
66 |
67 | Button(
68 | onClick = { numMailColumnIcons = (numMailColumnIcons) % MAX_NUM_OF_MAIL_ICONS + 1 },
69 | modifier = Modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp),
70 | ) {
71 | Text("Switch num of column mail icons")
72 | }
73 | }
74 |
75 | AutoSizeTable(
76 | content = List(NUM_OF_ITEMS_IN_EACH_ROW) { rowIndex ->
77 | List(NUM_OF_ITEMS_IN_EACH_COLUMN) { columnIndex ->
78 | {
79 | if (columnIndex == 0 && rowIndex % 2 == 1) {
80 | Column {
81 | repeat(numMailColumnIcons) {
82 | Row {
83 | repeat(numMailRowIcons) {
84 | Icon(
85 | imageVector = Icons.Default.MailOutline,
86 | contentDescription = null,
87 | modifier = Modifier
88 | .padding(4.dp)
89 | .size(48.dp),
90 | )
91 | }
92 | }
93 | }
94 | }
95 | } else {
96 | Text(
97 | text = "column: $columnIndex\nrow: $rowIndex",
98 | modifier = Modifier.padding(8.dp),
99 | )
100 | }
101 | }
102 | }
103 | },
104 | modifier = Modifier.padding(all = 8.dp),
105 | fixedTopSize = type[typeIndex].first,
106 | fixedStartSize = type[typeIndex].second,
107 | outlineColor = colorScheme.outline,
108 | backgroundColor = { rowIndex, columnIndex ->
109 | when {
110 | rowIndex in 0.. colorScheme.primaryContainer
111 | columnIndex in 0.. colorScheme.tertiaryContainer
112 | rowIndex % 2 == 0 -> colorScheme.surface
113 | else -> colorScheme.inverseOnSurface
114 | }
115 | },
116 | contentAlignment = { _, _ -> Alignment.Center },
117 | )
118 | }
119 | }
120 | }
121 |
122 | @Preview
123 | @Composable
124 | fun PreviewSampleScreen() {
125 | AutoSizeTableTheme {
126 | SampleScreen()
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/autosizetable/src/main/java/ksnd/autosizetable/DragScroll2DState.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable
2 |
3 | import androidx.compose.foundation.ScrollState
4 | import androidx.compose.foundation.rememberScrollState
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableFloatStateOf
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.setValue
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.Job
13 | import kotlinx.coroutines.delay
14 | import kotlinx.coroutines.launch
15 | import kotlin.math.pow
16 | import kotlin.math.sqrt
17 |
18 | /**
19 | * Configuration for 2D drag-based scrolling behavior.
20 | *
21 | * @param animationSteps Number of animation frames for fling. Should be at least 2 to avoid division by zero.
22 | * @param frameDurationMs Duration of each animation frame in milliseconds.
23 | * @param decelerationFactor Deceleration rate during fling animation (0.0 to 1.0).
24 | */
25 | data class DragScroll2DConfig(
26 | val animationSteps: Int = 50,
27 | val frameDurationMs: Long = 12L,
28 | val decelerationFactor: Float = 0.99f,
29 | )
30 |
31 | /**
32 | * 2D drag-based scroll state management class that handles vertical, horizontal, and diagonal scrolling.
33 | *
34 | * This class provides unified management of scroll velocities in both directions and
35 | * applies smooth fling animation with proper physics for diagonal scrolling.
36 | * It detects drag gestures and translates them into smooth 2D scrolling with inertia.
37 | *
38 | * @param horizontalScrollState The scroll state for horizontal scrolling.
39 | * @param verticalScrollState The scroll state for vertical scrolling.
40 | * @param config Configuration for fling animation behavior.
41 | */
42 | class DragScroll2DState(
43 | private val horizontalScrollState: ScrollState,
44 | private val verticalScrollState: ScrollState,
45 | private val config: DragScroll2DConfig = DragScroll2DConfig(),
46 | ) {
47 | // Velocity tracking
48 | private var horizontalVelocity by mutableFloatStateOf(0f)
49 | private var verticalVelocity by mutableFloatStateOf(0f)
50 |
51 | // Fling animation state
52 | private var isFlingActive by mutableStateOf(false)
53 |
54 | // Fling animation job
55 | private var flingJob: Job? = null
56 |
57 | /**
58 | * Called during drag to update velocities and apply scroll.
59 | */
60 | fun onDrag(dragAmountX: Float, dragAmountY: Float) {
61 | horizontalVelocity = dragAmountX
62 | verticalVelocity = dragAmountY
63 |
64 | // Apply scroll (dispatchRawDelta handles boundary checking)
65 | if (dragAmountX != 0f) horizontalScrollState.dispatchRawDelta(-dragAmountX)
66 | if (dragAmountY != 0f) verticalScrollState.dispatchRawDelta(-dragAmountY)
67 | }
68 |
69 | /**
70 | * Called when drag starts.
71 | */
72 | fun onDragStart() {
73 | flingJob?.cancel()
74 | horizontalVelocity = 0f
75 | verticalVelocity = 0f
76 | isFlingActive = false
77 | }
78 |
79 | /**
80 | * Called when drag is cancelled.
81 | */
82 | fun onDragCancel() {
83 | flingJob?.cancel()
84 | horizontalVelocity = 0f
85 | verticalVelocity = 0f
86 | isFlingActive = false
87 | }
88 |
89 | /**
90 | * Apply fling animation with smooth deceleration.
91 | * This handles diagonal scrolling by maintaining velocity ratios.
92 | *
93 | * @param coroutineScope The coroutine scope to launch the fling animation in.
94 | */
95 | fun onDragEnd(coroutineScope: CoroutineScope) {
96 | // Calculate the magnitude of velocity (diagonal velocity)
97 | val velocityMagnitude = sqrt(horizontalVelocity * horizontalVelocity + verticalVelocity * verticalVelocity)
98 |
99 | // If velocity is negligible, don't start fling animation
100 | if (velocityMagnitude < 1f) return
101 |
102 | // Cancel any existing fling animation
103 | flingJob?.cancel()
104 |
105 | isFlingActive = true
106 |
107 | // Normalize velocities to maintain direction during fling
108 | val normalizedHorizontal = if (velocityMagnitude > 0) horizontalVelocity / velocityMagnitude else 0f
109 | val normalizedVertical = if (velocityMagnitude > 0) verticalVelocity / velocityMagnitude else 0f
110 |
111 | // Launch fling animation with smooth exponential deceleration
112 | flingJob = coroutineScope.launch {
113 | repeat(config.animationSteps) { step ->
114 | // Decelerate from initial velocity 100% to 0%
115 | val progress = step.toFloat() / (config.animationSteps - 1)
116 | val currentDecelerationFactor = (1f - progress).pow(2f)
117 |
118 | val currentHorizontalVelocity = normalizedHorizontal * currentDecelerationFactor * velocityMagnitude
119 | val currentVerticalVelocity = normalizedVertical * currentDecelerationFactor * velocityMagnitude
120 |
121 | // Apply scroll for both axes simultaneously (enables diagonal fling)
122 | horizontalScrollState.dispatchRawDelta(-currentHorizontalVelocity)
123 | verticalScrollState.dispatchRawDelta(-currentVerticalVelocity)
124 |
125 | // Wait for next frame
126 | if (step < config.animationSteps - 1) {
127 | delay(config.frameDurationMs)
128 | }
129 | }
130 |
131 | // Animation complete
132 | horizontalVelocity = 0f
133 | verticalVelocity = 0f
134 | isFlingActive = false
135 | }
136 | }
137 | }
138 |
139 | /**
140 | * Create and remember a DragScroll2DState instance.
141 | *
142 | * @param horizontalScrollState The scroll state for horizontal scrolling. If not provided, a new one is created.
143 | * @param verticalScrollState The scroll state for vertical scrolling. If not provided, a new one is created.
144 | * @param config Configuration for fling animation behavior. See [DragScroll2DConfig] for customization options.
145 | * @return A remembered [DragScroll2DState] instance that survives recomposition.
146 | *
147 | * @sample
148 | * ```
149 | * // Use with default configuration
150 | * val dragScroll2DState = rememberDragScroll2DState()
151 | *
152 | * // Use with custom configuration
153 | * val customConfig = DragScroll2DConfig(
154 | * animationSteps = 60
155 | * )
156 | * val dragScroll2DState = rememberDragScroll2DState(config = customConfig)
157 | * ```
158 | */
159 | @Composable
160 | fun rememberDragScroll2DState(
161 | horizontalScrollState: ScrollState = rememberScrollState(),
162 | verticalScrollState: ScrollState = rememberScrollState(),
163 | config: DragScroll2DConfig = DragScroll2DConfig(),
164 | ): DragScroll2DState {
165 | return remember(horizontalScrollState, verticalScrollState, config) {
166 | DragScroll2DState(horizontalScrollState, verticalScrollState, config)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 |
118 |
119 | # Determine the Java command to use to start the JVM.
120 | if [ -n "$JAVA_HOME" ] ; then
121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
122 | # IBM's JDK on AIX uses strange locations for the executables
123 | JAVACMD=$JAVA_HOME/jre/sh/java
124 | else
125 | JAVACMD=$JAVA_HOME/bin/java
126 | fi
127 | if [ ! -x "$JAVACMD" ] ; then
128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
129 |
130 | Please set the JAVA_HOME variable in your environment to match the
131 | location of your Java installation."
132 | fi
133 | else
134 | JAVACMD=java
135 | if ! command -v java >/dev/null 2>&1
136 | then
137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
138 |
139 | Please set the JAVA_HOME variable in your environment to match the
140 | location of your Java installation."
141 | fi
142 | fi
143 |
144 | # Increase the maximum file descriptors if we can.
145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
146 | case $MAX_FD in #(
147 | max*)
148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
149 | # shellcheck disable=SC2039,SC3045
150 | MAX_FD=$( ulimit -H -n ) ||
151 | warn "Could not query maximum file descriptor limit"
152 | esac
153 | case $MAX_FD in #(
154 | '' | soft) :;; #(
155 | *)
156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
157 | # shellcheck disable=SC2039,SC3045
158 | ulimit -n "$MAX_FD" ||
159 | warn "Could not set maximum file descriptor limit to $MAX_FD"
160 | esac
161 | fi
162 |
163 | # Collect all arguments for the java command, stacking in reverse order:
164 | # * args from the command line
165 | # * the main class name
166 | # * -classpath
167 | # * -D...appname settings
168 | # * --module-path (only if needed)
169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
170 |
171 | # For Cygwin or MSYS, switch paths to Windows format before running java
172 | if "$cygwin" || "$msys" ; then
173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------
/autosizetable/src/main/java/ksnd/autosizetable/AutoSizeTable.kt:
--------------------------------------------------------------------------------
1 | package ksnd.autosizetable
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.LocalOverscrollConfiguration
5 | import androidx.compose.foundation.ScrollState
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.gestures.detectDragGestures
8 | import androidx.compose.foundation.horizontalScroll
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.foundation.verticalScroll
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.CompositionLocalProvider
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.runtime.rememberCoroutineScope
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.drawBehind
22 | import androidx.compose.ui.geometry.Offset
23 | import androidx.compose.ui.geometry.Size
24 | import androidx.compose.ui.graphics.Color
25 | import androidx.compose.ui.graphics.drawscope.Stroke
26 | import androidx.compose.ui.input.pointer.pointerInput
27 | import androidx.compose.ui.layout.SubcomposeLayout
28 | import androidx.compose.ui.unit.Constraints
29 | import androidx.compose.ui.unit.Dp
30 | import androidx.compose.ui.unit.dp
31 |
32 | /**
33 | * Display the table with the size of each item automatically adjusted.
34 | *
35 | * This composable creates a table that automatically sizes its cells based on content,
36 | * with support for fixed header rows and columns that remain visible during scrolling.
37 | * Supports 2D dragging with smooth inertia scrolling in any direction (vertical, horizontal, or diagonal).
38 | *
39 | * @param content Items to display in the table. Each inner list represents a row of cells.
40 | * @param modifier Modifier for the table container.
41 | * @param fixedTopSize Number of rows to be fixed at the top (header rows). Default is 1.
42 | * @param fixedStartSize Number of columns to be fixed at the start (header columns). Default is 1.
43 | * @param outlineStroke Stroke style for cell borders. Default is 1dp width.
44 | * @param outlineColor Color of the cell borders. Default is Black.
45 | * @param horizontalScrollState ScrollState for horizontal scroll control.
46 | * @param verticalScrollState ScrollState for vertical scroll control.
47 | * @param dragScroll2DState State manager for 2D drag-based scrolling with inertia. Customize using [DragScroll2DConfig].
48 | * @param backgroundColor Lambda function to determine background color for each cell based on row and column indices.
49 | * @param contentAlignment Lambda function to determine content alignment for each cell based on row and column indices.
50 | *
51 | * @sample
52 | * ```
53 | * // Basic usage with default 2D scrolling
54 | * AutoSizeTable(
55 | * content = listOf(
56 | * listOf({ Text("Header 1") }, { Text("Header 2") }),
57 | * listOf({ Text("Cell 1") }, { Text("Cell 2") })
58 | * ),
59 | * fixedTopSize = 1,
60 | * fixedStartSize = 1
61 | * )
62 | *
63 | * // Advanced usage with custom animation configuration
64 | * val customConfig = DragScroll2DConfig(
65 | * animationSteps = 60,
66 | * frameDurationMs = 16L
67 | * )
68 | * val dragScroll2DState = rememberDragScroll2DState(config = customConfig)
69 | * AutoSizeTable(
70 | * content = tableData,
71 | * dragScroll2DState = dragScroll2DState
72 | * )
73 | * ```
74 | */
75 | @OptIn(ExperimentalFoundationApi::class)
76 | @Composable
77 | fun AutoSizeTable(
78 | content: List Unit>>,
79 | modifier: Modifier = Modifier,
80 | fixedTopSize: Int = 1,
81 | fixedStartSize: Int = 1,
82 | outlineStroke: Stroke = Stroke(width = 1.0f),
83 | outlineColor: Color = Color.Black,
84 | horizontalScrollState: ScrollState = rememberScrollState(),
85 | verticalScrollState: ScrollState = rememberScrollState(),
86 | dragScroll2DState: DragScroll2DState = rememberDragScroll2DState(horizontalScrollState, verticalScrollState),
87 | backgroundColor: (rowIndex: Int, columnIndex: Int) -> Color = { _, _ -> Color.Unspecified },
88 | contentAlignment: (rowIndex: Int, columnIndex: Int) -> Alignment = { _, _ -> Alignment.Center },
89 | ) {
90 | validateTableParameters(content, fixedTopSize, fixedStartSize)
91 |
92 | val isFixedTop = remember(fixedTopSize) { fixedTopSize > 0 }
93 | val isFixedStart = remember(fixedStartSize) { fixedStartSize > 0 }
94 | val coroutineScope = rememberCoroutineScope()
95 |
96 | MeasureTable(
97 | modifier = modifier,
98 | items = content,
99 | ) { tableItemSize ->
100 | val scrollableModifier = if (isFixedTop.not() && isFixedStart.not()) {
101 | Modifier
102 | .horizontalScroll(horizontalScrollState)
103 | .verticalScroll(verticalScrollState)
104 | .pointerInput(Unit) {
105 | detectDragGestures(
106 | onDragStart = { dragScroll2DState.onDragStart() },
107 | onDrag = { change, dragAmount ->
108 | change.consume()
109 | dragScroll2DState.onDrag(dragAmount.x, dragAmount.y)
110 | },
111 | onDragEnd = { dragScroll2DState.onDragEnd(coroutineScope) },
112 | onDragCancel = { dragScroll2DState.onDragCancel() },
113 | )
114 | }
115 | } else {
116 | Modifier
117 | }
118 |
119 | Column(
120 | modifier = scrollableModifier,
121 | ) {
122 | // Fixed top part
123 | Row {
124 |
125 | // Fixed top and left part
126 | Column {
127 | content.take(fixedTopSize).forEachIndexed { rowIndex, rowList ->
128 | Row {
129 | rowList.take(fixedStartSize).forEachIndexed { columnIndex, item ->
130 | TableCell(
131 | width = tableItemSize.columnWidthSize[columnIndex],
132 | height = tableItemSize.rowHeightSize[rowIndex],
133 | backgroundColor = backgroundColor(rowIndex, columnIndex),
134 | contentAlignment = contentAlignment(rowIndex, columnIndex),
135 | outlineColor = outlineColor,
136 | outlineStroke = outlineStroke,
137 | content = item,
138 | )
139 | }
140 | }
141 | }
142 | }
143 |
144 | // Fixed top part
145 | CompositionLocalProvider(
146 | // Disable horizontal OverScrollEffect
147 | // because it cannot be common OverScrollEffect and results in strange behavior.
148 | LocalOverscrollConfiguration provides null,
149 | ) {
150 | Column(
151 | modifier = if (isFixedTop) {
152 | Modifier.horizontalScroll(horizontalScrollState)
153 | } else {
154 | Modifier
155 | },
156 | ) {
157 | content.take(fixedTopSize).forEachIndexed { rowIndex, rowList ->
158 | Row {
159 | rowList.takeLast(rowList.size - fixedStartSize).forEachIndexed { columnIndex, item ->
160 | TableCell(
161 | width = tableItemSize.columnWidthSize[columnIndex + fixedStartSize],
162 | height = tableItemSize.rowHeightSize[rowIndex],
163 | backgroundColor = backgroundColor(rowIndex, columnIndex + fixedStartSize),
164 | contentAlignment = contentAlignment(rowIndex, columnIndex + fixedStartSize),
165 | outlineColor = outlineColor,
166 | outlineStroke = outlineStroke,
167 | content = item,
168 | )
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
175 |
176 | // Unfixed top part
177 | val unFixedRowModifier = if (isFixedStart.not() && isFixedTop.not()) {
178 | Modifier
179 | } else {
180 | Modifier
181 | .verticalScroll(verticalScrollState)
182 | .pointerInput(Unit) {
183 | detectDragGestures(
184 | onDragStart = { dragScroll2DState.onDragStart() },
185 | onDrag = { change, dragAmount ->
186 | change.consume()
187 | dragScroll2DState.onDrag(dragAmount.x, dragAmount.y)
188 | },
189 | onDragEnd = { dragScroll2DState.onDragEnd(coroutineScope) },
190 | onDragCancel = { dragScroll2DState.onDragCancel() },
191 | )
192 | }
193 | }
194 |
195 | Row(modifier = unFixedRowModifier) {
196 |
197 | // Fixed left part
198 | Column {
199 | content.takeLast(content.size - fixedTopSize).forEachIndexed { rowIndex, rowList ->
200 | Row {
201 | rowList.take(fixedStartSize).forEachIndexed { columnIndex, item ->
202 | TableCell(
203 | width = tableItemSize.columnWidthSize[columnIndex],
204 | height = tableItemSize.rowHeightSize[rowIndex + fixedTopSize],
205 | backgroundColor = backgroundColor(rowIndex + fixedTopSize, columnIndex),
206 | contentAlignment = contentAlignment(rowIndex + fixedTopSize, columnIndex),
207 | outlineColor = outlineColor,
208 | outlineStroke = outlineStroke,
209 | content = item,
210 | )
211 | }
212 | }
213 | }
214 | }
215 |
216 | // Unfixed part
217 | CompositionLocalProvider(
218 | // Disable horizontal OverScrollEffect
219 | // because it cannot be common OverScrollEffect and results in strange behavior.
220 | LocalOverscrollConfiguration provides null,
221 | ) {
222 | val unFixedColumnModifier = if (isFixedTop || isFixedStart) {
223 | Modifier
224 | .horizontalScroll(horizontalScrollState)
225 | .pointerInput(Unit) {
226 | detectDragGestures(
227 | onDragStart = { dragScroll2DState.onDragStart() },
228 | onDrag = { change, dragAmount ->
229 | change.consume()
230 | dragScroll2DState.onDrag(dragAmount.x, dragAmount.y)
231 | },
232 | onDragEnd = { dragScroll2DState.onDragEnd(coroutineScope) },
233 | onDragCancel = { dragScroll2DState.onDragCancel() },
234 | )
235 | }
236 | } else {
237 | Modifier
238 | }
239 |
240 | Column(modifier = unFixedColumnModifier) {
241 | content.takeLast(content.size - fixedTopSize).forEachIndexed { rowIndex, rowList ->
242 | Row {
243 | rowList.takeLast(content.first().size - fixedStartSize).forEachIndexed { columnIndex, item ->
244 | TableCell(
245 | width = tableItemSize.columnWidthSize[columnIndex + fixedStartSize],
246 | height = tableItemSize.rowHeightSize[rowIndex + fixedTopSize],
247 | backgroundColor = backgroundColor(rowIndex + fixedTopSize, columnIndex + fixedStartSize),
248 | contentAlignment = contentAlignment(rowIndex + fixedTopSize, columnIndex + fixedStartSize),
249 | outlineColor = outlineColor,
250 | outlineStroke = outlineStroke,
251 | content = item,
252 | )
253 | }
254 | }
255 | }
256 | }
257 | }
258 | }
259 | }
260 | }
261 | }
262 |
263 | /**
264 | * Validates the input parameters for AutoSizeTable.
265 | *
266 | * @param content The table content to validate
267 | * @param fixedTopSize Number of fixed rows at the top
268 | * @param fixedStartSize Number of fixed columns at the start
269 | * @throws IllegalArgumentException if any validation fails
270 | */
271 | private fun validateTableParameters(
272 | content: List Unit>>,
273 | fixedTopSize: Int,
274 | fixedStartSize: Int,
275 | ) {
276 | // Validate content structure
277 | require(content.isNotEmpty()) {
278 | "Content must not be empty"
279 | }
280 |
281 | val columnCount = content.first().size
282 | require(content.all { it.size == columnCount }) {
283 | "All rows must have the same number of columns"
284 | }
285 |
286 | // Validate fixed size parameters
287 | require(fixedTopSize >= 0) {
288 | "fixedTopSize must be non-negative"
289 | }
290 | require(fixedStartSize >= 0) {
291 | "fixedStartSize must be non-negative"
292 | }
293 |
294 | // Validate fixed sizes are within bounds
295 | val rowCount = content.size
296 | require(fixedTopSize <= rowCount) {
297 | "fixedTopSize ($fixedTopSize) must not exceed the number of rows ($rowCount)"
298 | }
299 | require(fixedStartSize <= columnCount) {
300 | "fixedStartSize ($fixedStartSize) must not exceed the number of columns ($columnCount)"
301 | }
302 | }
303 |
304 | /**
305 | * Represents the size information for each column and row in the table.
306 | *
307 | * @param columnWidthSize Width of each column
308 | * @param rowHeightSize Height of each row
309 | */
310 | private data class TableItemSize(
311 | val columnWidthSize: List,
312 | val rowHeightSize: List,
313 | )
314 |
315 | /**
316 | * A single cell in the table with consistent styling.
317 | *
318 | * @param width Width of the cell
319 | * @param height Height of the cell
320 | * @param backgroundColor Background color of the cell
321 | * @param contentAlignment Alignment of the content within the cell
322 | * @param outlineColor Color of the cell border
323 | * @param outlineStroke Stroke style for the cell border
324 | * @param content Content to display in the cell
325 | */
326 | @Composable
327 | private fun TableCell(
328 | width: Dp,
329 | height: Dp,
330 | backgroundColor: Color,
331 | contentAlignment: Alignment,
332 | outlineColor: Color,
333 | outlineStroke: Stroke,
334 | content: @Composable () -> Unit,
335 | ) {
336 | Box(
337 | modifier = Modifier
338 | .size(width = width, height = height)
339 | .background(color = backgroundColor)
340 | .drawBehind {
341 | drawRect(
342 | color = outlineColor,
343 | topLeft = Offset(0f, 0f),
344 | size = Size(width = size.width, height = size.height),
345 | style = outlineStroke,
346 | )
347 | },
348 | contentAlignment = contentAlignment,
349 | ) {
350 | content()
351 | }
352 | }
353 |
354 | /**
355 | * Measure the size of each item in the table and display it.
356 | *
357 | * @param modifier Modifier for table
358 | * @param items List of items to be displayed in the table
359 | * @param content Content to be displayed in the table
360 | */
361 | @Composable
362 | private fun MeasureTable(
363 | modifier: Modifier = Modifier,
364 | items: List Unit>>,
365 | content: @Composable (TableItemSize) -> Unit,
366 | ) {
367 | SubcomposeLayout(modifier = modifier) { constraints ->
368 | val rowCount = items.size
369 | val columnCount = items.first().size
370 |
371 | val maxHeightPerRow = MutableList(rowCount) { 0.dp }
372 | val maxWidthPerColumn = MutableList(columnCount) { 0.dp }
373 |
374 | val itemsMeasurable = items.mapIndexed { rowIndex, rowList ->
375 | List(rowList.size) { columnIndex ->
376 | subcompose("${rowIndex}_$columnIndex") {
377 | items[rowIndex][columnIndex]()
378 | }.first().measure(Constraints())
379 | }
380 | }
381 |
382 | items.forEachIndexed { rowIndex, rowList ->
383 | rowList.forEachIndexed { columnIndex, _ ->
384 | val item = itemsMeasurable[rowIndex][columnIndex]
385 | val width = item.width.toDp()
386 | val height = item.height.toDp()
387 | maxWidthPerColumn[columnIndex] = maxOf(maxWidthPerColumn[columnIndex], width)
388 | maxHeightPerRow[rowIndex] = maxOf(maxHeightPerRow[rowIndex], height)
389 | }
390 | }
391 |
392 | val contentPlaceable = subcompose("content") {
393 | content(TableItemSize(maxWidthPerColumn, maxHeightPerRow))
394 | }.first().measure(constraints)
395 |
396 | layout(contentPlaceable.width, contentPlaceable.height) {
397 | contentPlaceable.place(0, 0)
398 | }
399 | }
400 | }
401 |
--------------------------------------------------------------------------------