├── .idea ├── .name ├── .gitignore ├── compiler.xml ├── vcs.xml ├── gradle.xml ├── misc.xml └── jarRepositories.xml ├── segmenteddisplay ├── consumer-rules.pro ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── rabross │ │ │ └── segmenteddisplay │ │ │ ├── seven │ │ │ ├── SegmentStyle.kt │ │ │ ├── Decoder.kt │ │ │ ├── Animations.kt │ │ │ └── Composable.kt │ │ │ ├── delimiter │ │ │ ├── Decoder.kt │ │ │ └── Composable.kt │ │ │ ├── Led.kt │ │ │ └── fourteen │ │ │ ├── Decoder.kt │ │ │ └── Composable.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro ├── build.gradle └── .gitignore ├── jitpack.yml ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ ├── drawable │ │ │ │ ├── bitmap.png │ │ │ │ ├── color_bitmap.png │ │ │ │ └── ic_launcher_background.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 │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── rabross │ │ │ └── segmenteddisplay │ │ │ └── app │ │ │ ├── SampleIndexActivity.kt │ │ │ ├── TypingActivity.kt │ │ │ ├── SevenSegmentHeartActivity.kt │ │ │ ├── SevenSegmentActivity.kt │ │ │ ├── FourteenSegmentActivity.kt │ │ │ └── SevenSegmentScreenActivity.kt │ └── test │ │ └── java │ │ └── com │ │ └── rabross │ │ └── segmenteddisplay │ │ └── app │ │ └── ByteBufferSevenSegmentIndexMapperTests.kt ├── proguard-rules.pro ├── build.gradle └── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── LICENSE ├── gradle.properties ├── README.md ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | SegmentDisplay -------------------------------------------------------------------------------- /segmenteddisplay/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | install: 4 | - ./gradlew build :segmenteddisplay:publishToMavenLocal -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SegmentedDisplay 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/bitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/drawable/bitmap.png -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | /inspectionProfiles/ 5 | /deploymentTargetDropDown.xml 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/color_bitmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/drawable/color_bitmap.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rabross/SegmentedDisplay/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/rabross/SegmentedDisplay/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/rabross/SegmentedDisplay/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/rabross/SegmentedDisplay/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/rabross/SegmentedDisplay/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/seven/SegmentStyle.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.seven 2 | 3 | enum class SegmentStyle { 4 | FLAT, DIFFUSER 5 | } -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /segmenteddisplay/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 16 09:55:56 BST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/delimiter/Decoder.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.delimiter 2 | 3 | interface Decoder { 4 | val a: Int 5 | val b: Int 6 | } 7 | 8 | class BinaryDecoder(data: Int = 0) : Decoder { 9 | override val a = data and 0b01 10 | override val b = data and 0b10 11 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | jcenter() // Warning: this repository is going to shut down soon 7 | } 8 | } 9 | rootProject.name = "SegmentDisplay" 10 | include ':app' 11 | include ':segmenteddisplay' 12 | -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/Led.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | fun interface Led { 6 | fun signal(value: Int): Color 7 | } 8 | 9 | class SingleColorLed(private val on: Color, private val off: Color) : Led { 10 | override fun signal(value: Int) = if (value > 0) on else off 11 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /segmenteddisplay/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 -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rab Ross 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 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/seven/Decoder.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.seven 2 | 3 | interface Decoder { 4 | val a: Int 5 | val b: Int 6 | val c: Int 7 | val d: Int 8 | val e: Int 9 | val f: Int 10 | val g: Int 11 | } 12 | 13 | class BinaryDecoder(data: Int = 0) : Decoder { 14 | override val a = data and 0b00000001 15 | override val b = data and 0b00000010 16 | override val c = data and 0b00000100 17 | override val d = data and 0b00001000 18 | override val e = data and 0b00010000 19 | override val f = data and 0b00100000 20 | override val g = data and 0b01000000 21 | 22 | companion object { 23 | fun mapToDisplay(number: Int): Int { 24 | return when (number) { 25 | 0 -> 0b00111111 26 | 1 -> 0b00000110 27 | 2 -> 0b01011011 28 | 3 -> 0b01001111 29 | 4 -> 0b01100110 30 | 5 -> 0b01101101 31 | 6 -> 0b01111101 32 | 7 -> 0b00000111 33 | 8 -> 0b01111111 34 | 9 -> 0b01100111 35 | else -> 0b00000000 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/rabross/SegmentedDisplay.svg)](https://jitpack.io/#rabross/SegmentedDisplay) 2 | ![GitHub](https://img.shields.io/github/license/rabross/SegmentedDisplay.svg) 3 | 4 | # SegmentedDisplay 5 | A 7-segment and 14-segment display for Android built with Jetpack Compose 6 | 7 | 8 | 9 | 10 | 11 | Inspired by [limpfish](https://www.plingboot.com/oldskool-demo-on-a-7-segment-display/) 12 | 13 | ## Customizable 14 | Highly customisable. 15 | 16 | 17 | 18 | ## Dependency 19 | 20 | implementation 'com.github.rabross:SegmentedDisplay:0.4.0' 21 | 22 | Add the JitPack repository to your root `build.gradle` file 23 | 24 | allprojects { 25 | repositories { 26 | ... 27 | maven { url 'https://jitpack.io' } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/rabross/segmenteddisplay/app/SampleIndexActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material.Button 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.unit.dp 16 | 17 | class SampleIndexActivity : ComponentActivity() { 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | 21 | setContent { 22 | Column { 23 | LaunchButton(text = "7-segment display") 24 | LaunchButton(text = "7-segment display heart") 25 | LaunchButton(text = "14-segment display") 26 | LaunchButton(text = "14-segment display typing") 27 | LaunchButton(text = "Displaying an image") 28 | } 29 | } 30 | } 31 | 32 | @Composable 33 | inline fun LaunchButton(text: String) { 34 | val context = LocalContext.current 35 | Button(modifier = Modifier 36 | .fillMaxWidth() 37 | .padding(16.dp), 38 | onClick = { context.startActivity(Intent(context, T::class.java)) }) { 39 | Text(text = text) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 30 8 | 9 | defaultConfig { 10 | applicationId "com.rabross.segmenteddisplay" 11 | minSdk 21 12 | targetSdk 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | vectorDrawables { 18 | useSupportLibrary true 19 | } 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | useIR = true 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion compose_version 41 | kotlinCompilerVersion '1.5.10' 42 | } 43 | packagingOptions { 44 | resources { 45 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 46 | } 47 | } 48 | } 49 | 50 | dependencies { 51 | 52 | implementation project(path: ':segmenteddisplay') 53 | implementation 'androidx.core:core-ktx:1.6.0' 54 | implementation 'androidx.appcompat:appcompat:1.3.1' 55 | implementation 'com.google.android.material:material:1.4.0' 56 | implementation "androidx.compose.ui:ui:$compose_version" 57 | implementation "androidx.compose.material:material:$compose_version" 58 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 59 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 60 | implementation 'androidx.activity:activity-compose:1.3.1' 61 | testImplementation 'junit:junit:4.13.2' 62 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 63 | } -------------------------------------------------------------------------------- /segmenteddisplay/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'maven-publish' 5 | } 6 | 7 | android { 8 | compileSdk 30 9 | 10 | defaultConfig { 11 | minSdk 21 12 | targetSdk 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_11 28 | targetCompatibility JavaVersion.VERSION_11 29 | } 30 | kotlinOptions { 31 | jvmTarget = '11' 32 | useIR = true 33 | } 34 | buildFeatures { 35 | compose true 36 | } 37 | composeOptions { 38 | kotlinCompilerExtensionVersion compose_version 39 | kotlinCompilerVersion '1.5.10' 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation 'androidx.core:core-ktx:1.6.0' 46 | implementation 'androidx.appcompat:appcompat:1.3.1' 47 | implementation 'com.google.android.material:material:1.4.0' 48 | implementation "androidx.compose.ui:ui:$compose_version" 49 | implementation "androidx.compose.material:material:$compose_version" 50 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 51 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 52 | implementation 'androidx.activity:activity-compose:1.3.1' 53 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 54 | } 55 | 56 | task androidSourcesJar(type: Jar) { 57 | classifier 'sources' 58 | from android.sourceSets.main.java.srcDirs 59 | } 60 | 61 | project.afterEvaluate { 62 | publishing { 63 | publications { 64 | release(MavenPublication) { 65 | from components.release 66 | artifact androidSourcesJar 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/delimiter/Composable.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.delimiter 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.geometry.Size 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.drawscope.DrawScope 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import androidx.compose.ui.unit.dp 14 | import com.rabross.segmenteddisplay.Led 15 | import com.rabross.segmenteddisplay.SingleColorLed 16 | 17 | @Preview 18 | @Composable 19 | fun DelimiterPreview() { 20 | Surface { 21 | Delimiter() 22 | } 23 | } 24 | 25 | @Composable 26 | fun Delimiter( 27 | modifier: Modifier = Modifier, 28 | segmentScale: Int = 3, 29 | led: Led = SingleColorLed(Color.Red, Color.DarkGray.copy(alpha = 0.3f)), 30 | decoder: Decoder = BinaryDecoder() 31 | ) { 32 | Canvas( 33 | modifier = modifier 34 | .aspectRatio(((1f + segmentScale + 1f)) / (1f + segmentScale + 1f + segmentScale + 1f), true) 35 | .size(100.dp) 36 | ) { 37 | val scaleWidth = 1 + segmentScale + 1 38 | val scaleHeight = 1 + segmentScale + 1 + segmentScale + 1 39 | 40 | val segmentWidthByWidth = size.width / scaleWidth 41 | val segmentWidthByHeight = size.height / scaleHeight 42 | 43 | val segmentWidth = if (scaleHeight * segmentWidthByWidth < size.height) segmentWidthByWidth else segmentWidthByHeight 44 | 45 | val totalWidth = segmentWidth * scaleWidth 46 | val totalHeight = segmentWidth * scaleHeight 47 | 48 | drawDelimiter( 49 | led.signal(decoder.a), 50 | led.signal(decoder.b), 51 | segmentWidth / 2, 52 | Offset(0f, segmentWidth / 2), 53 | Size(totalWidth, totalHeight - segmentWidth) 54 | ) 55 | } 56 | } 57 | 58 | private fun DrawScope.drawDelimiter(upDotColor: Color, downDotColor: Color, radius: Float, offset: Offset, size: Size) { 59 | val upDotCenterOffset = Offset(size.width / 2 + offset.x, size.height / 4 + offset.y) 60 | val downDotCenterOffset = Offset(size.width / 2 + offset.x, size.height / 4 * 3 + offset.y) 61 | drawCircle(upDotColor, radius, upDotCenterOffset) 62 | drawCircle(downDotColor, radius, downDotCenterOffset) 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rabross/segmenteddisplay/app/TypingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.Surface 8 | import androidx.compose.material.Text 9 | import androidx.compose.material.TextField 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import com.rabross.segmenteddisplay.fourteen.BinaryDecoder 18 | import com.rabross.segmenteddisplay.fourteen.SegmentDisplay 19 | 20 | class TypingActivity : ComponentActivity() { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | 26 | var text by remember { mutableStateOf("Hello World") } 27 | 28 | Column { 29 | Surface( 30 | modifier = Modifier 31 | .weight(1f), color = Color.Black 32 | ) { 33 | Row(modifier = Modifier.padding(24.dp, 24.dp, 24.dp, 0.dp)) { 34 | if (text.isBlank()) { 35 | Spacer(modifier = Modifier.weight(1f)) 36 | } else { 37 | for (c in text) { 38 | SegmentDisplay( 39 | modifier = Modifier.weight(1f), 40 | decoder = BinaryDecoder( 41 | BinaryDecoder.mapToDisplay(c.uppercaseChar()) 42 | ) 43 | ) 44 | } 45 | } 46 | } 47 | } 48 | Row( 49 | Modifier 50 | .weight(2f) 51 | .fillMaxWidth() 52 | .padding(24.dp, 0.dp) 53 | ) { 54 | TextField(modifier = Modifier.fillMaxWidth(), 55 | value = text, 56 | onValueChange = { text = it }, 57 | label = { Text("input") } 58 | ) 59 | } 60 | 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/rabross/segmenteddisplay/app/SevenSegmentHeartActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Surface 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.dp 13 | import com.rabross.segmenteddisplay.seven.BinaryDecoder 14 | import com.rabross.segmenteddisplay.seven.SegmentDisplay 15 | 16 | class SevenSegmentHeartActivity : ComponentActivity() { 17 | 18 | private val rows = 6 19 | private val columns = 13 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContent { 24 | 25 | Surface(color = Color.Black) { 26 | Column(modifier = Modifier.padding(24.dp)) { 27 | for (row in 0 until rows) { 28 | Row { 29 | for (column in 0 until columns) { 30 | SegmentDisplay( 31 | modifier = Modifier 32 | .weight(1f) 33 | .padding(2.dp), 34 | decoder = BinaryDecoder(buffer[(row * columns) + column]) 35 | ) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | private val buffer = listOf( 45 | 0b0000000, 0b0000100, 0b1011110, 0b1111111, 0b1111111, 0b1111110, 0b0010000, 0b0000100, 0b1111110, 0b1111111, 0b1111110, 0b1011100, 0b0000000, 46 | 0b0000100, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111101, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111101, 47 | 0b0000010, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111011, 48 | 0b0000000, 0b0000010, 0b1100111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1111111, 0b1100011, 0b0000000, 49 | 0b0000000, 0b0000000, 0b0000000, 0b0000000, 0b0100011, 0b1110111, 0b1111111, 0b1111111, 0b1110011, 0b0100001, 0b0000000, 0b0000000, 0b0000000, 50 | 0b0000000, 0b0000000, 0b0000000, 0b0000000, 0b0000000, 0b0000000, 0b1100111, 0b0100001, 0b0000000, 0b0000000, 0b0000000, 0b0000000, 0b0000000 51 | ) 52 | } -------------------------------------------------------------------------------- /segmenteddisplay/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | ### Gradle template 3 | .gradle 4 | **/build/ 5 | !src/**/build/ 6 | 7 | # Ignore Gradle GUI config 8 | gradle-app.setting 9 | 10 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 11 | !gradle-wrapper.jar 12 | 13 | # Cache of project 14 | .gradletasknamecache 15 | 16 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 17 | # gradle/wrapper/gradle-wrapper.properties 18 | 19 | ### Kotlin template 20 | # Compiled class file 21 | *.class 22 | 23 | # Log file 24 | *.log 25 | 26 | # BlueJ files 27 | *.ctxt 28 | 29 | # Mobile Tools for Java (J2ME) 30 | .mtj.tmp/ 31 | 32 | # Package Files # 33 | *.jar 34 | *.war 35 | *.nar 36 | *.ear 37 | *.zip 38 | *.tar.gz 39 | *.rar 40 | 41 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 42 | hs_err_pid* 43 | 44 | ### Android template 45 | # Built application files 46 | *.apk 47 | *.aar 48 | *.ap_ 49 | *.aab 50 | 51 | # Files for the ART/Dalvik VM 52 | *.dex 53 | 54 | # Java class files 55 | *.class 56 | 57 | # Generated files 58 | bin/ 59 | gen/ 60 | out/ 61 | # Uncomment the following line in case you need and you don't have the release build type files in your app 62 | # release/ 63 | 64 | # Gradle files 65 | .gradle/ 66 | build/ 67 | 68 | # Local configuration file (sdk path, etc) 69 | local.properties 70 | 71 | # Proguard folder generated by Eclipse 72 | proguard/ 73 | 74 | # Log Files 75 | *.log 76 | 77 | # Android Studio Navigation editor temp files 78 | .navigation/ 79 | 80 | # Android Studio captures folder 81 | captures/ 82 | 83 | # IntelliJ 84 | *.iml 85 | .idea/workspace.xml 86 | .idea/tasks.xml 87 | .idea/gradle.xml 88 | .idea/assetWizardSettings.xml 89 | .idea/dictionaries 90 | .idea/libraries 91 | # Android Studio 3 in .gitignore file. 92 | .idea/caches 93 | .idea/modules.xml 94 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 95 | .idea/navEditor.xml 96 | 97 | # Keystore files 98 | # Uncomment the following lines if you do not want to check your keystore files in. 99 | #*.jks 100 | #*.keystore 101 | 102 | # External native build folder generated in Android Studio 2.2 and later 103 | .externalNativeBuild 104 | .cxx/ 105 | 106 | # Google Services (e.g. APIs or Firebase) 107 | # google-services.json 108 | 109 | # Freeline 110 | freeline.py 111 | freeline/ 112 | freeline_project_description.json 113 | 114 | # fastlane 115 | fastlane/report.xml 116 | fastlane/Preview.html 117 | fastlane/screenshots 118 | fastlane/test_output 119 | fastlane/readme.md 120 | 121 | # Version control 122 | vcs.xml 123 | 124 | # lint 125 | lint/intermediates/ 126 | lint/generated/ 127 | lint/outputs/ 128 | lint/tmp/ 129 | # lint/reports/ 130 | 131 | # Android Profiling 132 | *.hprof 133 | 134 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/fourteen/Decoder.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.fourteen 2 | 3 | interface Decoder { 4 | val a: Int 5 | val b: Int 6 | val c: Int 7 | val d: Int 8 | val e: Int 9 | val f: Int 10 | val g1: Int 11 | val g2: Int 12 | val h: Int 13 | val i: Int 14 | val j: Int 15 | val k: Int 16 | val l: Int 17 | val m: Int 18 | } 19 | 20 | class BinaryDecoder(data: Int = 0) : Decoder { 21 | override val a = data and 0b0000000000000001 22 | override val b = data and 0b0000000000000010 23 | override val c = data and 0b0000000000000100 24 | override val d = data and 0b0000000000001000 25 | override val e = data and 0b0000000000010000 26 | override val f = data and 0b0000000000100000 27 | override val g1 = data and 0b0000000001000000 28 | override val g2 = data and 0b0000000010000000 29 | override val h = data and 0b0000000100000000 30 | override val i = data and 0b0000001000000000 31 | override val j = data and 0b0000010000000000 32 | override val k = data and 0b0000100000000000 33 | override val l = data and 0b0001000000000000 34 | override val m = data and 0b0010000000000000 35 | 36 | companion object { 37 | fun mapToDisplay(number: Int): Int { 38 | return when (number) { 39 | 0 -> 0xC3F 40 | 1 -> 0x406 41 | 2 -> 0xDB 42 | 3 -> 0x8F 43 | 4 -> 0xE6 44 | 5 -> 0xED 45 | 6 -> 0xFD 46 | 7 -> 0x1401 47 | 8 -> 0xFF 48 | 9 -> 0xE7 49 | else -> 0x0 50 | } 51 | } 52 | fun mapToDisplay(char: Char): Int { 53 | return when (char) { 54 | 'A' -> 0xF7 55 | 'B' -> 0x128F 56 | 'C' -> 0x39 57 | 'D' -> 0x120F 58 | 'E' -> 0xF9 59 | 'F' -> 0xF1 60 | 'G' -> 0xBD 61 | 'H' -> 0xF6 62 | 'I' -> 0x1209 63 | 'J' -> 0x1E 64 | 'K' -> 0x2470 65 | 'L' -> 0x38 66 | 'M' -> 0x536 67 | 'N' -> 0x2136 68 | 'O' -> 0x3F 69 | 'P' -> 0xF3 70 | 'Q' -> 0x203F 71 | 'R' -> 0x20F3 72 | 'S' -> 0x18D 73 | 'T' -> 0x1201 74 | 'U' -> 0x3E 75 | 'V' -> 0xC30 76 | 'W' -> 0x2836 77 | 'X' -> 0x2D00 78 | 'Y' -> 0x1500 79 | 'Z' -> 0xC09 80 | '0' -> mapToDisplay(0) 81 | '1' -> mapToDisplay(1) 82 | '2' -> mapToDisplay(2) 83 | '3' -> mapToDisplay(3) 84 | '4' -> mapToDisplay(4) 85 | '5' -> mapToDisplay(5) 86 | '6' -> mapToDisplay(6) 87 | '7' -> mapToDisplay(7) 88 | '8' -> mapToDisplay(8) 89 | '9' -> mapToDisplay(9) 90 | else -> 0x0 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/seven/Animations.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.seven 2 | 3 | import kotlinx.coroutines.currentCoroutineContext 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | import kotlinx.coroutines.isActive 8 | import kotlin.time.Duration 9 | import kotlin.time.ExperimentalTime 10 | 11 | class Animations { 12 | companion object { 13 | 14 | @ExperimentalTime 15 | private fun animationFlowGenerator(frames: List): Flow { 16 | return flow { 17 | while (currentCoroutineContext().isActive) { 18 | frames.forEach { 19 | emit(it) 20 | delay(Duration.milliseconds(100)) 21 | } 22 | } 23 | } 24 | } 25 | 26 | @ExperimentalTime 27 | val roundOutsideSingleSeg = animationFlowGenerator( 28 | listOf( 29 | 0b000000001, 30 | 0b000000010, 31 | 0b000000100, 32 | 0b000001000, 33 | 0b000010000, 34 | 0b000100000 35 | ) 36 | ) 37 | 38 | @ExperimentalTime 39 | val roundOutsideDoubleSeg = animationFlowGenerator( 40 | listOf( 41 | 0b000000001, 42 | 0b000000011, 43 | 0b000000010, 44 | 0b000000110, 45 | 0b000000100, 46 | 0b000001100, 47 | 0b000001000, 48 | 0b000011000, 49 | 0b000010000, 50 | 0b000110000, 51 | 0b000100000, 52 | 0b000100001 53 | ) 54 | ) 55 | 56 | @ExperimentalTime 57 | val fillOutside = animationFlowGenerator( 58 | listOf( 59 | 0b000000000, 60 | 0b000000001, 61 | 0b000000011, 62 | 0b000000111, 63 | 0b000001111, 64 | 0b000011111, 65 | 0b000111111 66 | ) 67 | ) 68 | 69 | @ExperimentalTime 70 | val pathEight = animationFlowGenerator( 71 | listOf( 72 | 0b000000011, 73 | 0b001000010, 74 | 0b001010000, 75 | 0b000011000, 76 | 0b000001100, 77 | 0b001000100, 78 | 0b001100000, 79 | 0b000100001 80 | ) 81 | ) 82 | 83 | @ExperimentalTime 84 | val opposing = animationFlowGenerator( 85 | listOf( 86 | 0b000001001, 87 | 0b000010010, 88 | 0b000100100, 89 | ) 90 | ) 91 | 92 | @ExperimentalTime 93 | val fallSingle = animationFlowGenerator( 94 | listOf( 95 | 0b000000001, 96 | 0b000100010, 97 | 0b001000000, 98 | 0b000010100, 99 | 0b000001000, 100 | ) 101 | ) 102 | 103 | @ExperimentalTime 104 | val fallFill = animationFlowGenerator( 105 | listOf( 106 | 0b000000000, 107 | 0b000000001, 108 | 0b000100011, 109 | 0b001100011, 110 | 0b001110111, 111 | 0b001111111, 112 | 0b001111110, 113 | 0b001011100, 114 | 0b000011100, 115 | 0b000001000, 116 | ) 117 | ) 118 | 119 | @ExperimentalTime 120 | val rightToLeftFill = animationFlowGenerator( 121 | listOf( 122 | 0b000000110, 123 | 0b001001111, 124 | 0b001111111, 125 | 0b001111001, 126 | 0b000110000, 127 | 0b000000000, 128 | ) 129 | ) 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rabross/segmenteddisplay/app/SevenSegmentActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.Surface 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.dp 13 | import androidx.lifecycle.lifecycleScope 14 | import com.rabross.segmenteddisplay.SingleColorLed 15 | import com.rabross.segmenteddisplay.seven.Animations 16 | import com.rabross.segmenteddisplay.seven.BinaryDecoder 17 | import com.rabross.segmenteddisplay.seven.DigitalClock 18 | import com.rabross.segmenteddisplay.seven.SegmentDisplay 19 | import kotlinx.coroutines.currentCoroutineContext 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.flow.* 22 | import kotlinx.coroutines.isActive 23 | import java.util.* 24 | import kotlin.time.Duration 25 | import kotlin.time.ExperimentalTime 26 | 27 | @OptIn(ExperimentalTime::class) 28 | class SevenSegmentActivity : ComponentActivity() { 29 | 30 | private val redLed = SingleColorLed(Color.Red, Color.Black) 31 | private val greenLed = SingleColorLed(Color.Green, Color.Black) 32 | private val blueLed = SingleColorLed(Color.Blue, Color.Black) 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | setContent { 37 | val animationRightToLeftFill = remember { mutableStateOf(0) } 38 | val animationRoundOutsideDoubleSeg = remember { mutableStateOf(0) } 39 | val animationFallFill = remember { mutableStateOf(0) } 40 | 41 | val hourFirst = remember { mutableStateOf(0) } 42 | val hourSecond = remember { mutableStateOf(0) } 43 | val minuteFirst = remember { mutableStateOf(0) } 44 | val minuteSecond = remember { mutableStateOf(0) } 45 | val secondFirst = remember { mutableStateOf(0) } 46 | val secondSecond = remember { mutableStateOf(0) } 47 | 48 | Surface(color = Color.Black) { 49 | Column(modifier = Modifier.padding(24.dp)) { 50 | DigitalClock( 51 | Modifier.weight(1f), 52 | BinaryDecoder.mapToDisplay(hourFirst.value), 53 | BinaryDecoder.mapToDisplay(hourSecond.value), 54 | BinaryDecoder.mapToDisplay(minuteFirst.value), 55 | BinaryDecoder.mapToDisplay(minuteSecond.value), 56 | BinaryDecoder.mapToDisplay(secondFirst.value), 57 | BinaryDecoder.mapToDisplay(secondSecond.value), 58 | delimiterSignal = 0b11 59 | ) 60 | Row( 61 | Modifier 62 | .weight(1f) 63 | .fillMaxWidth() 64 | ) { 65 | SegmentDisplay( 66 | modifier = Modifier.weight(1f).padding(4.dp), 67 | decoder = BinaryDecoder(animationFallFill.value), 68 | led = redLed 69 | ) 70 | SegmentDisplay( 71 | modifier = Modifier.weight(1f).padding(4.dp), 72 | decoder = BinaryDecoder(animationRightToLeftFill.value), 73 | led = greenLed 74 | ) 75 | SegmentDisplay( 76 | modifier = Modifier.weight(1f).padding(4.dp), 77 | decoder = BinaryDecoder(animationRoundOutsideDoubleSeg.value), 78 | led = blueLed 79 | ) 80 | } 81 | } 82 | } 83 | 84 | Animations.rightToLeftFill 85 | .onEach { animationRightToLeftFill.value = it } 86 | .launchIn(lifecycleScope) 87 | 88 | Animations.roundOutsideDoubleSeg 89 | .onEach { animationRoundOutsideDoubleSeg.value = it } 90 | .launchIn(lifecycleScope) 91 | 92 | Animations.fallFill 93 | .onEach { animationFallFill.value = it } 94 | .launchIn(lifecycleScope) 95 | 96 | tickerFlow(Duration.seconds(1)) 97 | .map { Calendar.getInstance() } 98 | .distinctUntilChanged { old, new -> 99 | old.get(Calendar.SECOND) == new.get(Calendar.SECOND) 100 | } 101 | .onEach { calendar -> 102 | val hour = calendar.get(Calendar.HOUR_OF_DAY) 103 | val hourDigits = hour.splitDigits() 104 | hourFirst.value = hourDigits.first 105 | hourSecond.value = hourDigits.second 106 | 107 | val minute = calendar.get(Calendar.MINUTE) 108 | val minuteDigits = minute.splitDigits() 109 | minuteFirst.value = minuteDigits.first 110 | minuteSecond.value = minuteDigits.second 111 | 112 | val second = calendar.get(Calendar.SECOND) 113 | val secondDigits = second.splitDigits() 114 | secondFirst.value = secondDigits.first 115 | secondSecond.value = secondDigits.second 116 | } 117 | .launchIn(lifecycleScope) 118 | } 119 | } 120 | 121 | private fun tickerFlow(period: Duration) = flow { 122 | while (currentCoroutineContext().isActive) { 123 | emit(Unit) 124 | delay(period) 125 | } 126 | } 127 | 128 | private fun Int.splitDigits(): Pair { 129 | return when { 130 | this <= 0 -> 0 to 0 131 | else -> { 132 | val secondDigit: Int = this % 10 133 | val firstDigit: Int = if (this < 10) 0 else this / 10 % 10 134 | firstDigit to secondDigit 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | # Android Profiling 90 | *.hprof 91 | 92 | ### Kotlin template 93 | # Compiled class file 94 | *.class 95 | 96 | # Log file 97 | *.log 98 | 99 | # BlueJ files 100 | *.ctxt 101 | 102 | # Mobile Tools for Java (J2ME) 103 | .mtj.tmp/ 104 | 105 | # Package Files # 106 | *.jar 107 | *.war 108 | *.nar 109 | *.ear 110 | *.zip 111 | *.tar.gz 112 | *.rar 113 | 114 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 115 | hs_err_pid* 116 | 117 | ### Kotlin template 118 | # Compiled class file 119 | *.class 120 | 121 | # Log file 122 | *.log 123 | 124 | # BlueJ files 125 | *.ctxt 126 | 127 | # Mobile Tools for Java (J2ME) 128 | .mtj.tmp/ 129 | 130 | # Package Files # 131 | *.jar 132 | *.war 133 | *.nar 134 | *.ear 135 | *.zip 136 | *.tar.gz 137 | *.rar 138 | 139 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 140 | hs_err_pid* 141 | 142 | ### Gradle template 143 | .gradle 144 | **/build/ 145 | !src/**/build/ 146 | 147 | # Ignore Gradle GUI config 148 | gradle-app.setting 149 | 150 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 151 | !gradle-wrapper.jar 152 | 153 | # Cache of project 154 | .gradletasknamecache 155 | 156 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 157 | # gradle/wrapper/gradle-wrapper.properties 158 | 159 | ### Android template 160 | # Built application files 161 | *.apk 162 | *.aar 163 | *.ap_ 164 | *.aab 165 | 166 | # Files for the ART/Dalvik VM 167 | *.dex 168 | 169 | # Java class files 170 | *.class 171 | 172 | # Generated files 173 | bin/ 174 | gen/ 175 | out/ 176 | # Uncomment the following line in case you need and you don't have the release build type files in your app 177 | # release/ 178 | 179 | # Gradle files 180 | .gradle/ 181 | build/ 182 | 183 | # Local configuration file (sdk path, etc) 184 | local.properties 185 | 186 | # Proguard folder generated by Eclipse 187 | proguard/ 188 | 189 | # Log Files 190 | *.log 191 | 192 | # Android Studio Navigation editor temp files 193 | .navigation/ 194 | 195 | # Android Studio captures folder 196 | captures/ 197 | 198 | # IntelliJ 199 | *.iml 200 | .idea/workspace.xml 201 | .idea/tasks.xml 202 | .idea/gradle.xml 203 | .idea/assetWizardSettings.xml 204 | .idea/dictionaries 205 | .idea/libraries 206 | # Android Studio 3 in .gitignore file. 207 | .idea/caches 208 | .idea/modules.xml 209 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 210 | .idea/navEditor.xml 211 | 212 | # Keystore files 213 | # Uncomment the following lines if you do not want to check your keystore files in. 214 | #*.jks 215 | #*.keystore 216 | 217 | # External native build folder generated in Android Studio 2.2 and later 218 | .externalNativeBuild 219 | .cxx/ 220 | 221 | # Google Services (e.g. APIs or Firebase) 222 | # google-services.json 223 | 224 | # Freeline 225 | freeline.py 226 | freeline/ 227 | freeline_project_description.json 228 | 229 | # fastlane 230 | fastlane/report.xml 231 | fastlane/Preview.html 232 | fastlane/screenshots 233 | fastlane/test_output 234 | fastlane/readme.md 235 | 236 | # Version control 237 | vcs.xml 238 | 239 | # lint 240 | lint/intermediates/ 241 | lint/generated/ 242 | lint/outputs/ 243 | lint/tmp/ 244 | # lint/reports/ 245 | 246 | # Android Profiling 247 | *.hprof 248 | 249 | ### Android template 250 | # Built application files 251 | *.apk 252 | *.aar 253 | *.ap_ 254 | *.aab 255 | 256 | # Files for the ART/Dalvik VM 257 | *.dex 258 | 259 | # Java class files 260 | *.class 261 | 262 | # Generated files 263 | bin/ 264 | gen/ 265 | out/ 266 | # Uncomment the following line in case you need and you don't have the release build type files in your app 267 | # release/ 268 | 269 | # Gradle files 270 | .gradle/ 271 | build/ 272 | 273 | # Local configuration file (sdk path, etc) 274 | local.properties 275 | 276 | # Proguard folder generated by Eclipse 277 | proguard/ 278 | 279 | # Log Files 280 | *.log 281 | 282 | # Android Studio Navigation editor temp files 283 | .navigation/ 284 | 285 | # Android Studio captures folder 286 | captures/ 287 | 288 | # IntelliJ 289 | *.iml 290 | .idea/workspace.xml 291 | .idea/tasks.xml 292 | .idea/gradle.xml 293 | .idea/assetWizardSettings.xml 294 | .idea/dictionaries 295 | .idea/libraries 296 | # Android Studio 3 in .gitignore file. 297 | .idea/caches 298 | .idea/modules.xml 299 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 300 | .idea/navEditor.xml 301 | 302 | # Keystore files 303 | # Uncomment the following lines if you do not want to check your keystore files in. 304 | #*.jks 305 | #*.keystore 306 | 307 | # External native build folder generated in Android Studio 2.2 and later 308 | .externalNativeBuild 309 | .cxx/ 310 | 311 | # Google Services (e.g. APIs or Firebase) 312 | # google-services.json 313 | 314 | # Freeline 315 | freeline.py 316 | freeline/ 317 | freeline_project_description.json 318 | 319 | # fastlane 320 | fastlane/report.xml 321 | fastlane/Preview.html 322 | fastlane/screenshots 323 | fastlane/test_output 324 | fastlane/readme.md 325 | 326 | # Version control 327 | vcs.xml 328 | 329 | # lint 330 | lint/intermediates/ 331 | lint/generated/ 332 | lint/outputs/ 333 | lint/tmp/ 334 | # lint/reports/ 335 | 336 | # Android Profiling 337 | *.hprof 338 | 339 | ### Gradle template 340 | .gradle 341 | **/build/ 342 | !src/**/build/ 343 | 344 | # Ignore Gradle GUI config 345 | gradle-app.setting 346 | 347 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 348 | !gradle-wrapper.jar 349 | 350 | # Cache of project 351 | .gradletasknamecache 352 | 353 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 354 | # gradle/wrapper/gradle-wrapper.properties 355 | 356 | -------------------------------------------------------------------------------- /app/src/main/java/com/rabross/segmenteddisplay/app/FourteenSegmentActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Surface 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.dp 15 | import androidx.lifecycle.lifecycleScope 16 | import com.rabross.segmenteddisplay.fourteen.BinaryDecoder 17 | import com.rabross.segmenteddisplay.fourteen.DigitalClock 18 | import com.rabross.segmenteddisplay.fourteen.SegmentDisplay 19 | import kotlinx.coroutines.currentCoroutineContext 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.flow.* 22 | import kotlinx.coroutines.isActive 23 | import java.util.* 24 | import kotlin.time.Duration 25 | import kotlin.time.ExperimentalTime 26 | 27 | @OptIn(ExperimentalTime::class) 28 | class FourteenSegmentActivity : ComponentActivity() { 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | setContent { 33 | val hourFirst = remember { mutableStateOf(0) } 34 | val hourSecond = remember { mutableStateOf(0) } 35 | val minuteFirst = remember { mutableStateOf(0) } 36 | val minuteSecond = remember { mutableStateOf(0) } 37 | val secondFirst = remember { mutableStateOf(0) } 38 | val secondSecond = remember { mutableStateOf(0) } 39 | 40 | Surface(color = Color.Black) { 41 | Column(modifier = Modifier.padding(24.dp)) { 42 | DigitalClock( 43 | Modifier.weight(1f), 44 | BinaryDecoder.mapToDisplay(hourFirst.value), 45 | BinaryDecoder.mapToDisplay(hourSecond.value), 46 | BinaryDecoder.mapToDisplay(minuteFirst.value), 47 | BinaryDecoder.mapToDisplay(minuteSecond.value), 48 | BinaryDecoder.mapToDisplay(secondFirst.value), 49 | BinaryDecoder.mapToDisplay(secondSecond.value), 50 | 3 51 | ) 52 | Row(Modifier.weight(1f)) { 53 | SegmentDisplay( 54 | modifier = Modifier 55 | .weight(1f) 56 | .padding(4.dp), 57 | decoder = BinaryDecoder( 58 | BinaryDecoder.mapToDisplay('H') 59 | ) 60 | ) 61 | SegmentDisplay( 62 | modifier = Modifier 63 | .weight(1f) 64 | .padding(4.dp), 65 | decoder = BinaryDecoder( 66 | BinaryDecoder.mapToDisplay('E') 67 | ) 68 | ) 69 | SegmentDisplay( 70 | modifier = Modifier 71 | .weight(1f) 72 | .padding(4.dp), 73 | decoder = BinaryDecoder( 74 | BinaryDecoder.mapToDisplay('L') 75 | ) 76 | ) 77 | SegmentDisplay( 78 | modifier = Modifier 79 | .weight(1f) 80 | .padding(4.dp), 81 | decoder = BinaryDecoder( 82 | BinaryDecoder.mapToDisplay('L') 83 | ) 84 | ) 85 | SegmentDisplay( 86 | modifier = Modifier 87 | .weight(1f) 88 | .padding(4.dp), 89 | decoder = BinaryDecoder( 90 | BinaryDecoder.mapToDisplay('O') 91 | ) 92 | ) 93 | } 94 | Row(Modifier.weight(1f)) { 95 | SegmentDisplay( 96 | modifier = Modifier 97 | .weight(1f) 98 | .padding(4.dp), 99 | decoder = BinaryDecoder( 100 | BinaryDecoder.mapToDisplay('W') 101 | ) 102 | ) 103 | SegmentDisplay( 104 | modifier = Modifier 105 | .weight(1f) 106 | .padding(4.dp), 107 | decoder = BinaryDecoder( 108 | BinaryDecoder.mapToDisplay('O') 109 | ) 110 | ) 111 | SegmentDisplay( 112 | modifier = Modifier 113 | .weight(1f) 114 | .padding(4.dp), 115 | decoder = BinaryDecoder( 116 | BinaryDecoder.mapToDisplay('R') 117 | ) 118 | ) 119 | SegmentDisplay( 120 | modifier = Modifier 121 | .weight(1f) 122 | .padding(4.dp), 123 | decoder = BinaryDecoder( 124 | BinaryDecoder.mapToDisplay('L') 125 | ) 126 | ) 127 | SegmentDisplay( 128 | modifier = Modifier 129 | .weight(1f) 130 | .padding(4.dp), 131 | decoder = BinaryDecoder( 132 | BinaryDecoder.mapToDisplay('D') 133 | ) 134 | ) 135 | } 136 | } 137 | } 138 | 139 | tickerFlow(Duration.seconds(1)) 140 | .map { Calendar.getInstance() } 141 | .distinctUntilChanged { old, new -> 142 | old.get(Calendar.SECOND) == new.get(Calendar.SECOND) 143 | } 144 | .onEach { calendar -> 145 | val hour = calendar.get(Calendar.HOUR_OF_DAY) 146 | val hourDigits = hour.splitDigits() 147 | hourFirst.value = hourDigits.first 148 | hourSecond.value = hourDigits.second 149 | 150 | val minute = calendar.get(Calendar.MINUTE) 151 | val minuteDigits = minute.splitDigits() 152 | minuteFirst.value = minuteDigits.first 153 | minuteSecond.value = minuteDigits.second 154 | 155 | val second = calendar.get(Calendar.SECOND) 156 | val secondDigits = second.splitDigits() 157 | secondFirst.value = secondDigits.first 158 | secondSecond.value = secondDigits.second 159 | } 160 | .launchIn(lifecycleScope) 161 | } 162 | } 163 | 164 | private fun tickerFlow(period: Duration) = flow { 165 | while (currentCoroutineContext().isActive) { 166 | emit(Unit) 167 | delay(period) 168 | } 169 | } 170 | 171 | private fun Int.splitDigits(): Pair { 172 | return when { 173 | this <= 0 -> 0 to 0 174 | else -> { 175 | val secondDigit: Int = this % 10 176 | val firstDigit: Int = if (this < 10) 0 else this / 10 % 10 177 | firstDigit to secondDigit 178 | } 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rabross/segmenteddisplay/app/SevenSegmentScreenActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.graphics.Canvas 6 | import android.os.Bundle 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.compose.foundation.Image 10 | import androidx.compose.foundation.background 11 | import androidx.compose.foundation.layout.* 12 | import androidx.compose.material.Surface 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.unit.dp 16 | import androidx.core.graphics.scale 17 | import com.rabross.segmenteddisplay.seven.Decoder 18 | import com.rabross.segmenteddisplay.seven.SegmentDisplay 19 | import java.nio.ByteBuffer 20 | import java.security.InvalidParameterException 21 | import androidx.compose.ui.graphics.Color as ComposeColor 22 | 23 | class SevenSegmentScreenActivity : ComponentActivity() { 24 | 25 | private val columns = 25 26 | private val rows = 10 27 | private val bufferWidth = columns * 2 28 | private val bufferHeight = rows * 5 29 | private val imageResource = R.drawable.color_bitmap 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | val bitmap = Bitmap.createBitmap(bufferWidth, bufferHeight, Bitmap.Config.ARGB_8888) 34 | .also { Canvas(it).drawBitmap( 35 | BitmapFactory.decodeResource(resources, imageResource) 36 | .scale(bufferWidth, bufferHeight), 0f, 0f, null 37 | ) 38 | } 39 | 40 | val screenBuffer = ByteBuffer.allocate(bitmap.allocationByteCount) 41 | .also { bitmap.copyPixelsToBuffer(it) } 42 | 43 | setContent { 44 | Surface(modifier = Modifier.fillMaxSize(), color = ComposeColor.Black) { 45 | Column(modifier = Modifier.padding(24.dp)) { 46 | Image( 47 | modifier = Modifier 48 | .background(color = ComposeColor.White) 49 | .fillMaxWidth(), 50 | painter = painterResource(id = imageResource), 51 | contentDescription = "preview") 52 | Spacer(modifier = Modifier.height(24.dp)) 53 | for (row in 0 until rows) { 54 | Row { 55 | for (column in 0 until columns) { 56 | val sevenSegmentIndex = (row * columns) + column 57 | SegmentDisplay( 58 | modifier = Modifier 59 | .weight(1f) 60 | .padding(2.dp), 61 | decoder = ARGB8888BufferDecoder(screenBuffer, bufferWidth, bufferHeight, columns, rows, sevenSegmentIndex), 62 | led = { value -> ComposeColor(value) } 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | class Alpha8BufferDecoder( 74 | imageBuffer: ByteBuffer, 75 | bufferWidth: Int, 76 | bufferHeight: Int, 77 | sevenSegmentCountX: Int, 78 | sevenSegmentCountY: Int, 79 | sevenSegmentIndex: Int 80 | ) : Decoder { 81 | override val a = imageBuffer.get(0.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 82 | override val b = imageBuffer.get(1.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 83 | override val c = imageBuffer.get(2.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 84 | override val d = imageBuffer.get(3.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 85 | override val e = imageBuffer.get(4.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 86 | override val f = imageBuffer.get(5.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 87 | override val g = imageBuffer.get(6.toALPHA8BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toInt() 88 | } 89 | 90 | class ARGB8888BufferDecoder( 91 | imageBuffer: ByteBuffer, 92 | bufferWidth: Int, 93 | bufferHeight: Int, 94 | sevenSegmentCountX: Int, 95 | sevenSegmentCountY: Int, 96 | sevenSegmentIndex: Int 97 | ) : Decoder { 98 | override val a = imageBuffer.getInt(0.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 99 | override val b = imageBuffer.getInt(1.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 100 | override val c = imageBuffer.getInt(2.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 101 | override val d = imageBuffer.getInt(3.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 102 | override val e = imageBuffer.getInt(4.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 103 | override val f = imageBuffer.getInt(5.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 104 | override val g = imageBuffer.getInt(6.toARGB8888BufferIndex(bufferWidth, bufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex)).toARGB() 105 | 106 | //Pixel data read from the image buffer us stored at RGBA and must be converted to ARGB 107 | private fun Int.toARGB(): Int { 108 | return (this shl 24) or (this shr 8) 109 | } 110 | } 111 | 112 | /* 113 | Each segment is 2x5 pixels 114 | 0,0 = A 115 | 0,1 = F 116 | 1,1 = B 117 | 0,2 = G 118 | 0,3 = E 119 | 1,3 = C 120 | 0,4 = D 121 | 122 | Segment index: 123 | A = 0 124 | B = 1 125 | C = 2 126 | D = 3 127 | E = 4 128 | F = 5 129 | G = 6 130 | */ 131 | 132 | internal fun Int.toALPHA8BufferIndex( 133 | imageBufferWidth: Int, 134 | imageBufferHeight: Int, 135 | sevenSegmentCountX: Int, 136 | sevenSegmentCountY: Int, 137 | sevenSegmentIndex: Int 138 | ): Int { 139 | val segmentWidth = 2 140 | val segmentHeight = 5 141 | 142 | if(imageBufferWidth % segmentWidth > 0 || imageBufferHeight % segmentHeight > 0) throw InvalidParameterException("Image buffer must be divisible by 7-segment size 2x5") 143 | if(imageBufferWidth < sevenSegmentCountX * segmentWidth) throw InvalidParameterException("Image buffer width must be large enough to support given 7-segment grid width") 144 | if(imageBufferHeight < sevenSegmentCountY * segmentHeight) throw InvalidParameterException("Image buffer height must be large enough to support given 7-segment grid height") 145 | 146 | val segmentOffsetX = sevenSegmentIndex % sevenSegmentCountX 147 | val segmentOffsetY = sevenSegmentIndex / sevenSegmentCountX 148 | 149 | val imageOffsetX = segmentOffsetX * segmentWidth 150 | val imageOffsetY = segmentOffsetY * segmentHeight 151 | 152 | val imageCoordinate = when(this) { 153 | /*A*/ 0 -> imageOffsetX to imageOffsetY 154 | /*B*/ 1 -> imageOffsetX + 1 to imageOffsetY + 1 155 | /*C*/ 2 -> imageOffsetX + 1 to imageOffsetY + 3 156 | /*D*/ 3 -> imageOffsetX to imageOffsetY + 4 157 | /*E*/ 4 -> imageOffsetX to imageOffsetY + 3 158 | /*F*/ 5 -> imageOffsetX to imageOffsetY + 1 159 | /*G*/ 6 -> imageOffsetX to imageOffsetY + 2 160 | else -> 0 to 0 161 | } 162 | 163 | return imageCoordinate.second * imageBufferWidth + imageCoordinate.first 164 | } 165 | 166 | internal fun Int.toARGB8888BufferIndex( 167 | imageBufferWidth: Int, 168 | imageBufferHeight: Int, 169 | sevenSegmentCountX: Int, 170 | sevenSegmentCountY: Int, 171 | sevenSegmentIndex: Int 172 | ): Int { 173 | return toALPHA8BufferIndex(imageBufferWidth, imageBufferHeight, sevenSegmentCountX, sevenSegmentCountY, sevenSegmentIndex) * 4 //bytes 174 | } -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/seven/Composable.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.seven 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material.Surface 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.geometry.Offset 12 | import androidx.compose.ui.geometry.Size 13 | import androidx.compose.ui.graphics.Brush 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.Path 16 | import androidx.compose.ui.graphics.drawscope.DrawScope 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | import com.rabross.segmenteddisplay.Led 20 | import com.rabross.segmenteddisplay.SingleColorLed 21 | import com.rabross.segmenteddisplay.delimiter.Delimiter 22 | import com.rabross.segmenteddisplay.delimiter.BinaryDecoder as DelimiterBinaryDecoder 23 | 24 | @Preview 25 | @Composable 26 | fun FlatSegmentDisplayPreview() { 27 | Surface(color = Color.Black) { 28 | SegmentDisplay( 29 | decoder = BinaryDecoder(BinaryDecoder.mapToDisplay(5)), 30 | segmentStyle = SegmentStyle.FLAT 31 | ) 32 | } 33 | } 34 | 35 | @Preview 36 | @Composable 37 | fun DiffuserSegmentDisplayPreview() { 38 | Surface(color = Color.Black) { 39 | SegmentDisplay( 40 | decoder = BinaryDecoder(BinaryDecoder.mapToDisplay(5)), 41 | segmentStyle = SegmentStyle.DIFFUSER 42 | ) 43 | } 44 | } 45 | 46 | @Preview 47 | @Composable 48 | fun DigitalClockPreview() { 49 | Surface(color = Color.Black) { 50 | DigitalClock() 51 | } 52 | } 53 | 54 | @Composable 55 | fun SegmentDisplay( 56 | modifier: Modifier = Modifier, 57 | segmentScale: Int = 3, 58 | spacingRatio: Float = 0.2f, 59 | led: Led = defaultLed, 60 | segmentStyle: SegmentStyle = SegmentStyle.FLAT, 61 | decoder: Decoder = BinaryDecoder() 62 | ) { 63 | Canvas(modifier = modifier 64 | .aspectRatio((1f + segmentScale + 1f) / (1f + segmentScale + 1f + segmentScale + 1f), true) 65 | .size(100.dp)) { 66 | 67 | val scaleWidth = 1 + segmentScale + 1 68 | val scaleHeight = 1 + segmentScale + 1 + segmentScale + 1 69 | 70 | val segmentWidthByWidth = size.width / scaleWidth 71 | val segmentWidthByHeight = size.height / scaleHeight 72 | 73 | val segmentWidth = if (scaleHeight * segmentWidthByWidth < size.height) segmentWidthByWidth else segmentWidthByHeight 74 | val segmentLength = segmentScale * segmentWidth 75 | 76 | val horizontalSegmentSize = Size(segmentLength, segmentWidth) 77 | val verticalSegmentSize = Size(segmentWidth, segmentLength) 78 | 79 | val aOffset = Offset(segmentWidth, 0f) 80 | val bOffset = Offset(segmentLength + segmentWidth, segmentWidth) 81 | val cOffset = Offset(segmentLength + segmentWidth, segmentLength + segmentWidth + segmentWidth) 82 | val dOffset = Offset(segmentWidth, segmentLength + segmentWidth + segmentLength + segmentWidth) 83 | val eOffset = Offset(0f, segmentLength + segmentWidth + segmentWidth) 84 | val fOffset = Offset(0f, segmentWidth) 85 | val gOffset = Offset(segmentWidth, segmentLength + segmentWidth) 86 | 87 | val spacingRatioLimited = spacingRatio.coerceIn(0f..0.9f) 88 | 89 | drawHorizontalSegment(led.signal(decoder.a), segmentStyle, aOffset, horizontalSegmentSize, spacingRatioLimited) 90 | drawVerticalSegment(led.signal(decoder.b), segmentStyle, bOffset, verticalSegmentSize, spacingRatioLimited) 91 | drawVerticalSegment(led.signal(decoder.c), segmentStyle, cOffset, verticalSegmentSize, spacingRatioLimited) 92 | drawHorizontalSegment(led.signal(decoder.d), segmentStyle, dOffset, horizontalSegmentSize, spacingRatioLimited) 93 | drawVerticalSegment(led.signal(decoder.e), segmentStyle, eOffset, verticalSegmentSize, spacingRatioLimited) 94 | drawVerticalSegment(led.signal(decoder.f), segmentStyle, fOffset, verticalSegmentSize, spacingRatioLimited) 95 | drawHorizontalSegment(led.signal(decoder.g), segmentStyle, gOffset, horizontalSegmentSize, spacingRatioLimited) 96 | } 97 | } 98 | 99 | private fun DrawScope.drawHorizontalSegment(color: Color, segmentStyle: SegmentStyle, offset: Offset, size: Size, spacingRatio: Float) { 100 | val radius = size.minDimension / 2 101 | val centerX: Float = offset.x + size.width / 2 102 | val centerY: Float = offset.y + size.height / 2 103 | val spacing = radius * spacingRatio 104 | val hexagonPath = Path().apply { 105 | moveTo(centerX, centerY + radius - spacing) 106 | lineTo(centerX - size.width/2, centerY + radius - spacing) 107 | lineTo(centerX - size.width/2 - radius + spacing, centerY) 108 | lineTo(centerX - size.width/2, centerY - radius + spacing) 109 | lineTo(centerX + size.width/2, centerY - radius + spacing) 110 | lineTo(centerX + size.width/2 + radius - spacing, centerY) 111 | lineTo(centerX + size.width/2, centerY + radius - spacing) 112 | } 113 | when(segmentStyle) { 114 | SegmentStyle.FLAT -> drawPath(hexagonPath, color) 115 | SegmentStyle.DIFFUSER -> drawPath(hexagonPath, color.toLedBrush(Offset(centerX, centerY))) 116 | } 117 | } 118 | 119 | private fun DrawScope.drawVerticalSegment(color: Color, segmentStyle: SegmentStyle, offset: Offset, size: Size, spacingRatio: Float) { 120 | val radius = size.minDimension / 2 121 | val centerX: Float = offset.x + size.width / 2 122 | val centerY: Float = offset.y + size.height / 2 123 | val spacing = radius * spacingRatio 124 | val hexagonPath = Path().apply { 125 | moveTo(centerX, centerY + radius + size.height/2 - spacing) 126 | lineTo(centerX - radius + spacing, centerY + size.height/2) 127 | lineTo(centerX - radius + spacing, centerY - size.height/2) 128 | lineTo(centerX, centerY - radius - size.height/2 + spacing) 129 | lineTo(centerX + radius - spacing, centerY - size.height/2) 130 | lineTo(centerX + radius - spacing, centerY + size.height/2) 131 | } 132 | when(segmentStyle) { 133 | SegmentStyle.FLAT -> drawPath(hexagonPath, color) 134 | SegmentStyle.DIFFUSER -> drawPath(hexagonPath, color.toLedBrush(Offset(centerX, centerY))) 135 | } 136 | } 137 | 138 | @Composable 139 | fun DigitalClock( 140 | modifier: Modifier = Modifier, 141 | hourFirst: Int = 0, 142 | hourSecond: Int = 0, 143 | minuteFirst: Int = 0, 144 | minuteSecond: Int = 0, 145 | secondFirst: Int = 0, 146 | secondSecond: Int = 0, 147 | delimiterSignal: Int = 0 148 | ) { 149 | Row(modifier = modifier) { 150 | SegmentDisplay( 151 | modifier = Modifier 152 | .weight(1f) 153 | .padding(4.dp), 154 | decoder = BinaryDecoder(hourFirst), 155 | segmentStyle = SegmentStyle.DIFFUSER 156 | ) 157 | SegmentDisplay( 158 | modifier = Modifier 159 | .weight(1f) 160 | .padding(4.dp), 161 | decoder = BinaryDecoder(hourSecond), 162 | segmentStyle = SegmentStyle.DIFFUSER 163 | ) 164 | Delimiter(modifier = Modifier 165 | .weight(1f) 166 | .padding(4.dp), 167 | decoder = DelimiterBinaryDecoder(delimiterSignal) 168 | ) 169 | SegmentDisplay( 170 | modifier = Modifier 171 | .weight(1f) 172 | .padding(4.dp), 173 | decoder = BinaryDecoder(minuteFirst), 174 | segmentStyle = SegmentStyle.DIFFUSER 175 | ) 176 | SegmentDisplay( 177 | modifier = Modifier 178 | .weight(1f) 179 | .padding(4.dp), 180 | decoder = BinaryDecoder(minuteSecond), 181 | segmentStyle = SegmentStyle.DIFFUSER 182 | ) 183 | Delimiter(modifier = Modifier 184 | .weight(1f) 185 | .padding(4.dp), 186 | decoder = DelimiterBinaryDecoder(delimiterSignal) 187 | ) 188 | SegmentDisplay( 189 | modifier = Modifier 190 | .weight(1f) 191 | .padding(4.dp), 192 | decoder = BinaryDecoder(secondFirst), 193 | segmentStyle = SegmentStyle.DIFFUSER 194 | ) 195 | SegmentDisplay( 196 | modifier = Modifier 197 | .weight(1f) 198 | .padding(4.dp), 199 | decoder = BinaryDecoder(secondSecond), 200 | segmentStyle = SegmentStyle.DIFFUSER 201 | ) 202 | } 203 | } 204 | 205 | private val defaultLed = SingleColorLed(Color.Red, Color.DarkGray.copy(alpha = 0.3f)) 206 | 207 | private const val diffuserFactor = 0.4f 208 | 209 | private fun Color.toLedBrush(offset: Offset) = Brush.radialGradient( 210 | colors = listOf( 211 | this, copy( 212 | red = red * diffuserFactor, 213 | green = green * diffuserFactor, 214 | blue = blue * diffuserFactor 215 | ) 216 | ), 217 | center = offset 218 | ) -------------------------------------------------------------------------------- /app/src/test/java/com/rabross/segmenteddisplay/app/ByteBufferSevenSegmentIndexMapperTests.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.app 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.FixMethodOrder 5 | import org.junit.Test 6 | import org.junit.runners.MethodSorters 7 | import java.security.InvalidParameterException 8 | 9 | @FixMethodOrder(MethodSorters.JVM) 10 | internal class ByteBufferSevenSegmentIndexMapperTests { 11 | 12 | private val minBufferWidth = 2 13 | private val minBufferHeight = 5 14 | private val minSevenSegmentCountX = 1 15 | private val minSevenSegmentCountY = 1 16 | private val minSevenSegmentIndex = 0 17 | 18 | @Test 19 | internal fun `Minimum - Segment A`() { 20 | val segmentAIndex = 0 21 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 22 | minBufferWidth, 23 | minBufferHeight, 24 | minSevenSegmentCountX, 25 | minSevenSegmentCountY, 26 | minSevenSegmentIndex, 27 | segmentAIndex 28 | ) 29 | assertEquals(0, bufferIndex) 30 | } 31 | 32 | @Test 33 | internal fun `Minimum - Segment B`() { 34 | val segmentBIndex = 1 35 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 36 | minBufferWidth, 37 | minBufferHeight, 38 | minSevenSegmentCountX, 39 | minSevenSegmentCountY, 40 | minSevenSegmentIndex, 41 | segmentBIndex 42 | ) 43 | assertEquals(3, bufferIndex) 44 | } 45 | 46 | @Test 47 | internal fun `Minimum - Segment C`() { 48 | val segmentCIndex = 2 49 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 50 | minBufferWidth, 51 | minBufferHeight, 52 | minSevenSegmentCountX, 53 | minSevenSegmentCountY, 54 | minSevenSegmentIndex, 55 | segmentCIndex 56 | ) 57 | assertEquals(7, bufferIndex) 58 | } 59 | 60 | @Test 61 | internal fun `Minimum - Segment D`() { 62 | val segmentDIndex = 3 63 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 64 | minBufferWidth, 65 | minBufferHeight, 66 | minSevenSegmentCountX, 67 | minSevenSegmentCountY, 68 | minSevenSegmentIndex, 69 | segmentDIndex 70 | ) 71 | assertEquals(8, bufferIndex) 72 | } 73 | 74 | @Test 75 | internal fun `Minimum - Segment E`() { 76 | val segmentEIndex = 4 77 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 78 | minBufferWidth, 79 | minBufferHeight, 80 | minSevenSegmentCountX, 81 | minSevenSegmentCountY, 82 | minSevenSegmentIndex, 83 | segmentEIndex 84 | ) 85 | assertEquals(6, bufferIndex) 86 | } 87 | 88 | @Test 89 | internal fun `Minimum - Segment F`() { 90 | val segmentFIndex = 5 91 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 92 | minBufferWidth, 93 | minBufferHeight, 94 | minSevenSegmentCountX, 95 | minSevenSegmentCountY, 96 | minSevenSegmentIndex, 97 | segmentFIndex 98 | ) 99 | assertEquals(2, bufferIndex) 100 | } 101 | 102 | @Test 103 | internal fun `Minimum - Segment G`() { 104 | val segmentGIndex = 6 105 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 106 | minBufferWidth, 107 | minBufferHeight, 108 | minSevenSegmentCountX, 109 | minSevenSegmentCountY, 110 | minSevenSegmentIndex, 111 | segmentGIndex 112 | ) 113 | assertEquals(4, bufferIndex) 114 | } 115 | 116 | @Test 117 | internal fun `Square Grid - Index 0 Segment A`() { 118 | val sevenSegmentCountX = 2 119 | val sevenSegmentCountY = 2 120 | val bufferWidth = 2 * sevenSegmentCountX 121 | val bufferHeight = 5 * sevenSegmentCountY 122 | val sevenSegmentIndex = 0 123 | val segmentIndex = 0 124 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 125 | bufferWidth, 126 | bufferHeight, 127 | sevenSegmentCountX, 128 | sevenSegmentCountY, 129 | sevenSegmentIndex, 130 | segmentIndex 131 | ) 132 | assertEquals(0, bufferIndex) 133 | } 134 | 135 | @Test 136 | internal fun `Square Grid - Index 1 Segment A`() { 137 | val sevenSegmentCountX = 2 138 | val sevenSegmentCountY = 2 139 | val bufferWidth = 2 * sevenSegmentCountX 140 | val bufferHeight = 5 * sevenSegmentCountY 141 | val sevenSegmentIndex = 1 142 | val segmentIndex = 0 143 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 144 | bufferWidth, 145 | bufferHeight, 146 | sevenSegmentCountX, 147 | sevenSegmentCountY, 148 | sevenSegmentIndex, 149 | segmentIndex 150 | ) 151 | assertEquals(2, bufferIndex) 152 | } 153 | 154 | @Test 155 | internal fun `Square Grid - Index 2 Segment A`() { 156 | val sevenSegmentCountX = 2 157 | val sevenSegmentCountY = 2 158 | val bufferWidth = 2 * sevenSegmentCountX 159 | val bufferHeight = 5 * sevenSegmentCountY 160 | val sevenSegmentIndex = 2 161 | val segmentIndex = 0 162 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 163 | bufferWidth, 164 | bufferHeight, 165 | sevenSegmentCountX, 166 | sevenSegmentCountY, 167 | sevenSegmentIndex, 168 | segmentIndex 169 | ) 170 | assertEquals(20, bufferIndex) 171 | } 172 | 173 | @Test 174 | internal fun `Square Grid - Index 3 Segment A`() { 175 | val sevenSegmentCountX = 2 176 | val sevenSegmentCountY = 2 177 | val bufferWidth = 2 * sevenSegmentCountX 178 | val bufferHeight = 5 * sevenSegmentCountY 179 | val sevenSegmentIndex = 3 180 | val segmentIndex = 0 181 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 182 | bufferWidth, 183 | bufferHeight, 184 | sevenSegmentCountX, 185 | sevenSegmentCountY, 186 | sevenSegmentIndex, 187 | segmentIndex 188 | ) 189 | assertEquals(22, bufferIndex) 190 | } 191 | 192 | @Test 193 | internal fun `Square Grid - Index 0 Segment B`() { 194 | val sevenSegmentCountX = 2 195 | val sevenSegmentCountY = 2 196 | val bufferWidth = 2 * sevenSegmentCountX 197 | val bufferHeight = 5 * sevenSegmentCountY 198 | val sevenSegmentIndex = 0 199 | val segmentIndex = 1 200 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 201 | bufferWidth, 202 | bufferHeight, 203 | sevenSegmentCountX, 204 | sevenSegmentCountY, 205 | sevenSegmentIndex, 206 | segmentIndex 207 | ) 208 | assertEquals(5, bufferIndex) 209 | } 210 | 211 | @Test 212 | internal fun `Square Grid - Index 1 Segment B`() { 213 | val sevenSegmentCountX = 2 214 | val sevenSegmentCountY = 2 215 | val bufferWidth = 2 * sevenSegmentCountX 216 | val bufferHeight = 5 * sevenSegmentCountY 217 | val sevenSegmentIndex = 1 218 | val segmentIndex = 1 219 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 220 | bufferWidth, 221 | bufferHeight, 222 | sevenSegmentCountX, 223 | sevenSegmentCountY, 224 | sevenSegmentIndex, 225 | segmentIndex 226 | ) 227 | assertEquals(7, bufferIndex) 228 | } 229 | 230 | @Test 231 | internal fun `Square Grid - Index 2 Segment B`() { 232 | val sevenSegmentCountX = 2 233 | val sevenSegmentCountY = 2 234 | val bufferWidth = 2 * sevenSegmentCountX 235 | val bufferHeight = 5 * sevenSegmentCountY 236 | val sevenSegmentIndex = 2 237 | val segmentIndex = 1 238 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 239 | bufferWidth, 240 | bufferHeight, 241 | sevenSegmentCountX, 242 | sevenSegmentCountY, 243 | sevenSegmentIndex, 244 | segmentIndex 245 | ) 246 | assertEquals(25, bufferIndex) 247 | } 248 | 249 | @Test 250 | internal fun `Square Grid - Index 3 Segment B`() { 251 | val sevenSegmentCountX = 2 252 | val sevenSegmentCountY = 2 253 | val bufferWidth = 2 * sevenSegmentCountX 254 | val bufferHeight = 5 * sevenSegmentCountY 255 | val sevenSegmentIndex = 3 256 | val segmentIndex = 1 257 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 258 | bufferWidth, 259 | bufferHeight, 260 | sevenSegmentCountX, 261 | sevenSegmentCountY, 262 | sevenSegmentIndex, 263 | segmentIndex 264 | ) 265 | assertEquals(27, bufferIndex) 266 | } 267 | 268 | @Test 269 | internal fun `Rectangle Grid - Index 3 Segment 0`() { 270 | val sevenSegmentCountX = 5 271 | val sevenSegmentCountY = 3 272 | val bufferWidth = 2 * sevenSegmentCountX 273 | val bufferHeight = 5 * sevenSegmentCountY 274 | val sevenSegmentIndex = 3 275 | val segmentIndex = 0 276 | val bufferIndex = mapSingleSegmentToALPHA8BufferIndex( 277 | bufferWidth, 278 | bufferHeight, 279 | sevenSegmentCountX, 280 | sevenSegmentCountY, 281 | sevenSegmentIndex, 282 | segmentIndex 283 | ) 284 | assertEquals(6, bufferIndex) 285 | } 286 | 287 | @Test(expected = InvalidParameterException::class) 288 | internal fun `Invalid buffer format throws error`() { 289 | mapSingleSegmentToALPHA8BufferIndex(3,5,1,1,0,0) 290 | } 291 | 292 | @Test(expected = InvalidParameterException::class) 293 | internal fun `Invalid buffer width throws error`() { 294 | mapSingleSegmentToALPHA8BufferIndex(2,5,2,1,0,0) 295 | } 296 | 297 | @Test(expected = InvalidParameterException::class) 298 | internal fun `Invalid buffer height throws error`() { 299 | mapSingleSegmentToALPHA8BufferIndex(2,5,1,2,0,0) 300 | } 301 | } -------------------------------------------------------------------------------- /segmenteddisplay/src/main/java/com/rabross/segmenteddisplay/fourteen/Composable.kt: -------------------------------------------------------------------------------- 1 | package com.rabross.segmenteddisplay.fourteen 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.geometry.Size 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.Path 12 | import androidx.compose.ui.graphics.drawscope.DrawScope 13 | import androidx.compose.ui.tooling.preview.Preview 14 | import androidx.compose.ui.unit.dp 15 | import com.rabross.segmenteddisplay.delimiter.BinaryDecoder as DelimiterBinaryDecoder 16 | import com.rabross.segmenteddisplay.Led 17 | import com.rabross.segmenteddisplay.SingleColorLed 18 | import com.rabross.segmenteddisplay.delimiter.Delimiter 19 | import kotlin.math.pow 20 | import kotlin.math.sqrt 21 | 22 | @Preview 23 | @Composable 24 | fun FourteenSegmentDisplayPreview() { 25 | Surface { 26 | SegmentDisplay() 27 | } 28 | } 29 | 30 | @Composable 31 | fun DigitalClock( 32 | modifier: Modifier = Modifier, 33 | hourFirst: Int = 0, 34 | hourSecond: Int = 0, 35 | minuteFirst: Int = 0, 36 | minuteSecond: Int = 0, 37 | secondFirst: Int = 0, 38 | secondSecond: Int = 0, 39 | delimiterSignal: Int = 0 40 | ) { 41 | Row(modifier = modifier) { 42 | SegmentDisplay(modifier = modifier.padding(4.dp), 43 | decoder = BinaryDecoder(hourFirst) 44 | ) 45 | SegmentDisplay(modifier = modifier.padding(4.dp), 46 | decoder = BinaryDecoder(hourSecond) 47 | ) 48 | Delimiter(modifier = modifier.padding(4.dp), 49 | decoder = DelimiterBinaryDecoder(delimiterSignal) 50 | ) 51 | SegmentDisplay(modifier = modifier.padding(4.dp), 52 | decoder = BinaryDecoder(minuteFirst) 53 | ) 54 | SegmentDisplay(modifier = modifier.padding(4.dp), 55 | decoder = BinaryDecoder(minuteSecond) 56 | ) 57 | Delimiter(modifier = modifier.padding(4.dp), 58 | decoder = DelimiterBinaryDecoder(delimiterSignal) 59 | ) 60 | SegmentDisplay(modifier = modifier.padding(4.dp), 61 | decoder = BinaryDecoder(secondFirst) 62 | ) 63 | SegmentDisplay(modifier = modifier.padding(4.dp), 64 | decoder = BinaryDecoder(secondSecond) 65 | ) 66 | } 67 | } 68 | 69 | @Composable 70 | fun SegmentDisplay( 71 | modifier: Modifier = Modifier, 72 | segmentScale: Int = 4, 73 | led: Led = SingleColorLed(Color.Red, Color.DarkGray.copy(alpha = 0.4f)), 74 | decoder: Decoder = BinaryDecoder() 75 | ) { 76 | Canvas(modifier = modifier 77 | .aspectRatio((1f + segmentScale + 1f) / (1f + segmentScale + 1f + segmentScale + 1f), true) 78 | .size(100.dp)) { 79 | 80 | val scaleWidth = 1 + segmentScale + 1 81 | val scaleHeight = 1 + segmentScale + 1 + segmentScale + 1 82 | 83 | val segmentWidthByWidth = size.width / scaleWidth 84 | val segmentWidthByHeight = size.height / scaleHeight 85 | 86 | val segmentWidth = if (scaleHeight * segmentWidthByWidth < size.height) segmentWidthByWidth else segmentWidthByHeight 87 | val segmentLength = segmentScale * segmentWidth 88 | 89 | val fullWidth = segmentWidth * scaleWidth 90 | val quarterGapWidth = (fullWidth - segmentWidth * 3)/2 91 | 92 | val horizontalSegmentSize = Size(segmentLength, segmentWidth) 93 | val smallHorizontalSegmentSize = Size(segmentLength / 2, segmentWidth) 94 | val verticalSegmentSize = Size(segmentWidth, segmentLength) 95 | val smallVerticalSegmentSize = Size(segmentWidth, segmentLength - segmentWidth/2) 96 | val diagonalSegmentSize = Size(quarterGapWidth, segmentLength) 97 | 98 | val aOffset = Offset(segmentWidth, 0f) 99 | val bOffset = Offset(segmentLength + segmentWidth, segmentWidth) 100 | val cOffset = Offset(segmentLength + segmentWidth, segmentLength + segmentWidth * 2) 101 | val dOffset = Offset(segmentWidth, segmentLength + segmentWidth + segmentLength + segmentWidth) 102 | val eOffset = Offset(0f, segmentLength + segmentWidth * 2) 103 | val fOffset = Offset(0f, segmentWidth) 104 | val g1Offset = Offset(segmentWidth, segmentLength + segmentWidth) 105 | val g2Offset = Offset(quarterGapWidth + segmentWidth * 2, segmentLength + segmentWidth) 106 | val hOffset = Offset(segmentWidth, segmentWidth) 107 | val iOffset = Offset(quarterGapWidth + segmentWidth, segmentWidth + segmentWidth / 2) 108 | val jOffset = Offset(quarterGapWidth + segmentWidth * 2, segmentWidth) 109 | val kOffset = Offset(segmentWidth, segmentLength + segmentWidth * 2) 110 | val lOffset = Offset(quarterGapWidth + segmentWidth, segmentLength + segmentWidth * 2) 111 | val mOffset = Offset(quarterGapWidth + segmentWidth * 2, segmentLength + segmentWidth * 2) 112 | 113 | drawSegmentA(led.signal(decoder.a), aOffset, horizontalSegmentSize) 114 | drawSegmentB(led.signal(decoder.b), bOffset, verticalSegmentSize) 115 | drawSegmentC(led.signal(decoder.c), cOffset, verticalSegmentSize) 116 | drawSegmentD(led.signal(decoder.d), dOffset, horizontalSegmentSize) 117 | drawSegmentE(led.signal(decoder.e), eOffset, verticalSegmentSize) 118 | drawSegmentF(led.signal(decoder.f), fOffset, verticalSegmentSize) 119 | drawSegmentG1(led.signal(decoder.g1), g1Offset, smallHorizontalSegmentSize) 120 | drawSegmentG2(led.signal(decoder.g2), g2Offset, smallHorizontalSegmentSize) 121 | drawSegmentH(led.signal(decoder.h), segmentWidth, hOffset, diagonalSegmentSize) 122 | drawSegmentI(led.signal(decoder.i), iOffset, smallVerticalSegmentSize) 123 | drawSegmentJ(led.signal(decoder.j), segmentWidth, jOffset, diagonalSegmentSize) 124 | drawSegmentK(led.signal(decoder.k), segmentWidth, kOffset, diagonalSegmentSize) 125 | drawSegmentL(led.signal(decoder.l), lOffset, smallVerticalSegmentSize) 126 | drawSegmentM(led.signal(decoder.m), segmentWidth, mOffset, diagonalSegmentSize) 127 | } 128 | } 129 | 130 | private fun DrawScope.drawSegmentA(c: Color, o: Offset, s: Size) = drawHorizontalSegment(c, o, s) 131 | private fun DrawScope.drawSegmentB(c: Color, o: Offset, s: Size) = drawVerticalSegment(c, o, s) 132 | private fun DrawScope.drawSegmentC(c: Color, o: Offset, s: Size) = drawVerticalSegment(c, o, s) 133 | private fun DrawScope.drawSegmentD(c: Color, o: Offset, s: Size) = drawHorizontalSegment(c, o, s) 134 | private fun DrawScope.drawSegmentE(c: Color, o: Offset, s: Size) = drawVerticalSegment(c, o, s) 135 | private fun DrawScope.drawSegmentF(c: Color, o: Offset, s: Size) = drawVerticalSegment(c, o, s) 136 | private fun DrawScope.drawSegmentG1(c: Color, o: Offset, s: Size) = drawSmallHorizontalSegment(c, o, s) 137 | private fun DrawScope.drawSegmentG2(c: Color, o: Offset, s: Size) = drawSmallHorizontalSegment(c, o, s) 138 | private fun DrawScope.drawSegmentH(c: Color, w: Float, o: Offset, s: Size) = drawBackSlashSegment(c, w, o, s) 139 | private fun DrawScope.drawSegmentI(c: Color, o: Offset, s: Size) = drawSmallVerticalSegment(c, o, s) 140 | private fun DrawScope.drawSegmentJ(c: Color, w: Float, o: Offset, s: Size) = drawForwardSlashSegment(c, w, o, s) 141 | private fun DrawScope.drawSegmentK(c: Color, w: Float, o: Offset, s: Size) = drawForwardSlashSegment(c, w, o, s) 142 | private fun DrawScope.drawSegmentL(c: Color, o: Offset, s: Size) = drawSmallVerticalSegment(c, o, s) 143 | private fun DrawScope.drawSegmentM(c: Color, w: Float, o: Offset, s: Size) = drawBackSlashSegment(c, w, o, s) 144 | 145 | private fun DrawScope.drawHorizontalSegment(color: Color, offset: Offset, size: Size) { 146 | val radius = size.minDimension / 2 147 | val centerX = offset.x + size.width / 2 148 | val centerY = offset.y + size.height / 2 149 | val spacing = radius / 10 150 | 151 | val hexagonPath = Path().apply { 152 | moveTo(centerX - size.width/2, centerY + radius - spacing) 153 | lineTo(centerX - size.width/2 - radius + spacing, centerY) 154 | lineTo(centerX - size.width/2, centerY - radius + spacing) 155 | lineTo(centerX + size.width/2, centerY - radius + spacing) 156 | lineTo(centerX + size.width/2 + radius - spacing, centerY) 157 | lineTo(centerX + size.width/2, centerY + radius - spacing) 158 | } 159 | 160 | drawPath(hexagonPath, color) 161 | } 162 | 163 | private fun DrawScope.drawVerticalSegment(color: Color, offset: Offset, size: Size) { 164 | val radius = size.minDimension / 2 165 | val centerX = offset.x + size.width / 2 166 | val centerY = offset.y + size.height / 2 167 | val spacing = radius / 10 168 | 169 | val hexagonPath = Path().apply { 170 | moveTo(centerX, centerY + radius + size.height / 2 - spacing) 171 | lineTo(centerX - radius + spacing, centerY + size.height / 2) 172 | lineTo(centerX - radius + spacing, centerY - size.height / 2) 173 | lineTo(centerX, centerY - radius - size.height / 2 + spacing) 174 | lineTo(centerX + radius - spacing, centerY - size.height / 2) 175 | lineTo(centerX + radius - spacing, centerY + size.height / 2) 176 | } 177 | 178 | drawPath(hexagonPath, color) 179 | } 180 | 181 | private fun DrawScope.drawSmallHorizontalSegment(color: Color, offset: Offset, size: Size) { 182 | val radius = size.minDimension / 2 183 | val centerX = offset.x + size.width / 2 184 | val centerY = offset.y + size.height / 2 185 | val spacing = radius / 10 186 | 187 | val hexagonPath = Path().apply { 188 | moveTo(centerX - size.width / 2, centerY + radius - spacing) 189 | lineTo(centerX - size.width / 2 - radius + spacing, centerY) 190 | lineTo(centerX - size.width / 2, centerY - radius + spacing) 191 | lineTo(centerX + size.width / 2 - radius, centerY - radius + spacing) 192 | lineTo(centerX + size.width / 2 - spacing, centerY) 193 | lineTo(centerX + size.width / 2 - radius, centerY + radius - spacing) 194 | } 195 | 196 | drawPath(hexagonPath, color) 197 | } 198 | 199 | private fun DrawScope.drawSmallVerticalSegment(color: Color, offset: Offset, size: Size) { 200 | val radius = size.minDimension / 2 201 | val centerX = offset.x + size.width / 2 202 | val centerY = offset.y + size.height / 2 203 | val spacing = radius / 10 204 | 205 | val hexagonPath = Path().apply { 206 | moveTo(centerX, centerY + radius + size.height / 2 - spacing) 207 | lineTo(centerX - radius + spacing, centerY + size.height / 2) 208 | lineTo(centerX - radius + spacing, centerY - size.height / 2) 209 | lineTo(centerX, centerY - radius - size.height / 2 + spacing) 210 | lineTo(centerX + radius - spacing, centerY - size.height / 2) 211 | lineTo(centerX + radius - spacing, centerY + size.height / 2) 212 | } 213 | 214 | drawPath(hexagonPath, color) 215 | } 216 | 217 | private fun DrawScope.drawBackSlashSegment(color: Color, width: Float, offset: Offset, size: Size) { 218 | val diagonalWidth = sqrt(width.pow(2)/2) * 2 219 | val spacing = diagonalWidth / 40 220 | 221 | val parallelogram = Path().apply { 222 | moveTo(offset.x + spacing, offset.y + spacing) 223 | lineTo(offset.x + size.width - spacing, offset.y + size.height - diagonalWidth) 224 | lineTo(offset.x + size.width - spacing, offset.y + size.height - spacing) 225 | lineTo(offset.x + spacing, offset.y + diagonalWidth) 226 | } 227 | 228 | drawPath(parallelogram, color) 229 | } 230 | 231 | private fun DrawScope.drawForwardSlashSegment(color: Color, width: Float, offset: Offset, size: Size) { 232 | val diagonalWidth = sqrt(width.pow(2)/2) * 2 233 | val spacing = diagonalWidth / 40 234 | 235 | val parallelogram = Path().apply { 236 | moveTo(offset.x + spacing, offset.y + size.height - spacing) 237 | lineTo(offset.x + spacing, offset.y + size.height - diagonalWidth) 238 | lineTo(offset.x + size.width - spacing, offset.y + spacing) 239 | lineTo(offset.x + size.width - spacing, offset.y + diagonalWidth) 240 | } 241 | 242 | drawPath(parallelogram, color) 243 | } 244 | --------------------------------------------------------------------------------