├── .gitattributes
├── .github
└── workflows
│ ├── build_workflow.main.kts
│ ├── build_workflow.yaml
│ ├── fix_workflow.main.kts
│ └── fix_workflow.yaml
├── .gitignore
├── .idea
├── .gitignore
├── .name
└── runConfigurations
│ └── app_watch__installDebug_.xml
├── .screenshots
└── kotlin-and-compose-fan-clock.png
├── LICENSE
├── README.md
├── app-phone
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── louiscad
│ │ │ └── composeoclockplayground
│ │ │ ├── MainActivity.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.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
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── louiscad
│ └── composeoclockplayground
│ └── ExampleUnitTest.kt
├── app-watch
├── .gitignore
├── build.gradle.kts
├── lint.xml
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ ├── MainActivity.kt
│ ├── SampleWatchFaceService.kt
│ ├── WatchFaceConfigActivity.kt
│ ├── editor
│ │ ├── WatchFaceConfigContent.kt
│ │ └── WatchFaceEditorSession.kt
│ └── presentation
│ │ └── theme
│ │ └── Theme.kt
│ └── res
│ ├── drawable
│ ├── splash_icon.xml
│ └── watch_preview.png
│ ├── mipmap-hdpi
│ └── ic_launcher.webp
│ ├── mipmap-mdpi
│ └── ic_launcher.webp
│ ├── mipmap-xhdpi
│ └── ic_launcher.webp
│ ├── mipmap-xxhdpi
│ └── ic_launcher.webp
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.webp
│ ├── values-round
│ └── strings.xml
│ ├── values
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── watch_face.xml
├── build.gradle.kts
├── convention-plugins
├── .gitignore
├── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── Versioning.kt
│ ├── android-app.gradle.kts
│ ├── android-crashlytics.gradle.kts
│ ├── android-lib.gradle.kts
│ ├── android-signing-config.gradle.kts
│ ├── version-code-phone.gradle.kts
│ └── version-code-watch.gradle.kts
├── debug.keystore
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── releaseBuildNumber.txt
├── settings.gradle.kts
├── shared
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── louiscad
│ │ └── shared
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── kotlin
│ │ ├── ComposeOClockWatermark.kt
│ │ ├── ForPaper.kt
│ │ ├── GoogleFonts.kt
│ │ ├── WatchFacePreview.kt
│ │ ├── cleanthisbeforerelease
│ │ │ ├── complications
│ │ │ │ └── PixelWatchStyleComplication.kt
│ │ │ ├── elements
│ │ │ │ ├── CirclePatterns.kt
│ │ │ │ ├── LinePatterns.kt
│ │ │ │ ├── PatternHelpers.kt
│ │ │ │ └── PatternsPreviews.kt
│ │ │ └── experiments
│ │ │ │ ├── CirclesExperiment.kt
│ │ │ │ ├── KotlinLogoExperiments.kt
│ │ │ │ ├── ShadersExperiments.kt
│ │ │ │ └── TextOnPathExperiments.kt
│ │ ├── elements
│ │ │ ├── ClockHand.kt
│ │ │ ├── FatHourDigits.kt
│ │ │ ├── HourDigitsHelpers.kt
│ │ │ ├── LoopingSeconds.kt
│ │ │ ├── MyPath.kt
│ │ │ ├── SinusoidalCrown.kt
│ │ │ └── vectors
│ │ │ │ ├── composeMultiplatformLogo.kt
│ │ │ │ └── wearOsLogo.kt
│ │ ├── extensions
│ │ │ ├── AndroidTextPaint.kt
│ │ │ ├── Colors.kt
│ │ │ ├── DrawScope.kt
│ │ │ ├── DrawTransform.kt
│ │ │ ├── GraphicsLayer.kt
│ │ │ ├── Math.kt
│ │ │ ├── Offset.kt
│ │ │ ├── Paint.kt
│ │ │ ├── Path.kt
│ │ │ ├── PathPattern.kt
│ │ │ ├── PatternsMath.kt
│ │ │ ├── Size.kt
│ │ │ ├── SizeDependentState.kt
│ │ │ ├── TextLayoutResult.kt
│ │ │ └── text
│ │ │ │ ├── TextOnPathLayoutResult.kt
│ │ │ │ ├── TextOnPathMeasurer.kt
│ │ │ │ └── TextOnPathMeasurerHelper.kt
│ │ ├── utils
│ │ │ ├── Bits.kt
│ │ │ ├── FiveMinutesLayoutOrder.kt
│ │ │ ├── FiveMinutesSlicePattern.kt
│ │ │ └── PieSlicePath.kt
│ │ └── watchfaces
│ │ │ ├── AllWatchFaces.kt
│ │ │ ├── BasicAnalogClock.kt
│ │ │ ├── ComposeFanClock.kt
│ │ │ ├── KotlinFanClock.kt
│ │ │ ├── LightLinesWatchFace.kt
│ │ │ ├── WatchFaceSwitcher.kt
│ │ │ └── hansie
│ │ │ └── HansieClock.kt
│ └── res
│ │ ├── drawable
│ │ └── jetpack_compose.xml
│ │ ├── font
│ │ └── outfit_extrabold.ttf
│ │ └── values
│ │ └── font_certs.xml
│ └── test
│ ├── kotlin
│ └── org
│ │ └── splitties
│ │ └── compose
│ │ └── oclock
│ │ └── sample
│ │ └── watchfaces
│ │ ├── BasicAnalogClockTest.kt
│ │ ├── ClockScreenshotTest.kt
│ │ ├── ComposeFanClockTest.kt
│ │ ├── DeviceClockScreenshotTest.kt
│ │ ├── Devices.kt
│ │ └── KotlinFanClockTest.kt
│ └── screenshots
│ ├── BasicAnalogClockTest_galaxy_watch_5.png
│ ├── BasicAnalogClockTest_galaxy_watch_5_ambient.png
│ ├── BasicAnalogClockTest_pixelwatch.png
│ ├── BasicAnalogClockTest_pixelwatch_ambient.png
│ ├── BasicAnalogClockTest_ticwatch_pro_5.png
│ ├── BasicAnalogClockTest_ticwatch_pro_5_ambient.png
│ ├── ComposeFanClockTest_galaxy_watch_5.png
│ ├── ComposeFanClockTest_galaxy_watch_5_ambient.png
│ ├── ComposeFanClockTest_pixelwatch.png
│ ├── ComposeFanClockTest_pixelwatch_ambient.png
│ ├── ComposeFanClockTest_ticwatch_pro_5.png
│ ├── ComposeFanClockTest_ticwatch_pro_5_ambient.png
│ ├── KotlinFanClockTest_galaxy_watch_5.png
│ ├── KotlinFanClockTest_galaxy_watch_5_ambient.png
│ ├── KotlinFanClockTest_pixelwatch.png
│ ├── KotlinFanClockTest_pixelwatch_ambient.png
│ ├── KotlinFanClockTest_ticwatch_pro_5.png
│ └── KotlinFanClockTest_ticwatch_pro_5_ambient.png
├── version.txt
└── versions.properties
/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # These are explicitly windows files and should use crlf
5 | *.bat text eol=crlf
6 |
7 |
--------------------------------------------------------------------------------
/.github/workflows/build_workflow.main.kts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env kotlin
2 |
3 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:1.11.0")
4 |
5 | import io.github.typesafegithub.workflows.actions.actions.CheckoutV4
6 | import io.github.typesafegithub.workflows.actions.actions.SetupJavaV4
7 | import io.github.typesafegithub.workflows.actions.actions.UploadArtifactV4
8 | import io.github.typesafegithub.workflows.actions.gradle.GradleBuildActionV3
9 | import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest
10 | import io.github.typesafegithub.workflows.domain.triggers.PullRequest
11 | import io.github.typesafegithub.workflows.domain.triggers.Push
12 | import io.github.typesafegithub.workflows.dsl.expressions.expr
13 | import io.github.typesafegithub.workflows.dsl.workflow
14 | import io.github.typesafegithub.workflows.yaml.writeToFile
15 |
16 | workflow(
17 | name = "Build workflow",
18 | on = listOf(
19 | Push(branches = listOf("main")),
20 | PullRequest(),
21 | ),
22 | sourceFile = __FILE__.toPath(),
23 | ) {
24 | job(
25 | id = "build-and-test",
26 | runsOn = UbuntuLatest
27 | ) {
28 | uses(name = "Check out", action = CheckoutV4())
29 | uses(
30 | name = "Setup Java",
31 | action = SetupJavaV4(
32 | distribution = SetupJavaV4.Distribution.Temurin,
33 | javaVersion = "17"
34 | )
35 | )
36 | uses(
37 | name = "Build",
38 | action = GradleBuildActionV3(
39 | arguments = "test",
40 | )
41 | )
42 | uses(
43 | name = "Upload reports",
44 | action = UploadArtifactV4(
45 | name = "Roborazzi",
46 | path = listOf("shared/build/outputs/roborazzi"),
47 | retentionDays = UploadArtifactV4.RetentionPeriod.Default,
48 | ),
49 | `if` = expr { always() }
50 | )
51 | uses(
52 | name = "Upload test reports",
53 | action = UploadArtifactV4(
54 | name = "Junit",
55 | path = listOf("**/build/reports/tests"),
56 | retentionDays = UploadArtifactV4.RetentionPeriod.Default,
57 | ),
58 | `if` = expr { always() }
59 | )
60 | }
61 | }.writeToFile()
62 |
--------------------------------------------------------------------------------
/.github/workflows/build_workflow.yaml:
--------------------------------------------------------------------------------
1 | # This file was generated using Kotlin DSL (.github/workflows/build_workflow.main.kts).
2 | # If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file.
3 | # Generated with https://github.com/typesafegithub/github-workflows-kt
4 |
5 | name: 'Build workflow'
6 | on:
7 | push:
8 | branches:
9 | - 'main'
10 | pull_request: {}
11 | jobs:
12 | check_yaml_consistency:
13 | name: 'Check YAML consistency'
14 | runs-on: 'ubuntu-latest'
15 | steps:
16 | - id: 'step-0'
17 | name: 'Check out'
18 | uses: 'actions/checkout@v4'
19 | - id: 'step-1'
20 | name: 'Execute script'
21 | run: 'rm ''.github/workflows/build_workflow.yaml'' && ''.github/workflows/build_workflow.main.kts'''
22 | - id: 'step-2'
23 | name: 'Consistency check'
24 | run: 'git diff --exit-code ''.github/workflows/build_workflow.yaml'''
25 | build-and-test:
26 | runs-on: 'ubuntu-latest'
27 | needs:
28 | - 'check_yaml_consistency'
29 | steps:
30 | - id: 'step-0'
31 | name: 'Check out'
32 | uses: 'actions/checkout@v4'
33 | - id: 'step-1'
34 | name: 'Setup Java'
35 | uses: 'actions/setup-java@v4'
36 | with:
37 | java-version: '17'
38 | distribution: 'temurin'
39 | - id: 'step-2'
40 | name: 'Build'
41 | uses: 'gradle/gradle-build-action@v3'
42 | with:
43 | arguments: 'test'
44 | - id: 'step-3'
45 | name: 'Upload reports'
46 | uses: 'actions/upload-artifact@v4'
47 | with:
48 | name: 'Roborazzi'
49 | path: 'shared/build/outputs/roborazzi'
50 | retention-days: '0'
51 | if: '${{ always() }}'
52 | - id: 'step-4'
53 | name: 'Upload test reports'
54 | uses: 'actions/upload-artifact@v4'
55 | with:
56 | name: 'Junit'
57 | path: '**/build/reports/tests'
58 | retention-days: '0'
59 | if: '${{ always() }}'
60 |
--------------------------------------------------------------------------------
/.github/workflows/fix_workflow.main.kts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env kotlin
2 |
3 | @file:DependsOn("io.github.typesafegithub:github-workflows-kt:1.11.0")
4 |
5 | import io.github.typesafegithub.workflows.actions.actions.CheckoutV4
6 | import io.github.typesafegithub.workflows.actions.actions.SetupJavaV4
7 | import io.github.typesafegithub.workflows.actions.gradle.GradleBuildActionV3
8 | import io.github.typesafegithub.workflows.actions.stefanzweifel.GitAutoCommitActionV5
9 | import io.github.typesafegithub.workflows.domain.RunnerType.UbuntuLatest
10 | import io.github.typesafegithub.workflows.domain.triggers.WorkflowDispatch
11 | import io.github.typesafegithub.workflows.dsl.expressions.expr
12 | import io.github.typesafegithub.workflows.dsl.workflow
13 | import io.github.typesafegithub.workflows.yaml.writeToFile
14 |
15 | workflow(
16 | name = "Fix workflow",
17 | on = listOf(
18 | WorkflowDispatch(),
19 | ),
20 | sourceFile = __FILE__.toPath(),
21 | ) {
22 | job(
23 | id = "fix-branch",
24 | runsOn = UbuntuLatest,
25 | // Ensure no auto-commits are ever made on the main/default branch.
26 | // This is an extra security measure, especially since we don't control what the
27 | // "stefanzweifel/git-auto-commit-action@v5" GitHub Action could theoretically do.
28 | `if` = expr { github.ref_name + " != " + github.eventWorkflowDispatch.repository.default_branch }
29 | ) {
30 | uses(name = "Check out", action = CheckoutV4())
31 | uses(
32 | name = "Setup Java",
33 | action = SetupJavaV4(distribution = SetupJavaV4.Distribution.Temurin, javaVersion = "17")
34 | )
35 | uses(
36 | name = "Record Screenshots",
37 | action = GradleBuildActionV3(
38 | arguments = "verifyAndRecordRoborazziDebug",
39 | )
40 | )
41 | uses(
42 | name = "Commit Screenshots",
43 | action = GitAutoCommitActionV5(
44 | filePattern = "**/src/test/screenshots/*.png",
45 | disableGlobbing = true,
46 | commitMessage = "🤖 Updates screenshots"
47 | )
48 | )
49 | }
50 | }.writeToFile()
51 |
--------------------------------------------------------------------------------
/.github/workflows/fix_workflow.yaml:
--------------------------------------------------------------------------------
1 | # This file was generated using Kotlin DSL (.github/workflows/fix_workflow.main.kts).
2 | # If you want to modify the workflow, please change the Kotlin file and regenerate this YAML file.
3 | # Generated with https://github.com/typesafegithub/github-workflows-kt
4 |
5 | name: 'Fix workflow'
6 | on:
7 | workflow_dispatch: {}
8 | jobs:
9 | check_yaml_consistency:
10 | name: 'Check YAML consistency'
11 | runs-on: 'ubuntu-latest'
12 | steps:
13 | - id: 'step-0'
14 | name: 'Check out'
15 | uses: 'actions/checkout@v4'
16 | - id: 'step-1'
17 | name: 'Execute script'
18 | run: 'rm ''.github/workflows/fix_workflow.yaml'' && ''.github/workflows/fix_workflow.main.kts'''
19 | - id: 'step-2'
20 | name: 'Consistency check'
21 | run: 'git diff --exit-code ''.github/workflows/fix_workflow.yaml'''
22 | fix-branch:
23 | runs-on: 'ubuntu-latest'
24 | needs:
25 | - 'check_yaml_consistency'
26 | if: '${{ github.ref_name != github.event.repository.default_branch }}'
27 | steps:
28 | - id: 'step-0'
29 | name: 'Check out'
30 | uses: 'actions/checkout@v4'
31 | - id: 'step-1'
32 | name: 'Setup Java'
33 | uses: 'actions/setup-java@v4'
34 | with:
35 | java-version: '17'
36 | distribution: 'temurin'
37 | - id: 'step-2'
38 | name: 'Record Screenshots'
39 | uses: 'gradle/gradle-build-action@v3'
40 | with:
41 | arguments: 'verifyAndRecordRoborazziDebug'
42 | - id: 'step-3'
43 | name: 'Commit Screenshots'
44 | uses: 'stefanzweifel/git-auto-commit-action@v5'
45 | with:
46 | commit_message: '🤖 Updates screenshots'
47 | file_pattern: '**/src/test/screenshots/*.png'
48 | disable_globbing: 'true'
49 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | /compiler.xml
5 | /deploymentTargetDropDown.xml
6 | /gradle.xml
7 | /kotlinc.xml
8 | /migrations.xml
9 | /misc.xml
10 | /vcs.xml
11 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | ComposeOClock sample
--------------------------------------------------------------------------------
/.idea/runConfigurations/app_watch__installDebug_.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
16 |
17 |
18 | false
19 | true
20 | false
21 | false
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.screenshots/kotlin-and-compose-fan-clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/.screenshots/kotlin-and-compose-fan-clock.png
--------------------------------------------------------------------------------
/app-phone/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-phone/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("android-app")
3 | id("version-code-phone")
4 | }
5 |
6 | android {
7 | namespace = "com.louiscad.composeoclockplayground"
8 |
9 | defaultConfig {
10 | applicationId = "com.louiscad.composeoclockplayground"
11 | minSdk = 26
12 | targetSdk = 34
13 | versionName = version.toString()
14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | compileOptions {
17 | isCoreLibraryDesugaringEnabled = true
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation {
23 | project(":shared")()
24 |
25 | AndroidX.compose.material3()
26 | AndroidX.compose.ui.toolingPreview()
27 | AndroidX.activity.compose()
28 | AndroidX.compose.ui()
29 | AndroidX.compose.ui.graphics()
30 | AndroidX.compose.ui.toolingPreview()
31 | AndroidX.compose.material3()
32 | }
33 | coreLibraryDesugaring(Android.tools.desugarJdkLibs)
34 | testImplementation {
35 | Testing.junit4()
36 | }
37 | androidTestImplementation {
38 | AndroidX.test.ext.junit()
39 | AndroidX.test.espresso.core()
40 | AndroidX.compose.ui.testJunit4()
41 | }
42 | debugImplementation {
43 | AndroidX.compose.ui.tooling()
44 | AndroidX.compose.ui.testManifest()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app-phone/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
--------------------------------------------------------------------------------
/app-phone/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app-phone/src/main/java/com/louiscad/composeoclockplayground/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.Scaffold
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.tooling.preview.Preview
15 | import com.louiscad.composeoclockplayground.ui.theme.MyComposeOClockPlaygroundTheme
16 | import org.splitties.compose.oclock.OClockRootCanvas
17 | import org.splitties.compose.oclock.sample.watchfaces.WatchFaceSwitcher
18 |
19 | class MainActivity : ComponentActivity() {
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | enableEdgeToEdge()
23 | setContent {
24 | MyComposeOClockPlaygroundTheme {
25 | Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
26 | Content(Modifier.padding(innerPadding))
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
33 | @Composable
34 | fun Content(modifier: Modifier = Modifier) {
35 | Box(
36 | modifier = modifier,
37 | contentAlignment = Alignment.Center
38 | ) {
39 | OClockRootCanvas {
40 | WatchFaceSwitcher()
41 | }
42 | }
43 | }
44 |
45 | @Preview(showBackground = true)
46 | @Composable
47 | fun ContentPreview() {
48 | MyComposeOClockPlaygroundTheme {
49 | Content()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app-phone/src/main/java/com/louiscad/composeoclockplayground/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app-phone/src/main/java/com/louiscad/composeoclockplayground/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun MyComposeOClockPlaygroundTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/app-phone/src/main/java/com/louiscad/composeoclockplayground/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app-phone/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 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-phone/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app-phone/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My ComposeOClock Playground
3 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app-phone/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
20 |
--------------------------------------------------------------------------------
/app-phone/src/test/java/com/louiscad/composeoclockplayground/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app-watch/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-watch/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("android-app")
3 | id("version-code-watch")
4 | }
5 |
6 | android {
7 | namespace = "com.louiscad.composeoclockplayground"
8 |
9 | defaultConfig {
10 | applicationId = "com.louiscad.composeoclockplayground"
11 | minSdk = 26
12 | targetSdk = 33
13 | versionName = version.toString()
14 | }
15 | compileOptions {
16 | isCoreLibraryDesugaringEnabled = true
17 | }
18 | }
19 |
20 | dependencies {
21 | coreLibraryDesugaring(Android.tools.desugarJdkLibs)
22 | implementation {
23 | project(":shared")()
24 | libs.compose.oclock.watchface.renderer()
25 | AndroidX.wear.watchFace.editor()
26 |
27 | AndroidX.wear.compose.material()
28 | AndroidX.wear.compose.foundation()
29 | AndroidX.activity.compose()
30 | AndroidX.core.splashscreen()
31 |
32 | Splitties.systemservices()
33 | Splitties.toast()
34 | }
35 | androidTestImplementation {
36 | AndroidX.compose.ui.testJunit4()
37 | }
38 | debugImplementation {
39 | AndroidX.compose.ui.tooling()
40 | AndroidX.compose.ui.testManifest()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app-watch/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app-watch/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
--------------------------------------------------------------------------------
/app-watch/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
17 |
18 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
51 |
54 |
57 |
60 |
61 |
62 |
63 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app-watch/src/main/kotlin/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /* While this template provides a good starting point for using Wear Compose, you can always
2 | * take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter and
3 | * https://github.com/android/wear-os-samples/tree/main/ComposeAdvanced to find the most up to date
4 | * changes to the libraries and their usages.
5 | */
6 |
7 | package com.louiscad.composeoclockplayground
8 |
9 | import android.os.Bundle
10 | import androidx.activity.ComponentActivity
11 | import androidx.activity.compose.setContent
12 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
13 | import androidx.compose.foundation.background
14 | import androidx.compose.foundation.layout.Box
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.foundation.layout.fillMaxWidth
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.res.stringResource
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.tooling.preview.Devices
23 | import androidx.compose.ui.tooling.preview.Preview
24 | import androidx.wear.compose.material.MaterialTheme
25 | import androidx.wear.compose.material.Text
26 | import androidx.wear.compose.material.TimeText
27 | import com.louiscad.composeoclockplayground.presentation.theme.MyComposeOClockPlaygroundTheme
28 |
29 | class MainActivity : ComponentActivity() {
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | installSplashScreen()
32 |
33 | super.onCreate(savedInstanceState)
34 |
35 | setTheme(android.R.style.Theme_DeviceDefault)
36 |
37 | setContent {
38 | WearApp("Android")
39 | }
40 | }
41 | }
42 |
43 | @Composable
44 | fun WearApp(greetingName: String) {
45 | MyComposeOClockPlaygroundTheme {
46 | Box(
47 | modifier = Modifier
48 | .fillMaxSize()
49 | .background(MaterialTheme.colors.background),
50 | contentAlignment = Alignment.Center
51 | ) {
52 | TimeText()
53 | Greeting(greetingName = greetingName)
54 | }
55 | }
56 | }
57 |
58 | @Composable
59 | fun Greeting(greetingName: String) {
60 | Text(
61 | modifier = Modifier.fillMaxWidth(),
62 | textAlign = TextAlign.Center,
63 | color = MaterialTheme.colors.primary,
64 | text = stringResource(R.string.hello_world, greetingName)
65 | )
66 | }
67 |
68 | @Preview(device = Devices.WEAR_OS_SMALL_ROUND, showSystemUi = true)
69 | @Composable
70 | fun DefaultPreview() {
71 | WearApp("Preview Android")
72 | }
73 |
--------------------------------------------------------------------------------
/app-watch/src/main/kotlin/SampleWatchFaceService.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.wear.watchface.complications.data.ComplicationData
5 | import androidx.wear.watchface.complications.data.ComplicationType
6 | import kotlinx.coroutines.flow.*
7 | import org.splitties.compose.oclock.ComposeWatchFaceService
8 | import org.splitties.compose.oclock.sample.watchfaces.KotlinFanClock
9 | import org.splitties.compose.oclock.sample.watchfaces.WatchFaceSwitcher
10 |
11 | class SampleWatchFaceService : ComposeWatchFaceService(
12 | complicationSlotIds = emptySet(),
13 | invalidationMode = InvalidationMode.WaitForInvalidation
14 | ) {
15 |
16 | @Composable
17 | override fun Content(complicationData: Map>) {
18 | WatchFaceSwitcher()
19 | }
20 |
21 | override fun supportedComplicationTypes(slotId: Int) = listOf(
22 | ComplicationType.RANGED_VALUE,
23 | ComplicationType.SHORT_TEXT
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/app-watch/src/main/kotlin/WatchFaceConfigActivity.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.provider.Settings
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.lifecycle.lifecycleScope
10 | import androidx.wear.watchface.editor.EditorSession
11 | import com.louiscad.composeoclockplayground.editor.WatchFaceConfigContent
12 | import com.louiscad.composeoclockplayground.editor.WatchFaceEditorSession
13 | import kotlinx.coroutines.*
14 | import splitties.toast.longToast
15 | import splitties.toast.toast
16 |
17 | class WatchFaceConfigActivity : ComponentActivity() {
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
22 | val activity = this@WatchFaceConfigActivity
23 | val editorSession = WatchFaceEditorSession(
24 | scope = lifecycleScope,
25 | session = EditorSession.createOnWatchEditorSession(activity)
26 | )
27 | setContent {
28 | WatchFaceConfigContent(
29 | editorSession = editorSession,
30 | )
31 | }
32 | if (Settings.System.canWrite(applicationContext)) {
33 | toast("Can write settings!")
34 | } else {
35 | longToast( "Go into \"Advcanced\"…")
36 | longToast( "and enable \"Modify system settings\"")
37 | startActivity(
38 | Intent(
39 | Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
40 | Uri.fromParts("package", packageName, null)
41 | )
42 | )
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app-watch/src/main/kotlin/editor/WatchFaceConfigContent.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground.editor
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.wear.compose.material.Text
9 |
10 | @Composable
11 | fun WatchFaceConfigContent(editorSession: WatchFaceEditorSession) {
12 | Box(
13 | Modifier.fillMaxSize(),
14 | contentAlignment = Alignment.Center
15 | ) {
16 | Text("Nothing there yet…")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app-watch/src/main/kotlin/editor/WatchFaceEditorSession.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground.editor
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.wear.watchface.complications.ComplicationDataSourceInfo
7 | import androidx.wear.watchface.editor.EditorSession
8 | import kotlinx.coroutines.*
9 | import kotlinx.coroutines.flow.*
10 |
11 | class WatchFaceEditorSession(
12 | scope: CoroutineScope,
13 | val session: EditorSession?
14 | ) {
15 | fun tryOpenDataSourcePicker(complicationId: Int) {
16 | flow.tryEmit(complicationId)
17 | }
18 |
19 | fun complicationDataSourceInfo(id: Int): Flow = session?.let {
20 | it.complicationsDataSourceInfo.map { map -> map[id] }
21 | } ?: flowOf(null)
22 |
23 | private val isBusyPickingComplicationState = mutableStateOf(false)
24 |
25 | val isBusyPickingDataSource: Boolean by isBusyPickingComplicationState
26 |
27 | private val flow = MutableSharedFlow(extraBufferCapacity = 1)
28 |
29 | init {
30 | scope.launch {
31 | flow.collect { id ->
32 | isBusyPickingComplicationState.withValue(valueInScope = true) {
33 | session?.openComplicationDataSourceChooser(id)
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
40 | private inline fun MutableState.withValue(
41 | valueInScope: T,
42 | block: () -> R
43 | ): R {
44 | val initialValue = value
45 | try {
46 | value = valueInScope
47 | return block()
48 | } finally {
49 | value = initialValue
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app-watch/src/main/kotlin/presentation/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.composeoclockplayground.presentation.theme
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.wear.compose.material.MaterialTheme
5 |
6 | @Composable
7 | fun MyComposeOClockPlaygroundTheme(
8 | content: @Composable () -> Unit
9 | ) {
10 | /**
11 | * Empty theme to customize for your app.
12 | * See: https://developer.android.com/jetpack/compose/designsystems/custom
13 | */
14 | MaterialTheme(
15 | content = content
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app-watch/src/main/res/drawable/splash_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
8 |
9 |
10 |
11 |
12 | -
16 |
22 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app-watch/src/main/res/drawable/watch_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-watch/src/main/res/drawable/watch_preview.png
--------------------------------------------------------------------------------
/app-watch/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-watch/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-watch/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-watch/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-watch/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-watch/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-watch/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-watch/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-watch/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/app-watch/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app-watch/src/main/res/values-round/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | From the Round world,\nHello, %1$s!
3 |
--------------------------------------------------------------------------------
/app-watch/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My ComposeOClock Playground
3 |
7 | From the Square world,\nHello, %1$s!
8 |
--------------------------------------------------------------------------------
/app-watch/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/app-watch/src/main/res/xml/watch_face.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id("com.android.application") apply false
4 | id("com.android.library") apply false
5 | kotlin("android") apply false
6 | alias(libs.plugins.roborazzi) apply false
7 | }
8 |
--------------------------------------------------------------------------------
/convention-plugins/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/convention-plugins/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | gradlePluginPortal()
9 | mavenLocal()
10 | }
11 |
12 |
13 | dependencies {
14 | fun plugin(id: String, version: String) = "$id:$id.gradle.plugin:$version"
15 | implementation {
16 | Android.tools.build.gradlePlugin()
17 | Kotlin.gradlePlugin()
18 | plugin("org.jetbrains.kotlin.plugin.compose", "_")()
19 | Google.playServicesGradlePlugin()
20 | Firebase.crashlyticsGradlePlugin()
21 | plugin("de.fayard.refreshVersions", "_")()
22 | plugin("org.splitties.dependencies-dsl", "_")()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/convention-plugins/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.parallel=true
2 | org.gradle.caching=true
3 | org.gradle.configuration-cache=true
4 |
5 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
6 | # When configured, Gradle will run in incubating parallel mode.
7 | # This option should only be used with decoupled projects. For more details, visit
8 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
9 | # org.gradle.parallel=true
10 | # AndroidX package structure to make it clearer which packages are bundled with the
11 | # Android operating system, and which are packaged with your app's APK
12 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
13 | android.useAndroidX=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 | # Enables namespacing of each library's R class so that its R class includes only the
17 | # resources declared in the library itself and none from the library's dependencies,
18 | # thereby reducing the size of the R class for that library
19 | android.nonTransitiveRClass=true
20 |
21 | kotlin.daemon.useFallbackStrategy=false
22 |
23 | android.defaults.buildfeatures.aidl=false
24 | android.defaults.buildfeatures.renderscript=false
25 |
26 |
--------------------------------------------------------------------------------
/convention-plugins/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | mavenCentral()
4 | gradlePluginPortal()
5 | mavenLocal()
6 | }
7 | plugins {
8 | id("org.splitties.dependencies-dsl") version "0.1.0"
9 | id("de.fayard.refreshVersions") version "0.60.5"
10 | }
11 | }
12 |
13 | plugins {
14 | id("org.splitties.dependencies-dsl")
15 | id("de.fayard.refreshVersions")
16 | }
17 |
18 | refreshVersions {
19 | versionsPropertiesFile = rootDir.parentFile.resolve("versions.properties")
20 | }
21 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/Versioning.kt:
--------------------------------------------------------------------------------
1 | package convention_plugins
2 |
3 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.the
6 |
7 | enum class AppKind {
8 | Phone,
9 | Watch
10 | }
11 |
12 | fun Project.setVersionCodeForApp(kind: AppKind) {
13 | val android = the()
14 | android.defaultConfig.versionCode = versionCode(kind)
15 | }
16 |
17 | private fun Project.versionCode(kind: AppKind): Int {
18 | val releaseBuildNumber = rootProject.layout.projectDirectory.file(
19 | "releaseBuildNumber.txt"
20 | ).asFile.useLines {
21 | it.first()
22 | }.toInt().also { require(it > 0) }
23 | return releaseBuildNumber * 2 - when (kind) {
24 | AppKind.Phone -> 1
25 | AppKind.Watch -> 0
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/android-app.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileNotFoundException
2 |
3 | plugins {
4 | kotlin("android")
5 | id("com.android.application")
6 | kotlin("plugin.compose")
7 | }
8 |
9 | android {
10 | compileSdk = 34
11 | defaultConfig {
12 | minSdk = 26
13 | targetSdk = 33
14 | resourceConfigurations += setOf("en", "fr")
15 | }
16 | // We embed the signing keystore to allow updating from another computer without uninstalling.
17 | val debugSigningConfig by signingConfigs.creating {
18 | storeFile = rootProject.file("debug.keystore")
19 | storePassword = "android"
20 | keyAlias = "androiddebugkey"
21 | keyPassword = "android"
22 | }
23 | val keystoreFile = (findProperty("androidKeyFile") as String?)?.let { keyFilePath ->
24 | File(System.getProperty("user.home")).resolve(keyFilePath).also {
25 | if (it.exists().not()) throw FileNotFoundException("Didn't find keystore file at $it")
26 | }
27 | }
28 | val releaseSigningConfig = keystoreFile?.let {
29 | signingConfigs.create("releaseSigningConfig") {
30 | val androidKeyFile: String by project
31 | storeFile = File(System.getProperty("user.home")).resolve(androidKeyFile)
32 | val androidKeyAlias: String by project
33 | val androidKeyUniversalMdp: String by project
34 | storePassword = androidKeyUniversalMdp
35 | keyAlias = androidKeyAlias
36 | keyPassword = androidKeyUniversalMdp
37 | }
38 | }
39 | buildTypes {
40 | debug {
41 | applicationIdSuffix = ".debug"
42 | signingConfig = debugSigningConfig
43 | }
44 | create("staging") {
45 | applicationIdSuffix = ".staging"
46 | matchingFallbacks += "release"
47 | isMinifyEnabled = true
48 | isShrinkResources = true
49 | isDebuggable = false
50 | signingConfig = debugSigningConfig
51 | }
52 | release {
53 | isMinifyEnabled = true
54 | isShrinkResources = true
55 | signingConfig = releaseSigningConfig ?: debugSigningConfig
56 | proguardFiles(
57 | getDefaultProguardFile("proguard-android-optimize.txt"),
58 | "proguard-rules.pro"
59 | )
60 | }
61 | }
62 | kotlinOptions {
63 | jvmTarget = "1.8"
64 | freeCompilerArgs += "-Xcontext-receivers"
65 | }
66 | compileOptions {
67 | sourceCompatibility = JavaVersion.VERSION_1_8
68 | targetCompatibility = JavaVersion.VERSION_1_8
69 | }
70 | buildFeatures.compose = true
71 | packagingOptions.resources {
72 | excludes += setOf(
73 | "META-INF/ASL2.0",
74 | "META-INF/AL2.0",
75 | "META-INF/LGPL2.1",
76 | "META-INF/LICENSE",
77 | "META-INF/license.txt",
78 | "META-INF/NOTICE",
79 | "META-INF/notice.txt"
80 | )
81 |
82 | // Exclude files that unused kotlin-reflect would need, to make the app smaller:
83 | // (see issue https://youtrack.jetbrains.com/issue/KT-9770)
84 | excludes += setOf(
85 | "META-INF/*.kotlin_module",
86 | "kotlin/*.kotlin_builtins",
87 | "kotlin/**/*.kotlin_builtins"
88 | )
89 | }
90 | }
91 |
92 | dependencies {
93 | implementation {
94 | AndroidX.activity.compose()
95 |
96 | AndroidX.compose.runtime()
97 | AndroidX.compose.foundation()
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/android-crashlytics.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("android")
3 | id("com.android.application")
4 | id("com.google.gms.google-services")
5 | id("com.google.firebase.crashlytics")
6 | }
7 |
8 | dependencies {
9 | implementation {
10 | platform(Firebase.bom)()
11 | Firebase.analyticsKtx()
12 | Firebase.crashlyticsKtx()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/android-lib.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("android")
3 | id("com.android.library")
4 | }
5 |
6 | android {
7 | compileSdk = 34
8 | defaultConfig {
9 | minSdk = 23
10 | }
11 | buildTypes {
12 | release {
13 | isMinifyEnabled = false
14 | }
15 | }
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_1_8
18 | targetCompatibility = JavaVersion.VERSION_1_8
19 | }
20 | kotlinOptions {
21 | jvmTarget = "1.8"
22 | freeCompilerArgs += "-Xcontext-receivers"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/android-signing-config.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.io.FileNotFoundException
2 |
3 | plugins {
4 | id("com.android.application")
5 | }
6 |
7 | android {
8 | val debugSigningConfig by signingConfigs.creating {
9 | storeFile = rootProject.file("debug.keystore")
10 | storePassword = "android"
11 | keyAlias = "androiddebugkey"
12 | keyPassword = "android"
13 | }
14 | val keystoreFile = (property("androidKeyFile") as String?)?.let { keyFilePath ->
15 | File(System.getProperty("user.home")).resolve(keyFilePath).also {
16 | if (it.exists().not()) throw FileNotFoundException("Didn't find keystore file at $it")
17 | }
18 | }
19 | val releaseSigningConfig = keystoreFile?.let {
20 | signingConfigs.create("releaseSigningConfig") {
21 | val androidKeyFile: String by project
22 | storeFile = File(System.getProperty("user.home")).resolve(androidKeyFile)
23 | val androidKeyAlias: String by project
24 | val androidKeyUniversalMdp: String by project
25 | storePassword = androidKeyUniversalMdp
26 | keyAlias = androidKeyAlias
27 | keyPassword = androidKeyUniversalMdp
28 | }
29 | }
30 | buildTypes {
31 | debug {
32 | signingConfig = debugSigningConfig
33 | }
34 | release {
35 | signingConfig = releaseSigningConfig ?: debugSigningConfig
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/version-code-phone.gradle.kts:
--------------------------------------------------------------------------------
1 | import convention_plugins.AppKind
2 | import convention_plugins.setVersionCodeForApp
3 |
4 | setVersionCodeForApp(AppKind.Phone)
5 |
--------------------------------------------------------------------------------
/convention-plugins/src/main/kotlin/version-code-watch.gradle.kts:
--------------------------------------------------------------------------------
1 | import convention_plugins.AppKind
2 | import convention_plugins.setVersionCodeForApp
3 |
4 | setVersionCodeForApp(AppKind.Watch)
5 |
--------------------------------------------------------------------------------
/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/debug.keystore
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.parallel=true
2 | org.gradle.caching=true
3 | org.gradle.configuration-cache=true
4 |
5 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
6 | # When configured, Gradle will run in incubating parallel mode.
7 | # This option should only be used with decoupled projects. For more details, visit
8 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
9 | # org.gradle.parallel=true
10 | # AndroidX package structure to make it clearer which packages are bundled with the
11 | # Android operating system, and which are packaged with your app's APK
12 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
13 | android.useAndroidX=true
14 | # Kotlin code style for this project: "official" or "obsolete":
15 | kotlin.code.style=official
16 | # Enables namespacing of each library's R class so that its R class includes only the
17 | # resources declared in the library itself and none from the library's dependencies,
18 | # thereby reducing the size of the R class for that library
19 | android.nonTransitiveRClass=true
20 |
21 | kotlin.daemon.useFallbackStrategy=false
22 |
23 | android.defaults.buildfeatures.aidl=false
24 | android.defaults.buildfeatures.renderscript=false
25 |
26 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [plugins]
2 |
3 | roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
4 |
5 | [versions]
6 | composeOClock = "0.2.0-SNAPSHOT"
7 | roborazzi = "1.10.1"
8 |
9 | [libraries]
10 | compose-oclock-core = { group = "org.splitties.compose.oclock", name = "core", version.ref = "composeOClock" }
11 | compose-oclock-watchface-renderer = { group = "org.splitties.compose.oclock", name = "watchface-renderer", version.ref = "composeOClock" }
12 | roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
13 | roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
14 | roborazzi-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" }
15 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/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 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/releaseBuildNumber.txt:
--------------------------------------------------------------------------------
1 | 1
2 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | includeBuild("convention-plugins")
3 | repositories {
4 | google {
5 | content {
6 | includeGroupByRegex("com\\.android.*")
7 | includeGroupByRegex("com\\.google.*")
8 | includeGroupByRegex("androidx.*")
9 | }
10 | }
11 | mavenCentral()
12 | gradlePluginPortal()
13 | }
14 | }
15 |
16 | plugins {
17 | id("de.fayard.refreshVersions") version "0.60.5"
18 | id("org.splitties.settings-include-dsl") version "0.2.6"
19 | id("org.splitties.dependencies-dsl") version "0.2.0"
20 | id("org.splitties.version-sync") version "0.2.6"
21 | }
22 |
23 | run { // Remove when https://github.com/gradle/gradle/issues/2534 is fixed.
24 | val rootProjectPropertiesFile = rootDir.resolve("gradle.properties")
25 | val includedBuildPropertiesFile = rootDir.resolve("convention-plugins").resolve("gradle.properties")
26 | if (includedBuildPropertiesFile.exists().not() ||
27 | rootProjectPropertiesFile.readText() != includedBuildPropertiesFile.readText()
28 | ) {
29 | rootProjectPropertiesFile.copyTo(target = includedBuildPropertiesFile, overwrite = true)
30 | }
31 | }
32 |
33 | @Suppress("UnstableApiUsage")
34 | dependencyResolutionManagement {
35 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
36 | repositories {
37 | google()
38 | mavenCentral()
39 | maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots")
40 | }
41 | }
42 |
43 | rootProject.name = "ComposeOClock sample"
44 |
45 | include {
46 | "app-phone"()
47 | "app-watch"()
48 | "shared"()
49 | }
50 |
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | plugins {
4 | id("android-lib")
5 | kotlin("plugin.compose")
6 | alias(libs.plugins.roborazzi)
7 | }
8 |
9 | android {
10 | namespace = "com.louiscad.composeoclockplayground.shared"
11 |
12 | defaultConfig {
13 | minSdk = 26
14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles("consumer-rules.pro")
16 | }
17 | buildFeatures {
18 | compose = true
19 | buildConfig = true
20 | }
21 | compileOptions {
22 | isCoreLibraryDesugaringEnabled = true
23 | }
24 |
25 | testOptions {
26 | unitTests {
27 | isIncludeAndroidResources = true
28 | }
29 | }
30 | }
31 |
32 | dependencies {
33 | api {
34 | libs.compose.oclock.core()
35 | Google.android.playServices.wearOS()
36 |
37 | AndroidX.compose.ui()
38 | AndroidX.compose.ui.graphics()
39 | AndroidX.compose.ui.toolingPreview()
40 | AndroidX.compose.ui.text.googleFonts()
41 |
42 | AndroidX.core.ktx()
43 | AndroidX.lifecycle.runtime.ktx()
44 | }
45 | coreLibraryDesugaring(Android.tools.desugarJdkLibs)
46 | implementation {
47 | Splitties.systemservices()
48 | Splitties.appctx()
49 | Splitties.bitflags()
50 | }
51 | debugImplementation {
52 | AndroidX.compose.ui.tooling() //Important so previews can work.
53 | AndroidX.compose.ui.testManifest() // import for tests
54 | }
55 | testImplementation {
56 | Testing.junit4()
57 | Testing.robolectric()
58 | AndroidX.test.ext.junit.ktx()
59 | AndroidX.test.runner()
60 | AndroidX.compose.ui.testJunit4()
61 | libs.roborazzi()
62 | libs.roborazzi.compose()
63 | libs.roborazzi.rule()
64 | }
65 | androidTestImplementation {
66 | AndroidX.test.ext.junit.ktx()
67 | AndroidX.test.espresso.core()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/shared/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/consumer-rules.pro
--------------------------------------------------------------------------------
/shared/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
--------------------------------------------------------------------------------
/shared/src/androidTest/java/com/louiscad/shared/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.louiscad.shared
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.louiscad.shared.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/ComposeOClockWatermark.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.produceState
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.geometry.Offset
8 | import androidx.compose.ui.geometry.Rect
9 | import androidx.compose.ui.graphics.Brush
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.Path
12 | import androidx.compose.ui.graphics.SolidColor
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.text.TextStyle
15 | import androidx.compose.ui.text.font.AndroidFont
16 | import androidx.compose.ui.text.font.FontFamily
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.text.googlefonts.Font
19 | import androidx.compose.ui.text.googlefonts.GoogleFont
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.text.style.TextMotion
22 | import androidx.compose.ui.tooling.preview.PreviewParameter
23 | import androidx.compose.ui.unit.Dp
24 | import androidx.compose.ui.unit.dp
25 | import androidx.compose.ui.unit.sp
26 | import org.splitties.compose.oclock.LocalIsAmbient
27 | import org.splitties.compose.oclock.OClockCanvas
28 | import org.splitties.compose.oclock.sample.extensions.drawTextOnPath
29 | import org.splitties.compose.oclock.sample.extensions.rememberStateWithSize
30 | import org.splitties.compose.oclock.sample.extensions.text.rememberTextOnPathMeasurer
31 |
32 | @Composable
33 | fun ComposeOClockWatermark(finalBrush: Brush) {
34 | val font = remember {
35 | Font(
36 | googleFont = GoogleFont("Jost"),
37 | // googleFont = GoogleFont("Raleway"),
38 | // googleFont = GoogleFont("Reem Kufi"),
39 | // googleFont = GoogleFont("Montez"),
40 | fontProvider = googleFontProvider,
41 | ) as AndroidFont
42 | }
43 | val fontFamily = remember(font) { FontFamily(font) }
44 | val context = LocalContext.current
45 | val brush by produceState(finalBrush, font) {
46 | val result = runCatching {
47 | font.typefaceLoader.awaitLoad(context, font)
48 | }
49 | value = if (result.isSuccess) finalBrush else SolidColor(Color.Red)
50 | }
51 | val interactiveTextStyle = remember(fontFamily, brush) {
52 | TextStyle(
53 | brush = brush,
54 | fontFamily = fontFamily,
55 | fontSize = 16.sp,
56 | fontWeight = FontWeight.W600,
57 | textAlign = TextAlign.Center,
58 | lineHeight = 20.sp,
59 | textMotion = TextMotion.Animated
60 | )
61 | }
62 | val ambientTextStyle = rememberStateWithSize(interactiveTextStyle) {
63 | interactiveTextStyle.copy(fontWeight = FontWeight.W300)
64 | }
65 | val textMeasurer = rememberTextOnPathMeasurer(cacheSize = 0)
66 | val isAmbient by LocalIsAmbient.current
67 | val cachedPath = remember { Path() }.let { path ->
68 | rememberStateWithSize {
69 | val minimumInset = interactiveTextStyle.fontSize.toPx()
70 | path.arcTo(
71 | rect = Rect(Offset.Zero, size).deflate(minimumInset + 4.dp.toPx()),
72 | startAngleDegrees = 89.5f,
73 | sweepAngleDegrees = 359f,
74 | forceMoveTo = true
75 | )
76 | path
77 | }
78 | }
79 | val string = "It's Compose O'Clock!"
80 | val interactiveText = rememberStateWithSize {
81 | textMeasurer.measure(
82 | text = string,
83 | style = interactiveTextStyle
84 | )
85 | }
86 | val ambientText = rememberStateWithSize {
87 | textMeasurer.measure(
88 | text = string,
89 | style = ambientTextStyle.get()
90 | )
91 | }
92 | OClockCanvas {
93 | val text = if (isAmbient) ambientText else interactiveText
94 | drawTextOnPath(
95 | textLayoutResult = text.get(),
96 | path = cachedPath.get()
97 | )
98 | }
99 | }
100 |
101 | @WatchFacePreview
102 | @Composable
103 | private fun ComposeOClockWatermarkPreview(
104 | @PreviewParameter(WearPreviewSizes::class) size: Dp
105 | ) = WatchFacePreview(size) {
106 | ComposeOClockWatermark(SolidColor(Color.Magenta))
107 | }
108 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/GoogleFonts.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample
2 |
3 | import androidx.compose.ui.text.googlefonts.GoogleFont
4 | import com.louiscad.composeoclockplayground.shared.R
5 |
6 | val googleFontProvider = GoogleFont.Provider(
7 | providerAuthority = "com.google.android.gms.fonts",
8 | providerPackage = "com.google.android.gms",
9 | certificates = R.array.com_google_android_gms_fonts_certs
10 | )
11 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/WatchFacePreview.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.animateColorAsState
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.requiredSize
11 | import androidx.compose.foundation.shape.CircleShape
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.collectAsState
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.runtime.setValue
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.draw.clip
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.tooling.preview.Preview
22 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider
23 | import androidx.compose.ui.unit.Dp
24 | import androidx.compose.ui.unit.dp
25 | import androidx.wear.watchface.complications.data.ComplicationData
26 | import kotlinx.coroutines.flow.*
27 | import org.splitties.compose.oclock.ExperimentalComposeOClockApi
28 | import org.splitties.compose.oclock.OClockRootCanvas
29 | import org.splitties.compose.oclock.PreviewTime
30 |
31 | class WearPreviewSizes : PreviewParameterProvider {
32 | override val values: Sequence = sequenceOf(
33 | WatchFacePreview.Size.small,
34 | WatchFacePreview.Size.large
35 | )
36 | }
37 |
38 | class WearSmallest : PreviewParameterProvider {
39 | override val values: Sequence = sequenceOf(
40 | WatchFacePreview.Size.small
41 | )
42 | }
43 |
44 | /**
45 | * Example usage:
46 | *
47 | * ```kotlin
48 | * @WatchFacePreview
49 | * @Composable
50 | * private fun MyFunClockPreview(
51 | * @PreviewParameter(WearPreviewSizes::class) size: Dp
52 | * ) = WatchFacePreview(size) {
53 | * MyFunClock()
54 | * }
55 | * ```
56 | */
57 | @Preview
58 | annotation class WatchFacePreview {
59 | object Size {
60 | val small = 192.dp
61 | val large = 227.dp
62 | }
63 | }
64 |
65 | @Composable
66 | fun WatchFacePreview(
67 | size: Dp,
68 | @OptIn(ExperimentalComposeOClockApi::class)
69 | previewTime: PreviewTime.Config = PreviewTime.rememberConfig(),
70 | content: @Composable (complicationData: Map>) -> Unit
71 | ) {
72 | val spacing = 8.dp
73 | var touched by remember { mutableStateOf(false) }
74 | val isAmbientFlow = remember { MutableStateFlow(false) }
75 | val isAmbient by isAmbientFlow.collectAsState()
76 | val backgroundColor by animateColorAsState(
77 | targetValue = when {
78 | touched.not() -> Color.Transparent
79 | isAmbient -> Color.DarkGray
80 | else -> Color.LightGray
81 | }
82 | )
83 | Column(
84 | modifier = Modifier.background(backgroundColor).clickable {
85 | touched = true
86 | isAmbientFlow.update { it.not() }
87 | }.padding(spacing),
88 | verticalArrangement = Arrangement.spacedBy(spacing)
89 | ) {
90 | val modifier = Modifier.requiredSize(size).clip(CircleShape)
91 | OClockRootCanvas(
92 | modifier = modifier,
93 | previewTime = previewTime,
94 | isAmbientFlow = isAmbientFlow
95 | ) { content(it) }
96 | AnimatedVisibility(touched.not()) {
97 | OClockRootCanvas(
98 | modifier = modifier,
99 | previewTime = previewTime,
100 | isAmbientFlow = remember { MutableStateFlow(true) }
101 | ) { content(it) }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/cleanthisbeforerelease/complications/PixelWatchStyleComplication.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.cleanthisbeforerelease.complications
2 |
3 | import android.app.PendingIntent
4 | import android.os.Build
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.NoLiveLiterals
7 | import androidx.compose.runtime.collectAsState
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.ui.graphics.BlendMode
10 | import androidx.compose.ui.graphics.StrokeCap
11 | import androidx.compose.ui.graphics.drawscope.Stroke
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.text.TextStyle
14 | import androidx.compose.ui.text.drawText
15 | import androidx.wear.watchface.complications.data.ComplicationData
16 | import androidx.wear.watchface.complications.data.EmptyComplicationData
17 | import androidx.wear.watchface.complications.data.GoalProgressComplicationData
18 | import androidx.wear.watchface.complications.data.LongTextComplicationData
19 | import androidx.wear.watchface.complications.data.MonochromaticImageComplicationData
20 | import androidx.wear.watchface.complications.data.NoDataComplicationData
21 | import androidx.wear.watchface.complications.data.NoPermissionComplicationData
22 | import androidx.wear.watchface.complications.data.NotConfiguredComplicationData
23 | import androidx.wear.watchface.complications.data.PhotoImageComplicationData
24 | import androidx.wear.watchface.complications.data.RangedValueComplicationData
25 | import androidx.wear.watchface.complications.data.ShortTextComplicationData
26 | import androidx.wear.watchface.complications.data.SmallImageComplicationData
27 | import androidx.wear.watchface.complications.data.WeightedElementsComplicationData
28 | import kotlinx.coroutines.flow.*
29 | import org.splitties.compose.oclock.LocalTime
30 | import org.splitties.compose.oclock.OClockCanvas
31 | import org.splitties.compose.oclock.complications.rememberDrawableAsState
32 | import org.splitties.compose.oclock.complications.rememberMeasuredAsState
33 | import org.splitties.compose.oclock.sample.extensions.topCenterAsTopLeft
34 |
35 | @NoLiveLiterals // Because compilation fails otherwise. TODO: Report issue
36 | @Composable
37 | fun PixelWatchStyleComplication(
38 | complicationDataFlow: StateFlow,
39 | textStyle: TextStyle,
40 | sizeRatio: Float
41 | ) {
42 | val time = LocalTime.current
43 | val resources = LocalContext.current.resources
44 | val complicationData by complicationDataFlow.collectAsState()
45 | OClockCanvas(onTap = {
46 | try {
47 | complicationData.tapAction?.send()
48 | } catch (e: PendingIntent.CanceledException) {
49 | // In case the PendingIntent is no longer able to execute the request.
50 | // We don't need to do anything here.
51 | }
52 | false
53 | }) {}
54 | when (val data = complicationData) {
55 | is EmptyComplicationData -> {}
56 | is NoDataComplicationData -> {
57 | data.placeholder
58 | data.contentDescription
59 | }
60 | is NoPermissionComplicationData -> {
61 | data.rememberDrawableAsState()
62 | data.text
63 | data.title
64 | }
65 | is NotConfiguredComplicationData -> {}
66 | is LongTextComplicationData -> {
67 | val text by data.text.rememberMeasuredAsState { string ->
68 | measure(text = string, style = textStyle)
69 | }
70 | val title by data.title.rememberMeasuredAsState("") { string ->
71 | measure(text = string, style = textStyle)
72 | }
73 | val image by data.rememberDrawableAsState()
74 | data.contentDescription
75 | }
76 | is MonochromaticImageComplicationData -> {
77 | data.rememberDrawableAsState()
78 | data.contentDescription
79 | }
80 | is PhotoImageComplicationData -> {
81 | data.rememberDrawableAsState()
82 | data.contentDescription
83 | }
84 | is RangedValueComplicationData -> {
85 | data.valueType
86 | RangedValueComplicationData.TYPE_RATING
87 | RangedValueComplicationData.TYPE_PERCENTAGE
88 | RangedValueComplicationData.TYPE_UNDEFINED
89 | data.min
90 | data.max
91 | data.value
92 | data.colorRamp
93 | data.title
94 | data.text
95 | data.rememberDrawableAsState()
96 | data.contentDescription
97 | val text by data.text.rememberMeasuredAsState("") { string ->
98 | measure(text = string, style = textStyle)
99 | }
100 | val title by data.title.rememberMeasuredAsState("") { string ->
101 | measure(text = string, style = textStyle)
102 | }
103 | OClockCanvas {
104 | drawArc(
105 | color = textStyle.color,
106 | startAngle = -90f,
107 | sweepAngle = 360 * data.value / data.max,
108 | useCenter = false,
109 | style = Stroke(width = 10f, cap = StrokeCap.Round),
110 | blendMode = BlendMode.Screen
111 | )
112 | drawText(
113 | textLayoutResult = title,
114 | topLeft = center.copy(
115 | y = 20f
116 | ).topCenterAsTopLeft(title.size)
117 | )
118 | drawText(
119 | textLayoutResult = text,
120 | topLeft = center.copy(
121 | y = 20f - title.size.height
122 | ).topCenterAsTopLeft(text.size)
123 | )
124 | }
125 | }
126 | is ShortTextComplicationData -> {
127 | data.rememberDrawableAsState()
128 | data.text
129 | data.title
130 | data.contentDescription
131 | }
132 | is SmallImageComplicationData -> {
133 | data.rememberDrawableAsState()
134 | data.contentDescription
135 | }
136 | else -> if (Build.VERSION.SDK_INT >= 33) when (data) {
137 | is GoalProgressComplicationData -> {
138 | data.targetValue
139 | data.value
140 | data.value
141 | data.colorRamp
142 | data.text
143 | data.title
144 | data.rememberDrawableAsState()
145 | data.contentDescription
146 | }
147 | is WeightedElementsComplicationData -> {
148 | data.elements.forEach {
149 | it.color
150 | it.weight
151 | }
152 | data.elementBackgroundColor
153 | data.rememberDrawableAsState()
154 | data.title
155 | data.text
156 | data.contentDescription
157 | }
158 | else -> {}
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/cleanthisbeforerelease/elements/CirclePatterns.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.cleanthisbeforerelease.elements
2 |
3 | import androidx.compose.ui.geometry.Size
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.graphics.drawscope.DrawScope
6 | import androidx.compose.ui.graphics.drawscope.Stroke
7 | import androidx.compose.ui.graphics.drawscope.rotate
8 | import androidx.compose.ui.unit.dp
9 | import org.splitties.compose.oclock.sample.extensions.drawOval
10 | import org.splitties.compose.oclock.sample.extensions.rotate
11 | import kotlin.math.PI
12 | import kotlin.math.sin
13 |
14 | fun DrawScope.circlesPattern(
15 | color: Color = Color.White.copy(alpha = .4f),
16 | count: Int = 90,
17 | startAngle: Float = 0f,
18 | endAngle: Float = 360f,
19 | diameters: Float,
20 | edgeMargin: Float = 0f,
21 | ) {
22 | val stroke = Stroke(width = 1.dp.toPx())
23 | val radius = diameters / 2
24 | for (i in 1..count) {
25 | val angle = 360f * i / count
26 | if (angle < startAngle) continue
27 | if (angle > endAngle) return
28 | drawCircle(
29 | color = color,
30 | style = stroke,
31 | radius = radius,
32 | center = center.copy(y = edgeMargin + stroke.width / 2 + radius).rotate(angle)
33 | )
34 | }
35 | }
36 |
37 | fun DrawScope.wavyCirclesPattern(
38 | color: Color = Color.White.copy(alpha = .4f),
39 | count: Int = 90,
40 | startAngle: Float = 0f,
41 | endAngle: Float = 360f,
42 | diameters: Float,
43 | travel: Float = diameters / 2f,
44 | periodAngle: Float = 30f,
45 | edgeMargin: Float = 0f,
46 | ) {
47 | val stroke = Stroke(width = 1.dp.toPx())
48 | val radius = diameters / 2
49 | for (i in 1..count) {
50 | val angle = 360f * i / count
51 | if (angle < startAngle) continue
52 | if (angle > endAngle) return
53 | val periodicOffset = (travel * (sin(2 * PI * (angle % periodAngle) / periodAngle - PI / 2) + 1)) / 2
54 | val y = edgeMargin + stroke.width / 2 + radius + periodicOffset.toFloat()
55 | drawCircle(
56 | color = color,
57 | style = stroke,
58 | radius = radius,
59 | center = center.copy(y = y).rotate(angle)
60 | )
61 | }
62 | }
63 |
64 | fun DrawScope.circlesPattern(
65 | color: Color = Color.White.copy(alpha = .4f),
66 | count: Int = 90,
67 | diameters: List,
68 | edgeMargin: Float = 0f,
69 | ) {
70 | val stroke = Stroke(width = 1.dp.toPx())
71 | diameters.forEach { diameter ->
72 | val radius = diameter / 2
73 | repeat(count) { i ->
74 | val angle = 360f * i / count
75 | drawCircle(
76 | color = color,
77 | style = stroke,
78 | radius = radius,
79 | center = center.copy(y = edgeMargin + stroke.width / 2 + radius).rotate(angle)
80 | )
81 | }
82 | }
83 | }
84 |
85 | fun DrawScope.ovalPattern(
86 | color: Color = Color.White.copy(alpha = .4f),
87 | count: Int = 90,
88 | size: Size,
89 | edgeMargin: Float = 0f,
90 | ) {
91 | val stroke = Stroke(width = 1.dp.toPx())
92 | repeat(count) { i ->
93 | rotate(degrees = 360f * i / count) {
94 | drawOval(
95 | color = color,
96 | style = stroke,
97 | size = size,
98 | center = center.copy(y = edgeMargin + stroke.width / 2 + size.height / 2)
99 | )
100 | }
101 | }
102 | }
103 |
104 | fun DrawScope.straightOvalPattern(
105 | color: Color = Color.White.copy(alpha = .4f),
106 | count: Int = 90,
107 | size: Size,
108 | edgeMargin: Float = 0f,
109 | ) {
110 | val stroke = Stroke(width = 1.dp.toPx())
111 | repeat(count) { i ->
112 | drawOval(
113 | color = color,
114 | style = stroke,
115 | size = size,
116 | center = center.copy(
117 | y = edgeMargin + stroke.width / 2 + size.height / 2
118 | ).rotate(degrees = 360f * i / count)
119 | )
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/cleanthisbeforerelease/elements/LinePatterns.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.cleanthisbeforerelease.elements
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.graphics.drawscope.DrawScope
5 | import androidx.compose.ui.unit.dp
6 | import org.splitties.compose.oclock.sample.extensions.rotate
7 |
8 | fun DrawScope.linesPattern(
9 | color: Color = Color.White.copy(alpha = .4f),
10 | count: Int = 90
11 | ) = circularRepeat(count) { degrees ->
12 | drawLine(
13 | color = color,
14 | strokeWidth = 1.dp.toPx(),
15 | start = center.copy(y = 0f).rotate(degrees = degrees),
16 | end = center.copy(y = 50.dp.toPx()).rotate(degrees = degrees),
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/cleanthisbeforerelease/elements/PatternHelpers.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.cleanthisbeforerelease.elements
2 |
3 | inline fun circularRepeat(
4 | count: Int,
5 | block: (angleInDegrees: Float) -> Unit
6 | ) {
7 | repeat(count) { i ->
8 | block(360f * i / count)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/cleanthisbeforerelease/experiments/TextOnPathExperiments.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.cleanthisbeforerelease.experiments
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.produceState
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.geometry.Offset
8 | import androidx.compose.ui.geometry.Rect
9 | import androidx.compose.ui.graphics.Brush
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.Paint
12 | import androidx.compose.ui.graphics.PaintingStyle
13 | import androidx.compose.ui.graphics.Path
14 | import androidx.compose.ui.graphics.PathEffect
15 | import androidx.compose.ui.graphics.SolidColor
16 | import androidx.compose.ui.graphics.drawscope.Stroke
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.text.TextStyle
19 | import androidx.compose.ui.text.font.AndroidFont
20 | import androidx.compose.ui.text.font.FontFamily
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.text.googlefonts.Font
23 | import androidx.compose.ui.text.googlefonts.GoogleFont
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.text.style.TextGeometricTransform
26 | import androidx.compose.ui.text.style.TextMotion
27 | import androidx.compose.ui.tooling.preview.PreviewParameter
28 | import androidx.compose.ui.unit.Dp
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import androidx.compose.ui.util.fastForEachIndexed
32 | import org.splitties.compose.oclock.LocalIsAmbient
33 | import org.splitties.compose.oclock.OClockCanvas
34 | import org.splitties.compose.oclock.sample.WatchFacePreview
35 | import org.splitties.compose.oclock.sample.WearPreviewSizes
36 | import org.splitties.compose.oclock.sample.extensions.centerAsTopLeft
37 | import org.splitties.compose.oclock.sample.extensions.drawTextOnPath
38 | import org.splitties.compose.oclock.sample.extensions.rememberStateWithSize
39 | import org.splitties.compose.oclock.sample.extensions.setFrom
40 | import org.splitties.compose.oclock.sample.extensions.text.rememberTextOnPathMeasurer
41 | import org.splitties.compose.oclock.sample.googleFontProvider
42 |
43 | @Composable
44 | fun TextOnPathExperiment(finalBrush: Brush) {
45 | val font = remember {
46 | Font(
47 | googleFont = GoogleFont("Jost"),
48 | // googleFont = GoogleFont("Raleway"),
49 | // googleFont = GoogleFont("Reem Kufi"),
50 | // googleFont = GoogleFont("Montez"),
51 | fontProvider = googleFontProvider,
52 | ) as AndroidFont
53 | }
54 | val fontFamily = remember(font) { FontFamily(font) }
55 | val context = LocalContext.current
56 | val brush by produceState(finalBrush, font) {
57 | val result = runCatching {
58 | font.typefaceLoader.awaitLoad(context, font)
59 | }
60 | value = if (result.isSuccess) finalBrush else Brush.linearGradient(listOf(Color.Red))
61 | }
62 | val textStyle = remember(fontFamily, brush) {
63 | TextStyle(
64 | brush = null,
65 | fontFamily = fontFamily,
66 | fontSize = 16.sp,
67 | fontWeight = FontWeight.W600,
68 | textAlign = TextAlign.Start,
69 | lineHeight = 20.sp,
70 | textMotion = TextMotion.Animated,
71 | textGeometricTransform = TextGeometricTransform(
72 | scaleX = 1f,
73 | skewX = Float.MIN_VALUE
74 | )
75 | )
76 | }
77 | val textMeasurer = rememberTextOnPathMeasurer(cacheSize = 0)
78 | val isAmbient by LocalIsAmbient.current
79 | val cachedPath = remember { Path() }.let { path ->
80 | rememberStateWithSize {
81 | path.arcTo(
82 | rect = Rect(Offset.Zero, size).deflate(textStyle.fontSize.toPx()),
83 | startAngleDegrees = -180f,
84 | sweepAngleDegrees = 359f,
85 | forceMoveTo = true
86 | )
87 | path
88 | }
89 | }
90 | val txt = "Hello Romain! Bonjour Romain!".repeat(2)
91 | val txtList = remember {
92 | txt.map { c ->
93 | val str = c.toString()
94 | textMeasurer.measure(
95 | text = str,
96 | style = textStyle.copy(Color.Red)
97 | )
98 | }
99 | }
100 | val text = remember {
101 | textMeasurer.measure(
102 | text = txt,
103 | style = textStyle.copy(Color.White.copy(alpha = .5f))
104 | )
105 | }
106 | val colors = remember { listOf(Color.Gray, Color.White) }
107 | val paint = rememberStateWithSize {
108 | Paint().also {
109 | it.pathEffect = PathEffect.cornerPathEffect(50f)
110 | Brush.verticalGradient(colors,
111 | startY = center.y - size.height / 6f,
112 | endY = center.y + size.height / 6f,
113 | ).applyTo(size, it, 1f)
114 | it.alpha = 1f
115 | it.style = PaintingStyle.Fill
116 | }
117 | }
118 | val outlinePaint = rememberStateWithSize {
119 | Paint().also {
120 | it.setFrom(paint.get())
121 | Brush.verticalGradient(
122 | colors.asReversed(),
123 | startY = center.y - size.height / 3f,
124 | endY = center.y + size.height / 3f,
125 | ).applyTo(size, it, 1f)
126 | it.strokeWidth = 2.dp.toPx()
127 | it.style = PaintingStyle.Stroke
128 | }
129 | }
130 | OClockCanvas {
131 | val y = 0f
132 | val path = cachedPath.get()
133 | // drawPath(path, Color.Green)
134 | val s = size / 3f
135 | if (false) drawRect(
136 | brush = brush,
137 | topLeft = center.centerAsTopLeft(s),
138 | size = s,
139 | style = Stroke(
140 | width = 1.dp.toPx(),
141 | pathEffect = PathEffect.cornerPathEffect(50f)
142 | )
143 | )
144 | val r = Rect(center.centerAsTopLeft(s), s)
145 | drawContext.canvas.drawRect(r, paint.get())
146 | drawContext.canvas.drawRect(r, outlinePaint.get())
147 | txtList.fastForEachIndexed { index, textOnPathLayoutResult ->
148 | var offset: Float = 0f
149 | var i = 0
150 | while (i < index) {
151 | val current = txtList[i]
152 | offset += current.internalResult.getBoundingBox(current.layoutInput.text.lastIndex).right
153 | // offset += txtList[i].internalResult.size.width - 1
154 | i++
155 | }
156 | drawTextOnPath(
157 | textOnPathLayoutResult,
158 | path = path,
159 | offset = Offset(x = offset, y = y),
160 | // blendMode = BlendMode.Darken
161 | )
162 | }
163 | drawTextOnPath(
164 | text,
165 | path = path,
166 | alpha = .5f,
167 | offset = Offset(x = 0f, y = y),
168 | // blendMode = BlendMode.SrcIn
169 | )
170 | }
171 | }
172 |
173 | @WatchFacePreview
174 | @Composable
175 | private fun TextOnPathExperimentPreview(
176 | @PreviewParameter(WearPreviewSizes::class) size: Dp
177 | ) = WatchFacePreview(size) {
178 | TextOnPathExperiment(finalBrush = SolidColor(Color.Magenta))
179 | }
180 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/elements/ClockHand.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.elements
2 |
3 | import androidx.compose.ui.geometry.CornerRadius
4 | import androidx.compose.ui.geometry.Offset
5 | import androidx.compose.ui.geometry.Size
6 | import androidx.compose.ui.graphics.BlendMode
7 | import androidx.compose.ui.graphics.Brush
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.graphics.drawscope.DrawScope
10 | import androidx.compose.ui.graphics.drawscope.DrawStyle
11 | import androidx.compose.ui.graphics.drawscope.Fill
12 | import androidx.compose.ui.graphics.drawscope.Stroke
13 | import androidx.compose.ui.unit.dp
14 |
15 | fun DrawScope.clockHand(
16 | color: Color,
17 | width: Float = 10.dp.toPx(),
18 | height: Float = size.height / 4f,
19 | style: DrawStyle = Fill,
20 | blendMode: BlendMode
21 | ) {
22 | val halfWidth = width / 2f
23 | val finalHeight = height + halfWidth
24 | drawRoundRect(
25 | color = color,
26 | topLeft = Offset(
27 | x = center.x - width / 2f,
28 | y = center.y - finalHeight + width / 2f
29 | ),
30 | size = Size(width, finalHeight),
31 | cornerRadius = CornerRadius(width),
32 | style = style,
33 | blendMode = blendMode
34 | )
35 | }
36 |
37 | fun DrawScope.clockHand(
38 | brush: Brush,
39 | width: Float = 10.dp.toPx(),
40 | height: Float = size.height / 4f,
41 | style: DrawStyle = Fill,
42 | blendMode: BlendMode = BlendMode.SrcOver
43 | ) {
44 | val padding = if (style is Stroke) style.width else 0f
45 | val finalWidth = width - padding
46 | val halfWidth = finalWidth / 2f
47 | val finalHeight = height + halfWidth - padding / 2f
48 | drawRoundRect(
49 | brush = brush,
50 | topLeft = Offset(
51 | x = center.x - halfWidth,
52 | y = center.y - finalHeight + halfWidth
53 | ),
54 | size = Size(finalWidth, finalHeight),
55 | cornerRadius = CornerRadius(finalWidth),
56 | style = style,
57 | blendMode = blendMode
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/elements/FatHourDigits.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.elements
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.graphics.BlendMode
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.Shadow
9 | import androidx.compose.ui.graphics.drawscope.DrawStyle
10 | import androidx.compose.ui.graphics.drawscope.Fill
11 | import androidx.compose.ui.graphics.layer.GraphicsLayer
12 | import androidx.compose.ui.graphics.layer.drawLayer
13 | import androidx.compose.ui.platform.LocalDensity
14 | import androidx.compose.ui.text.TextStyle
15 | import androidx.compose.ui.text.drawText
16 | import androidx.compose.ui.text.font.Font
17 | import androidx.compose.ui.text.font.FontFamily
18 | import androidx.compose.ui.text.font.FontWeight
19 | import androidx.compose.ui.text.rememberTextMeasurer
20 | import androidx.compose.ui.unit.Dp
21 | import androidx.compose.ui.unit.TextUnit
22 | import androidx.compose.ui.unit.dp
23 | import androidx.compose.ui.unit.sp
24 | import com.louiscad.composeoclockplayground.shared.R
25 | import org.splitties.compose.oclock.LocalIsAmbient
26 | import org.splitties.compose.oclock.LocalTime
27 | import org.splitties.compose.oclock.OClockCanvas
28 | import org.splitties.compose.oclock.sample.extensions.SizeDependentState
29 | import org.splitties.compose.oclock.sample.extensions.blurOffset
30 | import org.splitties.compose.oclock.sample.extensions.center
31 | import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState
32 | import org.splitties.compose.oclock.sample.extensions.sizeForLayer
33 |
34 | @Composable
35 | fun FatHourDigits(
36 | interactiveColor: Color,
37 | interactiveShadowColor: Color = interactiveColor,
38 | ambientShadowColor: Color = interactiveShadowColor,
39 | interactiveShadowRepeat: Int = 1,
40 | ambientShadowRepeat: Int = interactiveShadowRepeat.takeIf { it > 1 } ?: 2,
41 | fontSize: TextUnit = 70.sp,
42 | interactiveBlurRadius: Dp = 10.dp,
43 | ambientBlurRadius: Dp = 10.dp
44 | ) {
45 | val interactive = fatHourDigitsLayer(
46 | textColor = interactiveColor,
47 | shadowColor = interactiveShadowColor,
48 | fontSize = fontSize,
49 | blurRadius = interactiveBlurRadius
50 | )
51 | val ambient = fatHourDigitsLayer(
52 | textColor = Color.Black,
53 | shadowColor = ambientShadowColor,
54 | fontSize = fontSize,
55 | blurRadius = ambientBlurRadius
56 | )
57 | val isAmbient by LocalIsAmbient.current
58 | OClockCanvas {
59 | val count = if (isAmbient) ambientShadowRepeat else interactiveShadowRepeat
60 | val layer = (if (isAmbient) ambient else interactive).get()
61 | repeat(count) {
62 | drawLayer(layer)
63 | }
64 | }
65 | }
66 |
67 | @Composable
68 | fun fatHourDigitsLayer(
69 | textColor: Color = Color.White,
70 | shadowColor: Color = Color.White,
71 | blendMode: BlendMode = BlendMode.SrcOver,
72 | fontSize: TextUnit = 70.sp,
73 | blurRadius: Dp = 10.dp
74 | ): SizeDependentState {
75 | val style = rememberFatHourDigitsTextStyle(
76 | textColor = textColor,
77 | shadowColor = shadowColor,
78 | blendMode = blendMode,
79 | fontSize = fontSize,
80 | blurRadius = blurRadius
81 | )
82 | val hourDigits = rememberHourDigits(style)
83 | return rememberGraphicsLayerAsState { layer ->
84 | layer.blendMode = blendMode
85 | layer.record(size = hourDigits.sizeForLayer()) {
86 | drawText(hourDigits, topLeft = hourDigits.blurOffset())
87 | }
88 | layer.center()
89 | }
90 | }
91 |
92 | @Composable
93 | fun rememberFatHourDigitsTextStyle(
94 | textColor: Color = Color.White,
95 | shadowColor: Color = Color.White,
96 | blendMode: BlendMode = BlendMode.SrcOver,
97 | fontSize: TextUnit = 70.sp,
98 | drawStyle: DrawStyle = Fill,
99 | blurRadius: Dp = 10.dp
100 | ): TextStyle {
101 | val density = LocalDensity.current
102 | val fontFamily = remember { FontFamily(Font(R.font.outfit_extrabold)) }
103 | @Suppress("name_shadowing")
104 | val blurRadius = with(density) { blurRadius.toPx() }
105 | return remember(textColor, shadowColor) {
106 | TextStyle(
107 | color = textColor,
108 | fontSize = fontSize,
109 | // fontWeight = FontWeight.ExtraBold,
110 | drawStyle = drawStyle,
111 | fontFamily = fontFamily,
112 | shadow = if (blurRadius > 0f) Shadow(
113 | color = shadowColor,
114 | blurRadius = blurRadius
115 | ) else null
116 | )
117 | }
118 | }
119 |
120 | @Composable
121 | fun HourDigitsUnCached(
122 | textColor: Color = Color.White,
123 | shadowColor: Color = Color.White,
124 | blendMode: BlendMode = BlendMode.SrcOver,
125 | fontSize: TextUnit = 70.sp,
126 | ) {
127 | val textMeasurer = rememberTextMeasurer()
128 | val density = LocalDensity.current
129 | val style = remember(textColor, shadowColor) {
130 | val fontFamily = FontFamily(
131 | Font(R.font.outfit_extrabold)
132 | )
133 | TextStyle(
134 | color = textColor,
135 | fontSize = fontSize,
136 | fontWeight = FontWeight.ExtraBold,
137 | fontFamily = fontFamily,
138 | shadow = Shadow(
139 | color = shadowColor,
140 | blurRadius = with(density) { 10.dp.toPx() }
141 | )
142 | )
143 | }
144 | val time = LocalTime.current
145 | val measuredText = remember(time.hours) { textMeasurer.measure("${time.hours}", style) }
146 | OClockCanvas {
147 | drawText(
148 | measuredText,
149 | topLeft = center.run {
150 | copy(
151 | x = x - measuredText.size.width / 2f,
152 | y = y - measuredText.size.height / 2f
153 | )
154 | },
155 | blendMode = blendMode
156 | )
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/elements/HourDigitsHelpers.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.elements
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.text.TextLayoutResult
6 | import androidx.compose.ui.text.TextStyle
7 | import androidx.compose.ui.text.rememberTextMeasurer
8 | import org.splitties.compose.oclock.LocalTime
9 |
10 | @Composable
11 | fun rememberHourDigits(textStyle: TextStyle): TextLayoutResult {
12 | val time = LocalTime.current
13 | val textMeasurer = rememberTextMeasurer()
14 | val measuredText = remember(time.hours, textStyle) {
15 | textMeasurer.measure(text = "${time.hours}", style = textStyle)
16 | }
17 | return measuredText
18 | }
19 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/elements/MyPath.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.elements
2 |
3 | import androidx.annotation.FloatRange
4 | import androidx.compose.ui.geometry.Offset
5 | import androidx.compose.ui.geometry.Rect
6 | import androidx.compose.ui.geometry.Size
7 | import androidx.compose.ui.graphics.Path
8 | import androidx.compose.ui.graphics.drawscope.Stroke
9 | import org.splitties.compose.oclock.sample.extensions.cubicTo
10 | import org.splitties.compose.oclock.sample.extensions.lineTo
11 | import org.splitties.compose.oclock.sample.extensions.moveTo
12 | import org.splitties.compose.oclock.sample.extensions.offsetBy
13 | import org.splitties.compose.oclock.sample.extensions.plus
14 | import org.splitties.compose.oclock.sample.extensions.quadraticTo
15 | import org.splitties.compose.oclock.sample.extensions.rotateAround
16 | import kotlin.math.sqrt
17 |
18 | fun Path.setToStar4(
19 | side: Float,
20 | topLeft: Offset = Offset.Zero,
21 | ) {
22 | setToStarX(count = 6, side = side, topLeft = topLeft)
23 | }
24 |
25 | fun Path.setToStarX(
26 | count: Int,
27 | side: Float,
28 | topLeft: Offset = Offset.Zero,
29 | ) {
30 | if (isEmpty.not()) reset()
31 | val half = side / 2f
32 | val topMiddle = topLeft.offsetBy(x = half)
33 | val center = topLeft + half
34 | moveTo(topMiddle)
35 | for (i in 1..count) {
36 | val target = topMiddle.rotateAround(pivot = center, degrees = 360f * i / count)
37 | quadraticTo(center, target)
38 | }
39 | close()
40 | }
41 |
42 | fun Path.setToHeart(
43 | topLeft: Offset = Offset.Zero,
44 | size: Size,
45 | @FloatRange(.0, 1.65) tipSharpnessRatio: Float = 1.1f
46 | ) {
47 | if (isEmpty.not()) reset()
48 | val halfH = size.height / 2f
49 | val halfW = size.width / 2f
50 | val topControl = halfH * .68f
51 | val tipControl = halfH * tipSharpnessRatio
52 | val topMiddle = topLeft.offsetBy(
53 | x = halfW,
54 | y = halfW * .5f
55 | )
56 | moveTo(topMiddle)
57 | cubicTo(
58 | topMiddle.offsetBy(y = -topControl),
59 | topMiddle.offsetBy(x = -halfW, y = -topControl),
60 | topMiddle.offsetBy(x = -halfW) // Left
61 | )
62 | arcTo(Rect(topMiddle, topMiddle.offsetBy(x = -halfW)), startAngleDegrees = 0f, -180f, forceMoveTo = false)
63 | cubicTo(
64 | topMiddle.offsetBy(x = -halfW, y = topControl),
65 | topMiddle.offsetBy(y = tipControl),
66 | topMiddle.copy(y = topLeft.y + size.height) // Bottom
67 | )
68 | cubicTo(
69 | topMiddle.offsetBy(y = tipControl),
70 | topMiddle.offsetBy(x = halfW, y = topControl),
71 | topMiddle.offsetBy(x = halfW) // Right
72 | )
73 | cubicTo(
74 | topMiddle.offsetBy(x = halfW, y = -topControl),
75 | topMiddle.offsetBy(y = -topControl),
76 | topMiddle // Top
77 | )
78 | close()
79 | }
80 |
81 | fun Path.setToSketchyHeart(
82 | size: Size,
83 | topLeft: Offset = Offset.Zero,
84 | ) {
85 | if (isEmpty.not()) reset()
86 | val halfH = size.height / 2f
87 | val halfW = size.width / 2f
88 | val circlesRadius = size.width / 4f
89 | val topPoint = topLeft.offsetBy(x = halfW, y = circlesRadius)
90 | val bottomMiddle = topLeft.offsetBy(x = halfW, y = size.height)
91 | moveTo(bottomMiddle)
92 | val bottomControl = topLeft.offsetBy(x = halfW, y = size.height * .8f)
93 | val leftEdge = topLeft.offsetBy(y = size.height / 4)
94 | val rightEdge = topLeft.offsetBy(x = size.width,y = size.height / 4)
95 | cubicTo(
96 | cp1 = bottomMiddle,
97 | cp2 = bottomControl,
98 | point = bottomMiddle
99 | )
100 | cubicTo(
101 | cp1 = leftEdge.offsetBy(y = circlesRadius),
102 | cp2 = leftEdge.offsetBy(y = - circlesRadius),
103 | point = leftEdge
104 | )
105 | val topMiddle = topLeft.offsetBy(x = halfW)
106 | val topLeftEdge = topMiddle.offsetBy(x = -circlesRadius)
107 | val topRightEdge = topMiddle.offsetBy(x = circlesRadius)
108 | cubicTo(
109 | cp1 = topLeft,
110 | cp2 = topLeft.offsetBy(x = halfW),
111 | point = topLeftEdge
112 | )
113 | cubicTo(
114 | cp1 = topMiddle,
115 | cp2 = topPoint,
116 | point = topPoint
117 | )
118 | cubicTo(
119 | cp1 = topPoint,
120 | cp2 = topMiddle,
121 | point = topRightEdge
122 | )
123 | cubicTo(
124 | cp1 = rightEdge.offsetBy(y = -circlesRadius),
125 | cp2 = rightEdge.offsetBy(y = circlesRadius),
126 | point = rightEdge
127 | )
128 | cubicTo(
129 | cp1 = bottomControl,
130 | cp2 = bottomMiddle,
131 | point = bottomMiddle
132 | )
133 | close()
134 | }
135 |
136 | fun Path.setToKotlinLogo(
137 | side: Float,
138 | topLeft: Offset = Offset.Zero,
139 | ) {
140 | if (isEmpty.not()) reset()
141 | moveTo(topLeft)
142 | lineTo(topLeft.offsetBy(x = side))
143 | lineTo(topLeft.offsetBy(x = side / 2f, y = side / 2f))
144 | lineTo(topLeft.offsetBy(x = side, y = side))
145 | lineTo(topLeft.offsetBy(y = side))
146 | close()
147 | }
148 |
149 | fun Path.setToKotlinLogo(
150 | side: Float,
151 | topLeft: Offset = Offset.Zero,
152 | stroke: Stroke? = null,
153 | ) {
154 | val strokeWidth = stroke?.width ?: 0f
155 | val halfStroke = strokeWidth / 2f
156 | val endOffset = if (stroke != null) sqrt(halfStroke * halfStroke * 2) else 0f
157 | if (isEmpty.not()) reset()
158 | val logoCenterX = side / 2f - endOffset
159 | moveTo(topLeft + halfStroke)
160 | lineTo(topLeft.offsetBy(x = side - endOffset - halfStroke, y = halfStroke))
161 | lineTo(topLeft.offsetBy(x = logoCenterX, y = side / 2f))
162 | lineTo(topLeft.offsetBy(x = side - endOffset - halfStroke, y = side - halfStroke))
163 | lineTo(topLeft.offsetBy(x = halfStroke, y = side - halfStroke))
164 | close()
165 | }
166 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/elements/SinusoidalCrown.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.elements
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.geometry.Size
8 | import androidx.compose.ui.geometry.center
9 | import androidx.compose.ui.graphics.Brush
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.ColorFilter
12 | import androidx.compose.ui.graphics.Path
13 | import androidx.compose.ui.graphics.PathMeasure
14 | import androidx.compose.ui.graphics.SolidColor
15 | import androidx.compose.ui.graphics.StrokeCap
16 | import androidx.compose.ui.graphics.drawscope.Stroke
17 | import androidx.compose.ui.graphics.drawscope.scale
18 | import androidx.compose.ui.tooling.preview.PreviewParameter
19 | import androidx.compose.ui.unit.Dp
20 | import androidx.compose.ui.unit.dp
21 | import org.splitties.compose.oclock.LocalIsAmbient
22 | import org.splitties.compose.oclock.LocalTime
23 | import org.splitties.compose.oclock.OClockCanvas
24 | import org.splitties.compose.oclock.sample.WatchFacePreview
25 | import org.splitties.compose.oclock.sample.WearPreviewSizes
26 | import org.splitties.compose.oclock.sample.elements.vectors.rememberComposeMultiplatformVectorPainter
27 | import org.splitties.compose.oclock.sample.elements.vectors.rememberWearOsLogoVectorPainter
28 | import org.splitties.compose.oclock.sample.extensions.centerAsTopLeft
29 | import org.splitties.compose.oclock.sample.extensions.drawPainter
30 | import org.splitties.compose.oclock.sample.extensions.moveTo
31 | import org.splitties.compose.oclock.sample.extensions.quadraticTo
32 | import org.splitties.compose.oclock.sample.extensions.rememberStateWithSize
33 | import org.splitties.compose.oclock.sample.extensions.rotateAround
34 |
35 | @WatchFacePreview
36 | @Composable
37 | fun SinusoidalCrownPreview(
38 | @PreviewParameter(WearPreviewSizes::class) size: Dp
39 | ) = WatchFacePreview(size) {
40 | val time = LocalTime.current
41 | SinusoidalCrown(getRatio = { time.minutesWithSeconds / 60f })
42 | val wearOsLogo = rememberWearOsLogoVectorPainter()
43 | val composeLogo = rememberComposeMultiplatformVectorPainter()
44 | val washingOutFilter = remember {
45 | ColorFilter.lighting(
46 | add = Color.Gray,
47 | multiply = Color.hsl(hue = 0f, saturation = 0f, lightness = .2f)
48 | )
49 | }
50 | OClockCanvas {
51 | scale(.2f) {
52 | drawPainter(
53 | wearOsLogo,
54 | topLeft = center.centerAsTopLeft(wearOsLogo.intrinsicSize),
55 | colorFilter = washingOutFilter,
56 | alpha = .5f
57 | )
58 | }
59 | drawPainter(
60 | painter = composeLogo,
61 | topLeft = center.centerAsTopLeft(composeLogo.intrinsicSize)
62 | )
63 | }
64 | }
65 |
66 | @Composable
67 | fun SinusoidalCrown(
68 | getRatio: () -> Float,
69 | interactiveFillBrush: Brush = remember { SolidColor(Color.Cyan) },
70 | ambientFillBrush: Brush = remember { SolidColor(Color.Cyan) }
71 | ) {
72 | val pathMeasure = remember { PathMeasure() }
73 | val cachedPath = remember { Path() }.let { path ->
74 | rememberStateWithSize {
75 | path.also {
76 | it.setToSineWaveLike(size)
77 | pathMeasure.setPath(it, forceClosed = false)
78 | }
79 | }
80 | }
81 | val wholePathStroke = rememberStateWithSize {
82 | Stroke(width = 1.dp.toPx())
83 | }
84 | val progressPathStroke = rememberStateWithSize {
85 | Stroke(width = 3.dp.toPx(), cap = StrokeCap.Round)
86 | }
87 | val segment = remember { Path() }
88 | val isAmbient by LocalIsAmbient.current
89 | OClockCanvas {
90 | val path by cachedPath
91 | val ratio = getRatio()
92 | segment.reset()
93 | pathMeasure.getSegment(
94 | startDistance = 0f,
95 | stopDistance = ratio * pathMeasure.length,
96 | destination = segment
97 | )
98 | drawPath(path, Color.LightGray, style = wholePathStroke.get())
99 | val fillBrush = if (isAmbient) ambientFillBrush else interactiveFillBrush
100 | drawPath(segment, fillBrush, style = progressPathStroke.get())
101 | }
102 | }
103 |
104 | private fun Path.setToSineWaveLike(size: Size) {
105 | if (isEmpty.not()) reset()
106 | val center = size.center
107 | val amplitude = size.height / 10f
108 | val start = Offset(center.x, amplitude / 2f)
109 | moveTo(start)
110 | val count = 30
111 | for (i in 1..count) {
112 | val ratio = i.toFloat() / count
113 | val anglePeriod = 360f / count
114 | quadraticTo(
115 | p1 = start.copy(y = 0f).rotateAround(center, anglePeriod * (i - .75f)),
116 | p2 = start.rotateAround(pivot = center, degrees = anglePeriod * (i - .5f))
117 | )
118 | quadraticTo(
119 | p1 = start.copy(y = amplitude).rotateAround(center, anglePeriod * (i - .25f)),
120 | p2 = start.rotateAround(pivot = center, degrees = anglePeriod * (i))
121 | )
122 | }
123 | close()
124 | }
125 |
126 | private fun Path.setToDecoration(size: Size) {
127 | val start = Offset(size.width / 2f, 0f)
128 | moveTo(start)
129 | val amplitude = size.height / 10f
130 | val count = 30
131 | for (i in 0..count) {
132 | val ratio = i.toFloat() / count
133 | val topForward = start.rotateAround(pivot = size.center, degrees = 360f * ratio)
134 | val bottomForward = start.copy(y = amplitude).rotateAround(size.center, 360f * ratio)
135 | quadraticTo(
136 | p1 = topForward,
137 | p2 = bottomForward
138 | )
139 | val p3 = bottomForward.rotateAround(pivot = size.center, degrees = 360f / count / 2f)
140 | val p4 = topForward.rotateAround(pivot = size.center, degrees = 360f / count / 2f)
141 | quadraticTo(
142 | p3, p4
143 | )
144 | }
145 | close()
146 | }
147 |
148 | private fun Path.setToAccidentalArt1(size: Size) {
149 | val center = size.center
150 | val amplitude = size.height / 10f
151 | val start = Offset(center.x, amplitude / 2f)
152 | moveTo(start)
153 | val count = 60
154 | for (i in 0..count) {
155 | val ratio = i.toFloat() / count
156 | val anglePeriod = 360f / count
157 | val topForward = start.rotateAround(pivot = center, degrees = 360f * ratio)
158 | val bottomForward = start.copy(y = amplitude).rotateAround(size.center, 360f * ratio)
159 | quadraticTo(
160 | p1 = topForward,
161 | p2 = start.rotateAround(center, anglePeriod)
162 | )
163 | val p3 = bottomForward.rotateAround(pivot = size.center, degrees = 360f / count / 2f)
164 | val p4 = topForward.rotateAround(pivot = size.center, degrees = 360f / count / 2f)
165 | quadraticTo(
166 | p3, p4
167 | )
168 | }
169 | close()
170 | }
171 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/elements/vectors/wearOsLogo.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.elements.vectors
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.graphics.SolidColor
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.StrokeCap
8 | import androidx.compose.ui.graphics.StrokeJoin
9 | import androidx.compose.ui.graphics.vector.ImageVector
10 | import androidx.compose.ui.graphics.PathFillType
11 | import androidx.compose.ui.graphics.vector.VectorPainter
12 | import androidx.compose.ui.graphics.vector.path
13 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
14 | import androidx.compose.ui.unit.dp
15 |
16 | @Composable
17 | fun rememberWearOsLogoVectorPainter(): VectorPainter {
18 | return rememberVectorPainter(remember { wearOsIcon() })
19 | }
20 |
21 | fun wearOsIcon(): ImageVector = ImageVector.Builder(
22 | name = "WearOsIcon",
23 | defaultWidth = 469.7.dp,
24 | defaultHeight = 359.dp,
25 | viewportWidth = 469.7f,
26 | viewportHeight = 359f
27 | ).apply {
28 | path(
29 | fill = SolidColor(Color(0xFF00A94B)),
30 | fillAlpha = 1.0f,
31 | stroke = null,
32 | strokeAlpha = 1.0f,
33 | strokeLineWidth = 1.0f,
34 | strokeLineCap = StrokeCap.Butt,
35 | strokeLineJoin = StrokeJoin.Miter,
36 | strokeLineMiter = 1.0f,
37 | pathFillType = PathFillType.NonZero
38 | ) {
39 | moveTo(418.59999999999997f, 146.4f)
40 | arcTo(
41 | 48.7f,
42 | 48.7f,
43 | 0f,
44 | isMoreThanHalf = false,
45 | isPositiveArc = true,
46 | 369.9f,
47 | 195.10000000000002f
48 | )
49 | arcTo(48.7f, 48.7f, 0f, isMoreThanHalf = false, isPositiveArc = true, 321.2f, 146.4f)
50 | arcTo(
51 | 48.7f,
52 | 48.7f,
53 | 0f,
54 | isMoreThanHalf = false,
55 | isPositiveArc = true,
56 | 418.59999999999997f,
57 | 146.4f
58 | )
59 | close()
60 | }
61 | path(
62 | fill = SolidColor(Color(0xFFFF4131)),
63 | fillAlpha = 1.0f,
64 | stroke = null,
65 | strokeAlpha = 1.0f,
66 | strokeLineWidth = 1.0f,
67 | strokeLineCap = StrokeCap.Butt,
68 | strokeLineJoin = StrokeJoin.Miter,
69 | strokeLineMiter = 1.0f,
70 | pathFillType = PathFillType.NonZero
71 | ) {
72 | moveTo(469.7f, 46.3f)
73 | arcTo(45.5f, 45.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 424.2f, 91.8f)
74 | arcTo(45.5f, 45.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 378.7f, 46.3f)
75 | arcTo(45.5f, 45.5f, 0f, isMoreThanHalf = false, isPositiveArc = true, 469.7f, 46.3f)
76 | close()
77 | }
78 | path(
79 | fill = SolidColor(Color(0xFFFFBC00)),
80 | fillAlpha = 1.0f,
81 | stroke = null,
82 | strokeAlpha = 1.0f,
83 | strokeLineWidth = 1.0f,
84 | strokeLineCap = StrokeCap.Butt,
85 | strokeLineJoin = StrokeJoin.Miter,
86 | strokeLineMiter = 1.0f,
87 | pathFillType = PathFillType.NonZero
88 | ) {
89 | moveTo(305.5f, 359f)
90 | curveToRelative(-17.4f, 0f, -34.1f, -10.1f, -41.6f, -27f)
91 | lineTo(144.6f, 64.1f)
92 | curveToRelative(-10.2f, -23f, 0.1f, -49.9f, 23.1f, -60.1f)
93 | curveToRelative(23f, -10.2f, 49.9f, 0.1f, 60.1f, 23.1f)
94 | lineToRelative(119.3f, 267.9f)
95 | curveToRelative(10.2f, 23f, -0.1f, 49.9f, -23.1f, 60.1f)
96 | curveTo(318f, 357.8f, 311.7f, 359f, 305.5f, 359f)
97 | close()
98 | }
99 | path(
100 | fill = SolidColor(Color(0xFF0085F7)),
101 | fillAlpha = 1.0f,
102 | stroke = null,
103 | strokeAlpha = 1.0f,
104 | strokeLineWidth = 1.0f,
105 | strokeLineCap = StrokeCap.Butt,
106 | strokeLineJoin = StrokeJoin.Miter,
107 | strokeLineMiter = 1.0f,
108 | pathFillType = PathFillType.NonZero
109 | ) {
110 | moveTo(164.7f, 358.3f)
111 | curveToRelative(-19f, 0f, -37.1f, -11f, -45.3f, -29.4f)
112 | lineTo(4.3f, 70.3f)
113 | curveTo(-6.8f, 45.3f, 4.4f, 16f, 29.4f, 4.9f)
114 | reflectiveCurveTo(83.7f, 5f, 94.8f, 30f)
115 | lineToRelative(115.1f, 258.6f)
116 | curveToRelative(11.1f, 25f, -0.1f, 54.3f, -25.1f, 65.4f)
117 | curveTo(178.3f, 356.9f, 171.4f, 358.3f, 164.7f, 358.3f)
118 | close()
119 | }
120 | }.build()
121 |
122 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/AndroidTextPaint.kt:
--------------------------------------------------------------------------------
1 | package extensions
2 |
3 | import android.text.TextPaint
4 | import androidx.annotation.VisibleForTesting
5 | import androidx.compose.ui.geometry.Size
6 | import androidx.compose.ui.geometry.isSpecified
7 | import androidx.compose.ui.graphics.BlendMode
8 | import androidx.compose.ui.graphics.Brush
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.Paint
11 | import androidx.compose.ui.graphics.PaintingStyle
12 | import androidx.compose.ui.graphics.ShaderBrush
13 | import androidx.compose.ui.graphics.Shadow
14 | import androidx.compose.ui.graphics.SolidColor
15 | import androidx.compose.ui.graphics.asComposePaint
16 | import androidx.compose.ui.graphics.drawscope.DrawStyle
17 | import androidx.compose.ui.graphics.drawscope.Fill
18 | import androidx.compose.ui.graphics.drawscope.Stroke
19 | import androidx.compose.ui.graphics.isSpecified
20 | import androidx.compose.ui.graphics.toArgb
21 | import androidx.compose.ui.text.style.TextDecoration
22 | import kotlin.math.roundToInt
23 |
24 | internal class AndroidTextPaint(flags: Int, density: Float) : TextPaint(flags) {
25 | init {
26 | this.density = density
27 | }
28 |
29 | // A wrapper to use Compose Paint APIs on this TextPaint
30 | private val composePaint: Paint = this.asComposePaint()
31 |
32 | private var textDecoration: TextDecoration = TextDecoration.None
33 |
34 | @VisibleForTesting
35 | internal var shadow: Shadow = Shadow.None
36 |
37 | private var drawStyle: DrawStyle? = null
38 |
39 | fun setTextDecoration(textDecoration: TextDecoration?) {
40 | if (textDecoration == null) return
41 | if (this.textDecoration != textDecoration) {
42 | this.textDecoration = textDecoration
43 | isUnderlineText = TextDecoration.Underline in this.textDecoration
44 | isStrikeThruText = TextDecoration.LineThrough in this.textDecoration
45 | }
46 | }
47 |
48 | fun setShadow(shadow: Shadow?) {
49 | if (shadow == null) return
50 | if (this.shadow != shadow) {
51 | this.shadow = shadow
52 | if (this.shadow == Shadow.None) {
53 | clearShadowLayer()
54 | } else {
55 | setShadowLayer(
56 | correctBlurRadius(this.shadow.blurRadius),
57 | this.shadow.offset.x,
58 | this.shadow.offset.y,
59 | this.shadow.color.toArgb()
60 | )
61 | }
62 | }
63 | }
64 |
65 | fun setColor(color: Color) {
66 | if (color.isSpecified) {
67 | composePaint.color = color
68 | composePaint.shader = null
69 | }
70 | }
71 |
72 | fun setBrush(brush: Brush?, size: Size, alpha: Float = Float.NaN) {
73 | // if size is unspecified and brush is not null, nothing should be done.
74 | // it basically means brush is given but size is not yet calculated at this time.
75 | if ((brush is SolidColor && brush.value.isSpecified) ||
76 | (brush is ShaderBrush && size.isSpecified)) {
77 | // alpha is always applied even if Float.NaN is passed to applyTo function.
78 | // if it's actually Float.NaN, we simply send the current value
79 | brush.applyTo(
80 | size,
81 | composePaint,
82 | if (alpha.isNaN()) composePaint.alpha else alpha.coerceIn(0f, 1f)
83 | )
84 | } else if (brush == null) {
85 | composePaint.shader = null
86 | }
87 | }
88 |
89 | fun setDrawStyle(drawStyle: DrawStyle) {
90 | if (this.drawStyle != drawStyle) {
91 | this.drawStyle = drawStyle
92 | when (drawStyle) {
93 | Fill -> {
94 | // Stroke properties such as strokeWidth, strokeMiter are not re-set because
95 | // Fill style should make those properties no-op. Next time the style is set
96 | // as Stroke, stroke properties get re-set as well.
97 | composePaint.style = PaintingStyle.Fill
98 | }
99 | is Stroke -> {
100 | composePaint.style = PaintingStyle.Stroke
101 | composePaint.strokeWidth = drawStyle.width
102 | composePaint.strokeMiterLimit = drawStyle.miter
103 | composePaint.strokeJoin = drawStyle.join
104 | composePaint.strokeCap = drawStyle.cap
105 | composePaint.pathEffect = drawStyle.pathEffect
106 | }
107 | }
108 | }
109 | }
110 |
111 | // BlendMode is only available to DrawScope.drawText.
112 | // not intended to be used by TextStyle/SpanStyle.
113 | var blendMode: BlendMode by composePaint::blendMode
114 | }
115 |
116 | /**
117 | * Accepts an alpha value in the range [0f, 1f] then maps to an integer value
118 | * in [0, 255] range.
119 | */
120 | internal fun TextPaint.setAlpha(alpha: Float) {
121 | if (!alpha.isNaN()) {
122 | val alphaInt = alpha.coerceIn(0f, 1f).times(255).roundToInt()
123 | setAlpha(alphaInt)
124 | }
125 | }
126 |
127 | internal fun correctBlurRadius(blurRadius: Float) = if (blurRadius == 0f) {
128 | Float.MIN_VALUE
129 | } else {
130 | blurRadius
131 | }
132 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/Colors.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | fun List.loop(): List = this + this.first()
6 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/DrawScope.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import android.graphics.Paint
4 | import android.graphics.drawable.Drawable
5 | import androidx.annotation.FloatRange
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.geometry.Size
8 | import androidx.compose.ui.graphics.BlendMode
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.ColorFilter
11 | import androidx.compose.ui.graphics.DefaultAlpha
12 | import androidx.compose.ui.graphics.Path
13 | import androidx.compose.ui.graphics.asAndroidPath
14 | import androidx.compose.ui.graphics.drawscope.DrawScope
15 | import androidx.compose.ui.graphics.drawscope.DrawStyle
16 | import androidx.compose.ui.graphics.drawscope.Fill
17 | import androidx.compose.ui.graphics.drawscope.translate
18 | import androidx.compose.ui.graphics.drawscope.withTransform
19 | import androidx.compose.ui.graphics.nativeCanvas
20 | import androidx.compose.ui.graphics.painter.Painter
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.unit.LayoutDirection
23 | import androidx.compose.ui.unit.TextUnitType
24 | import androidx.core.graphics.drawable.updateBounds
25 | import extensions.AndroidTextPaint
26 | import extensions.setAlpha
27 | import org.splitties.compose.oclock.sample.extensions.text.TextOnPathLayoutResult
28 |
29 | fun DrawScope.drawOval(
30 | color: Color,
31 | size: Size = this.size,
32 | center: Offset = this.center,
33 | @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
34 | style: DrawStyle = Fill,
35 | colorFilter: ColorFilter? = null,
36 | blendMode: BlendMode = DrawScope.DefaultBlendMode
37 | ) = drawOval(
38 | color = color,
39 | topLeft = center.centerAsTopLeft(size),
40 | alpha = alpha,
41 | size = size,
42 | style = style,
43 | colorFilter = colorFilter,
44 | blendMode = blendMode
45 | )
46 |
47 | fun DrawScope.drawArc(
48 | color: Color,
49 | startAngle: Float,
50 | sweepAngle: Float,
51 | useCenter: Boolean,
52 | size: Size = this.size,
53 | center: Offset = this.center,
54 | @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
55 | style: DrawStyle = Fill,
56 | colorFilter: ColorFilter? = null,
57 | blendMode: BlendMode = DrawScope.DefaultBlendMode
58 | ) = drawArc(
59 | color = color,
60 | topLeft = center.centerAsTopLeft(size),
61 | startAngle = startAngle,
62 | sweepAngle = sweepAngle,
63 | useCenter = useCenter,
64 | alpha = alpha,
65 | size = size,
66 | style = style,
67 | colorFilter = colorFilter,
68 | blendMode = blendMode
69 | )
70 |
71 | fun DrawScope.drawPainter(
72 | painter: Painter,
73 | topLeft: Offset = Offset.Zero,
74 | size: Size = painter.intrinsicSize,
75 | alpha: Float = DefaultAlpha,
76 | colorFilter: ColorFilter? = null
77 | ) {
78 | withTransform({
79 | translate(topLeft)
80 | }) {
81 | with(painter) {
82 | draw(size, alpha, colorFilter)
83 | }
84 | }
85 | }
86 |
87 | fun DrawScope.drawTextOnPath(
88 | textLayoutResult: TextOnPathLayoutResult,
89 | path: Path,
90 | offset: Offset = Offset.Zero,
91 | @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
92 | blendMode: BlendMode = DrawScope.DefaultBlendMode
93 | ) {
94 | drawContext.canvas.nativeCanvas.drawTextOnPath(
95 | textLayoutResult.layoutInput.text.text,
96 | path.asAndroidPath(),
97 | offset.x,
98 | offset.y,
99 | textPaint.also {
100 | it.setFrom(
101 | textLayoutResult = textLayoutResult,
102 | blendMode = blendMode,
103 | alpha = alpha
104 | )
105 | }
106 | )
107 | }
108 |
109 | private val textPaint = AndroidTextPaint(
110 | flags = Paint.ANTI_ALIAS_FLAG or Paint.SUBPIXEL_TEXT_FLAG,
111 | density = 1f
112 | )
113 |
114 | context(DrawScope)
115 | internal fun AndroidTextPaint.setFrom(
116 | textLayoutResult: TextOnPathLayoutResult,
117 | blendMode: BlendMode,
118 | @FloatRange(from = 0.0, to = 1.0) alpha: Float,
119 | ) {
120 | val layoutInput = textLayoutResult.layoutInput
121 | val style = layoutInput.style
122 | this.density = layoutInput.density.density
123 | setColor(style.color)
124 | setBrush(style.brush, size)
125 | setShadow(style.shadow)
126 | setDrawStyle(style.drawStyle ?: Fill)
127 | this.blendMode = blendMode
128 | when (style.fontSize.type) {
129 | TextUnitType.Sp -> textSize = style.fontSize.toPx()
130 | TextUnitType.Em -> {
131 | textSize *= style.fontSize.value
132 | }
133 | else -> {} // Do nothing
134 | }
135 | textAlign = style.textAlign.toPaintAlign(layoutDirection)
136 | setTextDecoration(style.textDecoration)
137 |
138 | setAlpha(alpha)
139 |
140 | // See internal fun AndroidTextPaint.applySpanStyle in TextPaintExtensions.android.kt
141 | typeface = textLayoutResult.typeface
142 | fontFeatureSettings = style.fontFeatureSettings
143 | val textGeometricTransform = style.textGeometricTransform
144 | textScaleX = textGeometricTransform?.scaleX ?: 1f
145 | textSkewX = textGeometricTransform?.skewX ?: 0f
146 | if (style.letterSpacing.type == TextUnitType.Sp && style.letterSpacing.value != 0.0f) {
147 | val emWidth = textSize * textScaleX
148 | val letterSpacingPx = style.letterSpacing.toPx()
149 | // Do nothing if emWidth is 0.0f.
150 | if (emWidth != 0.0f) {
151 | letterSpacing = letterSpacingPx / emWidth
152 | }
153 | } else if (style.letterSpacing.type == TextUnitType.Em) {
154 | letterSpacing = style.letterSpacing.value
155 | }
156 | }
157 |
158 | private fun TextAlign.toPaintAlign(layoutDirection: LayoutDirection): Paint.Align = when (this) {
159 | TextAlign.Left -> Paint.Align.LEFT
160 | TextAlign.Right -> Paint.Align.RIGHT
161 | TextAlign.Center -> Paint.Align.CENTER
162 | TextAlign.Start -> when (layoutDirection) {
163 | LayoutDirection.Ltr -> Paint.Align.LEFT
164 | LayoutDirection.Rtl -> Paint.Align.RIGHT
165 | }
166 | TextAlign.End -> when (layoutDirection) {
167 | LayoutDirection.Ltr -> Paint.Align.RIGHT
168 | LayoutDirection.Rtl -> Paint.Align.LEFT
169 | }
170 | else -> TextAlign.Start.toPaintAlign(layoutDirection)
171 | }
172 |
173 | fun DrawScope.drawDrawable(
174 | drawable: Drawable,
175 | topLeft: Offset = Offset.Zero,
176 | size: Size = this.size
177 | ) {
178 | val left = topLeft.x
179 | val top = topLeft.y
180 | val right = left + size.width
181 | val bottom = top + size.height
182 | drawable.updateBounds(
183 | left = left.toInt(),
184 | top = top.toInt(),
185 | right = right.toInt(),
186 | bottom = bottom.toInt(),
187 | )
188 | drawable.draw(drawContext.canvas.nativeCanvas)
189 | }
190 |
191 | inline fun DrawScope.translate(offset: Offset, block: DrawScope.() -> Unit) {
192 | translate(left = offset.x, top = offset.y, block = block)
193 | }
194 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/DrawTransform.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package org.splitties.compose.oclock.sample.extensions
4 |
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.drawscope.DrawTransform
7 |
8 | inline fun DrawTransform.translate(offset: Offset) {
9 | translate(left = offset.x, top = offset.y)
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/GraphicsLayer.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package org.splitties.compose.oclock.sample.extensions
4 |
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.graphics.drawscope.DrawScope
8 | import androidx.compose.ui.graphics.layer.GraphicsLayer
9 | import androidx.compose.ui.graphics.layer.drawLayer
10 | import androidx.compose.ui.graphics.rememberGraphicsLayer
11 | import androidx.compose.ui.platform.LocalLayoutDirection
12 | import androidx.compose.ui.unit.Density
13 | import androidx.compose.ui.unit.IntSize
14 | import androidx.compose.ui.unit.LayoutDirection
15 | import androidx.compose.ui.unit.toIntSize
16 |
17 | context(SizeDependentState.Scope)
18 | inline fun GraphicsLayer.centerHorizontally() {
19 | translationX = scope().size.width / 2f - size.width / 2f
20 | }
21 |
22 | context(SizeDependentState.Scope)
23 | inline fun GraphicsLayer.center() {
24 | translationX = scope().size.width / 2f - size.width / 2f
25 | translationY = scope().size.height / 2f - size.height / 2f
26 | }
27 |
28 | @Composable
29 | fun rememberFullSizeRecordedGraphicsLayer(
30 | configureLayer: (GraphicsLayer) -> Unit = {},
31 | block: DrawScope.() -> Unit
32 | ): SizeDependentState {
33 | return rememberGraphicsLayerAsState { layer ->
34 | configureLayer(layer)
35 | layer.record(size = size.toIntSize()) {
36 | block()
37 | }
38 | }
39 | }
40 |
41 | @Composable
42 | fun rememberGraphicsLayerAsState(
43 | key1: Any? = Unit,
44 | key2: Any? = Unit,
45 | block: context(SimplifiedGraphicsLayerContext) SizeDependentState.Scope.(layer: GraphicsLayer) -> Unit
46 | ): SizeDependentState {
47 | val layoutDir = LocalLayoutDirection.current
48 | val simplifiedGraphicsLayerContext = remember(layoutDir) {
49 | SimplifiedGraphicsLayerContext(layoutDir)
50 | }
51 | return rememberGraphicsLayer().rememberAsStateWithSize(key1, key2, simplifiedGraphicsLayerContext) {
52 | with(simplifiedGraphicsLayerContext) {
53 | block(this@rememberAsStateWithSize, it)
54 | }
55 | }
56 | }
57 |
58 | inline fun DrawScope.drawLayer(layer: SizeDependentState) {
59 | drawLayer(layer.get())
60 | }
61 |
62 | class SimplifiedGraphicsLayerContext internal constructor(private val layoutDir: LayoutDirection) {
63 |
64 | /**
65 | * Any previously content drawn into a [GraphicsLayer] is dropped at some point
66 | * when it is no longer visible (e.g. when switching to other apps),
67 | * and we need to re-record the content.
68 | *
69 | * To avoid this problem, we re-record every time we become visible again.
70 | */
71 | context(SizeDependentState.Scope)
72 | fun GraphicsLayer.record(
73 | size: IntSize,
74 | block: DrawScope.() -> Unit
75 | ) {
76 | record(
77 | density = asDensity(),
78 | layoutDirection = layoutDir,
79 | size = size
80 | ) {
81 | block()
82 | }
83 | }
84 |
85 | private fun SizeDependentState.Scope.asDensity(): Density = this
86 | }
87 |
88 | @PublishedApi
89 | internal inline fun SizeDependentState.Scope.scope(): SizeDependentState.Scope = this
90 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/Math.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import kotlin.math.PI
4 |
5 | fun Float.degreesToRadians(): Float = (PI / 180 * this).toFloat()
6 | fun Float.radiansToDegrees(): Float = this * (180 / PI).toFloat()
7 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/Offset.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.drawscope.DrawScope
6 | import androidx.compose.ui.unit.IntSize
7 | import kotlin.math.atan2
8 | import kotlin.math.cos
9 | import kotlin.math.sin
10 |
11 | context (DrawScope)
12 | fun Offset.rotate(degrees: Float): Offset {
13 | return rotateAround(pivot = center, degrees = degrees)
14 | }
15 |
16 | fun Offset.rotateAround(
17 | pivot: Offset,
18 | degrees: Float,
19 | ): Offset {
20 | val angle = Math.toRadians(degrees.toDouble()).toFloat()
21 | val cx = pivot.x
22 | val cy = pivot.y
23 | val offsetX = x - cx // Offset backwards to match the center of the Unit Circle.
24 | val offsetY = y - cy // Offset backwards to match the center of the Unit Circle.
25 |
26 | val offsetRotatedX = offsetX * cos(angle) - offsetY * sin(angle)
27 | val offsetRotatedY = offsetX * sin(angle) + offsetY * cos(angle)
28 | return Offset(
29 | x = offsetRotatedX + cx, // Offset back, forward.
30 | y = offsetRotatedY + cy // Offset back, forward.
31 | )
32 | }
33 |
34 | fun Offset.centerAsTopLeft(size: Size): Offset {
35 | return copy(x = x - size.width / 2f, y = y - size.height / 2f)
36 | }
37 |
38 | fun Offset.centerAsTopLeft(side: Float): Offset {
39 | return copy(x = x - side / 2f, y = y - side / 2f)
40 | }
41 |
42 | fun Offset.centerAsTopLeft(size: IntSize): Offset {
43 | return copy(x = x - size.width / 2f, y = y - size.height / 2f)
44 | }
45 |
46 | fun Offset.topCenterAsTopLeft(size: Size): Offset {
47 | return copy(x = x - size.width / 2f)
48 | }
49 |
50 | fun Offset.topCenterAsTopLeft(size: IntSize): Offset {
51 | return copy(x = x - size.width / 2f)
52 | }
53 |
54 | fun Offset.topCenterAsTopLeft(width: Float): Offset {
55 | return copy(x = x - width / 2f)
56 | }
57 |
58 | operator fun Offset.plus(amount: Float): Offset = copy(x = x + amount, y = y + amount)
59 | operator fun Offset.plus(size: Size): Offset = copy(x = x + size.width, y = y + size.height)
60 |
61 | operator fun Offset.minus(size: Size): Offset = copy(x = x - size.width, y = y - size.height)
62 | operator fun Offset.minus(amount: Float): Offset = copy(x = x - amount, y = y - amount)
63 |
64 | fun Offset.offsetBy(x: Float = 0f, y: Float = 0f): Offset {
65 | return copy(x = this.x + x, y = this.y + y)
66 | }
67 |
68 | fun Offset.angleTo(other: Offset): Float {
69 | return Math.toDegrees(atan2(
70 | y = y * other.x - x * other.y,
71 | x = x * other.x + y * other.y
72 | ).toDouble()).toFloat()
73 | }
74 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/Paint.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.ui.graphics.Paint
4 |
5 | fun Paint.setFrom(other: Paint) {
6 | asFrameworkPaint().set(other.asFrameworkPaint())
7 | }
8 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/Path.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 |
3 | package org.splitties.compose.oclock.sample.extensions
4 |
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.graphics.Path
7 |
8 | inline fun Path.moveTo(position: Offset) {
9 | moveTo(x = position.x, y = position.y)
10 | }
11 |
12 | inline fun Path.lineTo(position: Offset) {
13 | lineTo(x = position.x, y = position.y)
14 | }
15 |
16 | inline fun Path.quadraticTo(
17 | p1: Offset,
18 | p2: Offset
19 | ) = quadraticTo(
20 | x1 = p1.x,
21 | y1 = p1.y,
22 | x2 = p2.x,
23 | y2 = p2.y
24 | )
25 |
26 | inline fun Path.cubicTo(
27 | cp1: Offset,
28 | cp2: Offset,
29 | point: Offset
30 | ) = cubicTo(
31 | x1 = cp1.x,
32 | y1 = cp1.y,
33 | x2 = cp2.x,
34 | y2 = cp2.y,
35 | x3 = point.x,
36 | y3 = point.y
37 | )
38 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/PathPattern.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.geometry.Rect
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.graphics.Path
8 | import androidx.compose.ui.graphics.PathMeasure
9 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope
10 | import androidx.compose.ui.graphics.drawscope.DrawScope
11 | import androidx.compose.ui.graphics.drawscope.DrawStyle
12 | import androidx.compose.ui.graphics.drawscope.Fill
13 | import androidx.compose.ui.graphics.drawscope.Stroke
14 | import androidx.compose.ui.graphics.drawscope.rotate
15 | import androidx.compose.ui.graphics.layer.CompositingStrategy
16 | import androidx.compose.ui.graphics.layer.GraphicsLayer
17 | import androidx.compose.ui.graphics.layer.drawLayer
18 | import androidx.compose.ui.unit.IntSize
19 | import androidx.compose.ui.unit.toIntSize
20 | import org.splitties.compose.oclock.OClockCanvas
21 | import kotlin.math.ceil
22 | import kotlin.math.floor
23 | import kotlin.math.roundToInt
24 |
25 | @Composable
26 | fun PathPattern(
27 | slices: Int = 1,
28 | slicesCompositingStrategy: CompositingStrategy = CompositingStrategy.Offscreen,
29 | getRatio: () -> Float = { 1f },
30 | getSliceDrawStyle: SizeDependentState.Scope.() -> DrawStyle = { Fill },
31 | updateSlice: SizeDependentState.Scope.(Path) -> Unit,
32 | drawWireframeSlicePath: (DrawScope.(path: Path) -> Unit)? = null,
33 | drawPath: DrawScope.(path: Path) -> Unit = { drawPath(it, Color.White) },
34 | onDrawWireframe: ContentDrawScope.(reusedSliceLayer: GraphicsLayer) -> Unit = { drawContent() },
35 | onDrawWithSliceContent: ContentDrawScope.(
36 | reusedSliceLayer: GraphicsLayer,
37 | index: Int
38 | ) -> Unit = { _, _ -> drawContent() }
39 | ) {
40 | val pathMeasure = remember { PathMeasure() }
41 | val slicePath = remember { Path() }.rememberAsStateWithSize(updateSlice) {
42 | it.reset()
43 | updateSlice(it)
44 | pathMeasure.setPath(it, forceClosed = false)
45 | }
46 | val lastSegmentPath = remember { Path() }.rememberAsStateWithSize(getRatio) {
47 | val slicesInverse = 60f / slices
48 | val ratio = getRatio() * 60 % slicesInverse / slicesInverse
49 | val end = pathMeasure.length * ratio
50 | it.reset()
51 | pathMeasure.getSegment(startDistance = 0f, stopDistance = end, destination = it)
52 | }
53 | val sliceWireframeLayer = rememberGraphicsLayerAsState(drawWireframeSlicePath) {
54 | val path = slicePath.get()
55 | it.compositingStrategy = CompositingStrategy.Offscreen
56 | it.record(size = size.toIntSize()) {
57 | drawWireframeSlicePath?.let {
58 | repeat(slices) { i ->
59 | rotate(360f * i / slices.toFloat()) {
60 | it(path)
61 | }
62 | }
63 | }
64 | }
65 | }
66 | val sliceLayer = rememberGraphicsLayerAsState(getSliceDrawStyle) {
67 | val path = slicePath.get()
68 | it.compositingStrategy = slicesCompositingStrategy
69 | if (slicesCompositingStrategy == CompositingStrategy.Offscreen) {
70 |
71 | val bounds = path.getBounds()
72 | val layerSize = bounds.size
73 | val width = when (val style = getSliceDrawStyle()) {
74 | Fill -> 0f
75 | is Stroke -> style.width
76 | }
77 | val halfW = width / 2f
78 | val offset = bounds.topLeft
79 | val margin = halfW + 1f
80 | it.record(size = (layerSize + margin * 2f).toIntSize()) {
81 | translate(-offset + margin) {
82 | drawPath(path)
83 | }
84 | }
85 | offset.let { (x, y) ->
86 | it.translationX = x - margin
87 | it.translationY = y - margin
88 | }
89 | } else {
90 | it.record(size = size.toIntSize()) {
91 | drawPath(path)
92 | }
93 | }
94 | }
95 | val lastSliceLayer = rememberGraphicsLayerAsState {
96 | it.compositingStrategy = CompositingStrategy.Offscreen
97 | val path = lastSegmentPath.get()
98 | val bounds = path.getBounds()
99 | val layerSize = bounds.size
100 | val width = when (val style = getSliceDrawStyle()) {
101 | Fill -> 0f
102 | is Stroke -> style.width
103 | }
104 | val halfW = width / 2f
105 | val offset = bounds.topLeft
106 | val margin = halfW + 1f
107 | it.record(size = (layerSize + margin * 2f).toIntSize()) {
108 | translate(-offset + margin) {
109 | drawPath(path)
110 | }
111 | }
112 | offset.let { (x, y) ->
113 | it.translationX = x - margin
114 | it.translationY = y - margin
115 | }
116 | }
117 | val layerScopeFactory = remember { LayerDrawScopeFactory() }
118 | OClockCanvas {
119 | val layerDrawScope = layerScopeFactory.getLayerDrawScope(this)
120 | if (drawWireframeSlicePath != null) layerDrawScope.withLayer(sliceWireframeLayer.get()) {
121 | onDrawWireframe(it)
122 | }
123 | val ratio = getRatio()
124 | val last = floor(ratio * slices).roundToInt()
125 | for (i in 0.. Unit) {
154 | this.layer = layer
155 | try {
156 | block(layer)
157 | } finally {
158 | this.layer = null
159 | }
160 | }
161 |
162 | private var layer: GraphicsLayer? = null
163 | override fun drawContent() {
164 | drawLayer(layer ?: return)
165 | }
166 | }
167 |
168 | private fun Rect.toIntSize(): IntSize = IntSize(
169 | width = ceil(width).toInt(),
170 | height = ceil(height).toInt()
171 | )
172 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/PatternsMath.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import kotlin.math.PI
4 | import kotlin.math.sin
5 |
6 | fun circleDiameterInCircularPattern(
7 | outerCircle: Float,
8 | n: Int
9 | ): Float {
10 | val a = sin(PI / n).toFloat()
11 | return (a * outerCircle) / (1 + a)
12 | }
13 |
14 | fun circleRadiusInCircularPattern(
15 | outerCircle: Float,
16 | n: Int
17 | ): Float = circleDiameterInCircularPattern(outerCircle, n) / 2f
18 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/Size.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.ui.geometry.Size
4 | import androidx.compose.ui.unit.IntSize
5 | import kotlin.math.floor
6 | import kotlin.math.min
7 |
8 |
9 | operator fun Size.minus(pixels: Float): Size {
10 | return Size(width - pixels, height - pixels)
11 | }
12 |
13 | operator fun Size.plus(pixels: Float): Size {
14 | return Size(width + pixels, height + pixels)
15 | }
16 |
17 | fun Size.Companion.square(pixels: Float): Size {
18 | return Size(pixels, pixels)
19 | }
20 |
21 | /**
22 | * Returns the largest Size that would fit into [other], while keeping the aspect ratio.
23 | */
24 | fun Size.fitIn(other: Size): Size {
25 | val maxFactor = other.maxDimension / maxDimension
26 | val minFactor = other.minDimension / minDimension
27 | val factor = min(maxFactor, minFactor)
28 | return this * factor
29 | }
30 |
31 | fun Size.toFlooredIntSize(): IntSize {
32 | return IntSize(floor(width).toInt(), floor(height).toInt())
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/SizeDependentState.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.SnapshotMutationPolicy
5 | import androidx.compose.runtime.Stable
6 | import androidx.compose.runtime.cache
7 | import androidx.compose.runtime.currentComposer
8 | import androidx.compose.runtime.derivedStateOf
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableFloatStateOf
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.neverEqualPolicy
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.runtime.structuralEqualityPolicy
16 | import androidx.compose.ui.geometry.Offset
17 | import androidx.compose.ui.geometry.Size
18 | import androidx.compose.ui.geometry.center
19 | import androidx.compose.ui.graphics.drawscope.DrawScope
20 | import androidx.compose.ui.unit.Density
21 | import kotlin.reflect.KProperty
22 |
23 | @Composable fun T.rememberAsStateWithSize(
24 | key1: Any? = Unit,
25 | key2: Any? = Unit,
26 | key3: Any? = Unit,
27 | calculation: SizeDependentState.Scope.(T) -> Unit
28 | ): SizeDependentState = currentComposer.let {
29 | val t = this
30 | it.cache(
31 | it.changed(value = key1) or
32 | it.changed(value = key2) or
33 | it.changed(value = key3) or
34 | it.changed(value = calculation)
35 | ) {
36 | SizeDependentState(
37 | calculation = { calculation(t);t },
38 | policy = neverEqualPolicy()
39 | )
40 | }
41 | }
42 |
43 | @Composable
44 | fun rememberStateWithSize(
45 | calculation: SizeDependentState.Scope.() -> T
46 | ): SizeDependentState = remember(calculation) {
47 | SizeDependentState(calculation)
48 | }
49 |
50 | @Composable
51 | fun rememberStateWithSize(
52 | key1: Any?,
53 | calculation: SizeDependentState.Scope.() -> T
54 | ): SizeDependentState = remember(
55 | key1 = key1,
56 | key2 = calculation
57 | ) {
58 | SizeDependentState(calculation)
59 | }
60 |
61 | @Composable
62 | fun rememberStateWithSize(
63 | key1: Any?,
64 | key2: Any?,
65 | calculation: SizeDependentState.Scope.() -> T
66 | ): SizeDependentState = remember(
67 | key1 = key1,
68 | key2 = key2,
69 | key3 = calculation
70 | ) {
71 | SizeDependentState(calculation)
72 | }
73 |
74 | @Composable
75 | fun rememberStateWithSize(
76 | vararg keys: Any?,
77 | calculation: SizeDependentState.Scope.() -> T
78 | ): SizeDependentState = remember(*keys, calculation) {
79 | SizeDependentState(calculation)
80 | }
81 |
82 | @Stable
83 | class SizeDependentState(
84 | private val calculation: Scope.() -> T,
85 | policy: SnapshotMutationPolicy = structuralEqualityPolicy()
86 | ) {
87 |
88 | interface Scope : Density {
89 | val size: Size
90 | val center: Offset
91 | }
92 |
93 | context (Scope)
94 | operator fun getValue(thisRef: Nothing?, property: KProperty<*>?): T = get()
95 |
96 | context (DrawScope)
97 | operator fun getValue(thisRef: Nothing?, property: KProperty<*>?): T = get()
98 |
99 | context (DrawScope, Scope)
100 | operator fun getValue(thisRef: Nothing?, property: KProperty<*>?): T = get()
101 |
102 | context (Scope)
103 | fun provideDelegate(thisRef: Nothing?, property: KProperty<*>?): SizeDependentState {
104 | return this
105 | }
106 |
107 | context (DrawScope)
108 | fun provideDelegate(thisRef: Nothing?, property: KProperty<*>?): SizeDependentState {
109 | get() // Ensure it's activated if declared, in case side-effects are needed at draw time.
110 | return this
111 | }
112 |
113 | context (DrawScope, Scope)
114 | fun provideDelegate(thisRef: Nothing?, property: KProperty<*>?): SizeDependentState {
115 | get() // Ensure it's activated if declared, in case side-effects are needed at draw time.
116 | return this
117 | }
118 |
119 | context (DrawScope)
120 | fun pro(thisRef: Nothing?, property: KProperty<*>): T = get()
121 |
122 | context (Scope)
123 | fun get(): T = get(size, density, fontScale)
124 |
125 | context (DrawScope)
126 | private fun getDrawScopeOnly(): T = get(size, density, fontScale)
127 |
128 | context (DrawScope)
129 | fun get(): T = get(size, density, fontScale)
130 |
131 | context (DrawScope, Scope)
132 | fun get(): T = getDrawScopeOnly()
133 |
134 | private fun get(size: Size, density: Float, fontScale: Float): T {
135 | scope.also {
136 | it.size = size
137 | it.density = density
138 | it.fontScale = fontScale
139 | }
140 | return value
141 | }
142 |
143 | private val value by derivedStateOf(policy = policy) { calculation(scope) }
144 |
145 | private val scope = object : Scope {
146 | override var density: Float by mutableFloatStateOf(1f)
147 | override var fontScale: Float by mutableFloatStateOf(1f)
148 | override var size: Size by mutableStateOf(Size.Unspecified)
149 | override val center: Offset get() = size.center
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/TextLayoutResult.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.text.TextLayoutResult
5 | import androidx.compose.ui.unit.IntSize
6 | import androidx.compose.ui.unit.toIntSize
7 | import androidx.compose.ui.unit.toSize
8 |
9 | fun TextLayoutResult.sizeForLayer(): IntSize {
10 | val spaceForBlur = (layoutInput.style.shadow?.blurRadius ?: 0f) * 2
11 | return (size.toSize() + spaceForBlur).toIntSize()
12 | }
13 |
14 | fun TextLayoutResult.blurOffset(): Offset = layoutInput.style.shadow?.blurRadius?.let {
15 | Offset(it, it)
16 | } ?: Offset.Zero
17 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/text/TextOnPathLayoutResult.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions.text
2 |
3 | import android.graphics.Typeface
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.ui.text.TextLayoutInput
7 | import androidx.compose.ui.text.TextLayoutResult
8 |
9 | class TextOnPathLayoutResult(
10 | val internalResult: TextLayoutResult,
11 | typeFaceState: State
12 | ) {
13 | val layoutInput: TextLayoutInput get() = internalResult.layoutInput
14 | val typeface by typeFaceState
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/extensions/text/TextOnPathMeasurerHelper.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.extensions.text
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.platform.LocalDensity
6 | import androidx.compose.ui.platform.LocalFontFamilyResolver
7 | import androidx.compose.ui.platform.LocalLayoutDirection
8 | import androidx.compose.ui.text.TextMeasurer
9 |
10 | /**
11 | * This value should reflect the default cache size for TextMeasurer.
12 | */
13 | private val DefaultCacheSize: Int = 8
14 |
15 | /**
16 | * Creates and remembers a [TextMeasurer]. All parameters that are required for [TextMeasurer]
17 | * except [cacheSize] are read from CompositionLocals. Created [TextMeasurer] carries an internal
18 | * [androidx.compose.ui.text.TextLayoutCache] with [cacheSize] capacity. Provide 0 for [cacheSize] to opt-out from internal
19 | * caching behavior.
20 | *
21 | * @param cacheSize Capacity of internal cache inside [TextMeasurer]. Size unit is the number of
22 | * unique text layout inputs that are measured. Value of this parameter highly depends on the
23 | * consumer use case. Provide a cache size that is in line with how many distinct text layouts are
24 | * going to be calculated by this measurer repeatedly. If you are animating font attributes, or any
25 | * other layout affecting input, cache can be skipped because most repeated measure calls would miss
26 | * the cache.
27 | */
28 | @Composable
29 | fun rememberTextOnPathMeasurer(
30 | cacheSize: Int = DefaultCacheSize
31 | ): TextOnPathMeasurer {
32 | val fontFamilyResolver = LocalFontFamilyResolver.current
33 | val density = LocalDensity.current
34 | val layoutDirection = LocalLayoutDirection.current
35 |
36 | return remember(fontFamilyResolver, density, layoutDirection, cacheSize) {
37 | TextOnPathMeasurer(fontFamilyResolver, density, layoutDirection, cacheSize)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/utils/Bits.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.utils
2 |
3 | @PublishedApi
4 | internal fun Int.getBitAt(position: Int, last: Int = 32): Boolean = this shr (last - position) and 1 > 0
5 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/utils/FiveMinutesLayoutOrder.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.utils
2 |
3 | import androidx.annotation.IntRange
4 |
5 | @JvmInline
6 | value class FiveMinutesLayoutOrder(
7 | private val storage: Int
8 | ) {
9 | init {
10 | require((storage and 0b11111_00000_00000_00000).countOneBits() == 1)
11 | require((storage and 0b00000_11111_00000_00000).countOneBits() == 2)
12 | require((storage and 0b00000_00000_11111_00000).countOneBits() == 3)
13 | require((storage and 0b00000_00000_00000_11111).countOneBits() == 4)
14 | }
15 |
16 | companion object {
17 | val linear = FiveMinutesLayoutOrder(0b10000_11000_11100_11110)
18 | val symmetricalSpread = FiveMinutesLayoutOrder(0b00100_01010_10101_11011)
19 | val symmetricalPacked = FiveMinutesLayoutOrder(0b00100_01010_01110_11011)
20 | val symmetricalEdges = FiveMinutesLayoutOrder(0b00100_10001_10101_11011)
21 | }
22 |
23 | fun showFor(
24 | @IntRange(0, 59) minute: Int,
25 | @IntRange(0, 4) minuteMarkIndex: Int,
26 | ): Boolean {
27 | require(minute in 0..59)
28 | require(minuteMarkIndex in 0..4)
29 | return storage.getBitAt(
30 | position = ((minute - 1) % 5) * 5 + minuteMarkIndex,
31 | last = 19
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/utils/FiveMinutesSlicePattern.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.utils
2 |
3 | import androidx.annotation.IntRange
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.geometry.Rect
6 | import androidx.compose.ui.graphics.Path
7 | import androidx.compose.ui.graphics.drawscope.DrawScope
8 | import androidx.compose.ui.graphics.drawscope.rotate
9 | import androidx.compose.ui.graphics.drawscope.scale
10 | import androidx.compose.ui.graphics.layer.CompositingStrategy
11 | import androidx.compose.ui.graphics.layer.drawLayer
12 | import org.splitties.compose.oclock.LocalTime
13 | import org.splitties.compose.oclock.OClockCanvas
14 | import org.splitties.compose.oclock.sample.extensions.SizeDependentState
15 | import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState
16 | import org.splitties.compose.oclock.sample.extensions.rememberStateWithSize
17 | import org.splitties.compose.oclock.sample.extensions.toFlooredIntSize
18 |
19 | @Composable
20 | fun FiveMinutesSlicePattern(
21 | layoutOrder: FiveMinutesLayoutOrder = FiveMinutesLayoutOrder.symmetricalSpread,
22 | mirrored: Boolean = true,
23 | createSliceData: SizeDependentState.Scope.(slicePath: Path, bounds: Rect) -> T,
24 | drawSliceContent: DrawScope.(fiveMinutesIndex: Int, sliceData: T) -> Unit
25 | ) {
26 | val slicePathAndBounds = rememberStateWithSize {
27 | val path = Path().also {
28 | it.setToPieSlice(size, 30f, centered = false)
29 | }
30 | path to path.getBounds()
31 | }
32 | val sliceData = rememberStateWithSize {
33 | val (path, bounds) = slicePathAndBounds.get()
34 | createSliceData(path, bounds)
35 | }
36 | val minuteLayers = List(5) { i ->
37 | minuteSliceLayer(
38 | slicePath = slicePathAndBounds,
39 | sliceData = sliceData,
40 | fiveMinutesIndex = i,
41 | drawSliceContent = drawSliceContent
42 | )
43 | }
44 | val time = LocalTime.current
45 | OClockCanvas {
46 | val max = 12
47 | val minutes = time.minutes
48 | for (i in 0.. minuteSliceLayer(
79 | slicePath: SizeDependentState>,
80 | sliceData: SizeDependentState,
81 | @IntRange(0, 4) fiveMinutesIndex: Int,
82 | drawSliceContent: DrawScope.(fiveMinutesIndex: Int, sliceData: T) -> Unit
83 | ) = rememberGraphicsLayerAsState { layer ->
84 | require(fiveMinutesIndex in 0..4)
85 | val (path, bounds) = slicePath.get()
86 | layer.compositingStrategy = CompositingStrategy.Offscreen
87 | layer.setPathOutline(path)
88 | layer.clip = true
89 | layer.translationX = center.x
90 | @Suppress("name_shadowing") val sliceData = sliceData.get()
91 | layer.record(size = bounds.size.toFlooredIntSize()) {
92 | drawSliceContent(fiveMinutesIndex, sliceData)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/utils/PieSlicePath.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.utils
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.geometry.center
6 | import androidx.compose.ui.geometry.toRect
7 | import androidx.compose.ui.graphics.Path
8 | import org.splitties.compose.oclock.sample.extensions.lineTo
9 | import org.splitties.compose.oclock.sample.extensions.moveTo
10 |
11 | fun Path.setToPieSlice(size: Size, degrees: Float, centered: Boolean = true) {
12 | reset()
13 | moveTo(size.center)
14 | lineTo(size.center.copy(y = 0f))
15 | arcTo(size.toRect(), startAngleDegrees = -90f, sweepAngleDegrees = degrees, forceMoveTo = false)
16 | lineTo(size.center)
17 | close()
18 | if (centered.not()) translate(Offset(x = -size.center.x, y = 0f))
19 | }
20 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/watchfaces/AllWatchFaces.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | val allWatchFaces: List<@Composable () -> Unit> = listOf(
6 | // { LoopingSeconds() },
7 | // { LightLinesWatchFace() },
8 | { KotlinFanClock() },
9 | { ComposeFanClock() },
10 | { BasicAnalogClock() },
11 | )
12 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/watchfaces/BasicAnalogClock.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.graphics.BlendMode
7 | import androidx.compose.ui.graphics.Brush
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.graphics.SolidColor
10 | import androidx.compose.ui.graphics.StrokeCap
11 | import androidx.compose.ui.graphics.drawscope.Fill
12 | import androidx.compose.ui.graphics.drawscope.Stroke
13 | import androidx.compose.ui.graphics.drawscope.rotate
14 | import androidx.compose.ui.tooling.preview.PreviewParameter
15 | import androidx.compose.ui.unit.Dp
16 | import androidx.compose.ui.unit.dp
17 | import org.splitties.compose.oclock.LocalIsAmbient
18 | import org.splitties.compose.oclock.LocalTime
19 | import org.splitties.compose.oclock.OClockCanvas
20 | import org.splitties.compose.oclock.sample.ComposeOClockWatermark
21 | import org.splitties.compose.oclock.sample.WatchFacePreview
22 | import org.splitties.compose.oclock.sample.WearPreviewSizes
23 | import org.splitties.compose.oclock.sample.elements.clockHand
24 | import org.splitties.compose.oclock.sample.extensions.rotate
25 |
26 | @Composable
27 | fun BasicAnalogClock() {
28 | Background()
29 | val textBrush = remember {
30 | Brush.sweepGradient(
31 | 4.5f/8f to Color(0x7C00E5FF),
32 | 5.1f/8f to Color(0xFF00E5FF),
33 | 6.2f/8f to Color(0xFF00E5FF),
34 | 6.5f/8f to Color(0xFFFFFF8D),
35 | )
36 | }
37 | ComposeOClockWatermark(textBrush)
38 | HoursHand()
39 | MinutesHand()
40 | SecondsHand()
41 | CenterDot()
42 | }
43 |
44 | @Composable
45 | private fun Background() {
46 | val isAmbient by LocalIsAmbient.current
47 | OClockCanvas {
48 | if (isAmbient.not()) drawCircle(kotlinDarkBg)
49 | }
50 | }
51 |
52 | @Composable
53 | private fun HoursHand() {
54 | val time = LocalTime.current
55 | val isAmbient by LocalIsAmbient.current
56 | OClockCanvas {
57 | rotate(degrees = time.hourWithMinutes * 30) {
58 | val width = 10.dp.toPx()
59 | clockHand(
60 | brush = SolidColor(kotlinLogoColors[0]),
61 | width = width,
62 | height = size.height / 4f,
63 | style = if (isAmbient) Stroke(width = 3.dp.toPx()) else Fill,
64 | blendMode = BlendMode.Plus
65 | )
66 | }
67 | }
68 | }
69 |
70 | @Composable
71 | private fun MinutesHand() {
72 | val time = LocalTime.current
73 | val isAmbient by LocalIsAmbient.current
74 | OClockCanvas {
75 | rotate(degrees = time.minutes * 6f) {
76 | val width = 10.dp.toPx()
77 | clockHand(
78 | brush = SolidColor(kotlinLogoColors[1]),
79 | width = width,
80 | height = size.height * 3 / 8f,
81 | style = if (isAmbient) Stroke(width = 3.dp.toPx()) else Fill,
82 | blendMode = BlendMode.Plus
83 | )
84 | }
85 | }
86 | }
87 |
88 | @Composable
89 | private fun SecondsHand() {
90 | val time = LocalTime.current
91 | val isAmbient by LocalIsAmbient.current
92 | OClockCanvas {
93 | if (isAmbient) return@OClockCanvas
94 | val color = kotlinLogoColors[2]
95 | drawLine(
96 | color,
97 | start = center,
98 | end = center.copy(y = size.height / 32f).rotate(time.seconds * 6f),
99 | strokeWidth = 2.dp.toPx(),
100 | blendMode = BlendMode.Lighten,
101 | cap = StrokeCap.Round
102 | )
103 | }
104 | }
105 |
106 | @Composable
107 | private fun CenterDot() {
108 | val isAmbient by LocalIsAmbient.current
109 | OClockCanvas {
110 | drawCircle(kotlinDarkBg, radius = 6.dp.toPx(), blendMode = BlendMode.Xor)
111 | drawCircle(
112 | color = if (isAmbient) Color.Black else kotlinLogoColors[2],
113 | radius = 5.dp.toPx()
114 | )
115 | if (isAmbient) {
116 | drawCircle(
117 | Color.Gray,
118 | radius = 5.dp.toPx()
119 | )
120 | drawCircle(
121 | Color.Black,
122 | radius = 2.dp.toPx()
123 | )
124 | }
125 | }
126 | }
127 |
128 | private val kotlinDarkBg = Color(0xFF1B1B1B)
129 | private val kotlinBlue = Color(0xFF7F52FF)
130 | private val kotlinLogoColors = listOf(
131 | kotlinBlue,
132 | Color(0xFF_C811E2),
133 | Color(0xFF_E54857),
134 | )
135 |
136 | @WatchFacePreview
137 | @Composable
138 | private fun BasicAnalogClockPreview(
139 | @PreviewParameter(WearPreviewSizes::class) size: Dp
140 | ) = WatchFacePreview(size) {
141 | BasicAnalogClock()
142 | }
143 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/watchfaces/LightLinesWatchFace.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.geometry.Size
7 | import androidx.compose.ui.graphics.BlendMode
8 | import androidx.compose.ui.graphics.Brush
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.drawscope.rotate
11 | import androidx.compose.ui.graphics.drawscope.withTransform
12 | import androidx.compose.ui.graphics.layer.CompositingStrategy
13 | import androidx.compose.ui.graphics.layer.drawLayer
14 | import androidx.compose.ui.unit.Dp
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.unit.toIntSize
17 | import androidx.compose.ui.util.lerp
18 | import org.splitties.compose.oclock.OClockCanvas
19 | import org.splitties.compose.oclock.sample.elements.FatHourDigits
20 | import org.splitties.compose.oclock.sample.extensions.angleTo
21 | import org.splitties.compose.oclock.sample.extensions.rememberGraphicsLayerAsState
22 | import org.splitties.compose.oclock.sample.extensions.rotateAround
23 | import org.splitties.compose.oclock.sample.utils.FiveMinutesLayoutOrder
24 | import org.splitties.compose.oclock.sample.utils.FiveMinutesSlicePattern
25 |
26 | @Composable
27 | fun LightLinesWatchFace() {
28 | OClockCanvas {
29 | drawRect(Color.Black)
30 | }
31 | val blue = Color.hsl(180f, 1f, .9f)
32 | val yellow = Color(0xFFF5D370)
33 | LightLinesMinutes(blue, cutAngle = 53f)
34 | FatHourDigits(interactiveColor = blue, ambientShadowRepeat = 2)
35 | }
36 |
37 | @Composable
38 | fun LightLinesMinutes(
39 | color: Color = Color.White,
40 | layoutOrder: FiveMinutesLayoutOrder = FiveMinutesLayoutOrder.symmetricalSpread,
41 | lineStartTopPadding: Dp = 8.dp,
42 | lineStartBottomPadding: Dp = 74.dp,
43 | cutAngle: Float = -1f,
44 | mirrored: Boolean = true
45 | ) {
46 | val brush = remember { lightBrush(color) }
47 | val horizontalLine = rememberGraphicsLayerAsState {
48 | it.compositingStrategy = CompositingStrategy.Offscreen
49 | it.record(Size(size.width * 1.3f, 8.dp.toPx()).toIntSize()) {
50 | drawRect(brush)
51 | }
52 | }
53 | FiveMinutesSlicePattern(
54 | layoutOrder = layoutOrder,
55 | mirrored = mirrored,
56 | createSliceData = { slicePath, bounds ->
57 | val topLimit = lineStartTopPadding.toPx()
58 | val bottomLimit = bounds.height - lineStartBottomPadding.toPx()
59 | val band = bottomLimit - topLimit
60 | val middlePoint = Offset(x = 0f, y = topLimit + band / 2f)
61 | val sliceTip = Offset(x = 0f, y = bounds.height)
62 | val topLeftCorner = Offset.Zero
63 | val topRightCorner = topLeftCorner.rotateAround(sliceTip, degrees = 30f)
64 | val vector = topRightCorner - middlePoint
65 | SliceData(
66 | topLimit = topLimit,
67 | bottomLimit = bottomLimit,
68 | lineAngle = vector.angleTo(Offset(x = 1f, y = 0f))
69 | )
70 | },
71 | drawSliceContent = { i, sliceData ->
72 | val max = 5
73 | val startingPoint = lerp(
74 | start = sliceData.topLimit,
75 | stop = sliceData.bottomLimit,
76 | fraction = i.toFloat() / (max - 1)
77 | )
78 | val lineLayer = horizontalLine.get()
79 | withTransform({
80 | translate(top = startingPoint - lineLayer.size.height / 2f)
81 | rotate(sliceData.lineAngle, pivot = Offset.Zero)
82 | translate(left = -lineLayer.size.height.toFloat())
83 | }) {
84 | drawLayer(lineLayer)
85 | }
86 | if (cutAngle != -1f) rotate(
87 | degrees = sliceData.lineAngle + cutAngle, pivot = Offset.Zero
88 | ) {
89 | drawRect(
90 | Color.Red,
91 | size = Size(width = size.height, 40.dp.toPx()),
92 | blendMode = BlendMode.Clear
93 | )
94 | }
95 | }
96 | )
97 | }
98 |
99 | private fun lightBrush(color: Color = Color.White): Brush {
100 | val transparent = color.copy(alpha = 0f)
101 | val mid = color.copy(alpha = .5f)
102 | return Brush.verticalGradient(
103 | 0f to transparent,
104 | .45f to mid,
105 | .45f to color,
106 | .5f to color,
107 | .55f to color,
108 | .55f to mid,
109 | 1f to transparent
110 | )
111 | }
112 |
113 | private class SliceData(
114 | val topLimit: Float,
115 | val bottomLimit: Float,
116 | val lineAngle: Float
117 | )
118 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/watchfaces/WatchFaceSwitcher.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableIntStateOf
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.runtime.setValue
8 | import androidx.compose.ui.tooling.preview.PreviewParameter
9 | import androidx.compose.ui.unit.Dp
10 | import androidx.compose.ui.unit.dp
11 | import org.splitties.compose.oclock.OClockCanvas
12 | import org.splitties.compose.oclock.TapEvent
13 | import org.splitties.compose.oclock.sample.WatchFacePreview
14 | import org.splitties.compose.oclock.sample.WearPreviewSizes
15 |
16 | @Composable
17 | fun WatchFaceSwitcher() {
18 | var index by remember { mutableIntStateOf(0) }
19 | OClockCanvas(
20 | onTap = handler@{ event ->
21 | val touchEdgeWidth = 48.dp.toPx()
22 | val isOnLeftEdge = event.position.x <= touchEdgeWidth
23 | val isOnRightEdge = isOnLeftEdge.not() &&
24 | event.position.x >= size.width - touchEdgeWidth
25 | if (isOnLeftEdge.not() && isOnRightEdge.not()) return@handler false
26 | if (event is TapEvent.Up) {
27 | val lastIndex = allWatchFaces.lastIndex
28 | index = when {
29 | isOnLeftEdge -> index - 1
30 | else -> index + 1
31 | }.let {
32 | when {
33 | it < 0 -> lastIndex
34 | it > lastIndex -> 0
35 | else -> it
36 | }
37 | }
38 | }
39 | true
40 | }
41 | ) { }
42 | allWatchFaces[index]()
43 | }
44 |
45 | @WatchFacePreview
46 | @Composable
47 | private fun WatchFaceSwitcherPreview(
48 | @PreviewParameter(WearPreviewSizes::class) size: Dp
49 | ) = WatchFacePreview(size) {
50 | WatchFaceSwitcher()
51 | }
52 |
--------------------------------------------------------------------------------
/shared/src/main/kotlin/watchfaces/hansie/HansieClock.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces.hansie
2 |
3 | import android.icu.text.MessageFormat
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.derivedStateOf
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.geometry.Offset
9 | import androidx.compose.ui.graphics.BlendMode
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.SolidColor
12 | import androidx.compose.ui.graphics.drawscope.rotate
13 | import androidx.compose.ui.platform.LocalConfiguration
14 | import androidx.compose.ui.text.TextStyle
15 | import androidx.compose.ui.text.drawText
16 | import androidx.compose.ui.text.font.FontWeight
17 | import androidx.compose.ui.text.style.TextGeometricTransform
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.unit.sp
20 | import org.splitties.compose.oclock.CurrentTime
21 | import org.splitties.compose.oclock.LocalIsAmbient
22 | import org.splitties.compose.oclock.LocalTextMeasurerWithoutCache
23 | import org.splitties.compose.oclock.LocalTime
24 | import org.splitties.compose.oclock.OClockCanvas
25 | import org.splitties.compose.oclock.internal.InternalComposeOClockApi
26 | import org.splitties.compose.oclock.sample.WatchFacePreview
27 | import java.util.Locale
28 |
29 | @Suppress("DEPRECATION")
30 | @Composable
31 | fun HansieClock(locale: Locale = LocalConfiguration.current.locale) {
32 | val isAmbient by LocalIsAmbient.current
33 | HansieBackground()
34 | if (isAmbient.not()) {
35 | HansieSecondsHand(locale)
36 | }
37 | HansieMinutesHand(locale)
38 | HansieHoursHand(locale)
39 | HansieCenterDot()
40 | }
41 |
42 | @Composable
43 | private fun HansieBackground() {
44 | val isAmbient by LocalIsAmbient.current
45 | OClockCanvas {
46 | if (isAmbient.not()) drawCircle(kotlinDarkBg)
47 | }
48 | }
49 |
50 | @Composable
51 | private fun HansieHoursHand(locale: Locale) {
52 | val style = remember {
53 | TextStyle(
54 | fontSize = 26.sp,
55 | fontWeight = FontWeight.W600,
56 | color = Color.White
57 | )
58 | }
59 | HansieHand(locale, style, HansieHandType.Hour)
60 | }
61 |
62 | @Composable
63 | private fun HansieMinutesHand(locale: Locale) {
64 | val style = remember {
65 | TextStyle(
66 | fontSize = 26.sp,
67 | fontWeight = FontWeight.W400,
68 | color = Color.DarkGray.copy(alpha = 0.8f),
69 | textGeometricTransform = TextGeometricTransform(
70 | scaleX = 1.25f,
71 | skewX = Float.MIN_VALUE
72 | ),
73 | )
74 | }
75 |
76 | HansieHand(locale, style, HansieHandType.Minute)
77 | }
78 |
79 | @Composable
80 | private fun HansieSecondsHand(locale: Locale) {
81 | val style = remember {
82 | TextStyle(
83 | fontSize = 14.sp,
84 | fontWeight = FontWeight.W200,
85 | color = Color.LightGray.copy(alpha = 0.6f),
86 | textGeometricTransform = TextGeometricTransform(
87 | scaleX = 1.50f,
88 | skewX = Float.MIN_VALUE
89 | ),
90 | )
91 | }
92 |
93 | HansieHand(locale, style, HansieHandType.Second)
94 | }
95 |
96 | @Composable
97 | private fun HansieHand(
98 | locale: Locale,
99 | style: TextStyle,
100 | type: HansieHandType
101 | ) {
102 | val time = LocalTime.current
103 |
104 | val timeFormatter = remember(locale) {
105 | MessageFormat(
106 | "{0,spellout,full}",
107 | locale
108 | )
109 | }
110 | @OptIn(InternalComposeOClockApi::class)
111 | val measurer = LocalTextMeasurerWithoutCache.current
112 |
113 | val textLayoutResult by remember(timeFormatter) {
114 | derivedStateOf {
115 | val handValueInt = type.handValue(time).toInt()
116 | val timeText = timeFormatter.format(arrayOf(handValueInt))
117 | measurer.measure(
118 | timeText,
119 | style
120 | )
121 | }
122 | }
123 |
124 | OClockCanvas {
125 | val handValue = type.handValue(time)
126 | val degrees = (handValue * type.factor) - 90f
127 | rotate(degrees = degrees) {
128 | drawText(
129 | brush = style.brush ?: SolidColor(style.color),
130 | textLayoutResult = textLayoutResult,
131 | topLeft = center.plus(Offset(25f, -textLayoutResult.size.height / 2f))
132 | )
133 | }
134 | }
135 | }
136 |
137 | enum class HansieHandType(val factor: Float) {
138 | Hour(30f) {
139 | override fun handValue(current: CurrentTime): Float = current.hours.toFloat()
140 | },
141 | Minute(6f) {
142 | override fun handValue(current: CurrentTime): Float = current.minutes.toFloat()
143 | },
144 | Second(6f) {
145 | override fun handValue(current: CurrentTime): Float = current.seconds.toFloat()
146 | };
147 |
148 | abstract fun handValue(current: CurrentTime): Float
149 | }
150 |
151 | @Composable
152 | private fun HansieCenterDot() {
153 | val isAmbient by LocalIsAmbient.current
154 | OClockCanvas {
155 | drawCircle(kotlinDarkBg, radius = 6.dp.toPx(), blendMode = BlendMode.Xor)
156 | drawCircle(
157 | color = if (isAmbient) Color.Black else kotlinLogoColors[2],
158 | radius = 5.dp.toPx()
159 | )
160 | if (isAmbient) {
161 | drawCircle(
162 | Color.Gray,
163 | radius = 5.dp.toPx()
164 | )
165 | drawCircle(
166 | Color.Black,
167 | radius = 2.dp.toPx()
168 | )
169 | }
170 | }
171 | }
172 |
173 | private val kotlinDarkBg = Color(0xFF1B1B1B)
174 | private val kotlinBlue = Color(0xFF7F52FF)
175 | private val kotlinLogoColors = listOf(
176 | kotlinBlue,
177 | Color(0xFF_C811E2),
178 | Color(0xFF_E54857),
179 | )
180 |
181 | @WatchFacePreview
182 | @Composable
183 | private fun HansieClockPreview(
184 | ) = WatchFacePreview(WatchFacePreview.Size.large) {
185 | HansieClock(Locale.US)
186 | }
187 |
188 | @WatchFacePreview
189 | @Composable
190 | private fun HansieClockPreviewFr(
191 | ) = WatchFacePreview(WatchFacePreview.Size.large) {
192 | HansieClock(Locale.FRENCH)
193 | }
194 |
195 | @WatchFacePreview
196 | @Composable
197 | private fun HansieClockPreviewSa(
198 | ) = WatchFacePreview(WatchFacePreview.Size.large) {
199 | HansieClock(Locale("af", "ZA"))
200 | }
201 |
--------------------------------------------------------------------------------
/shared/src/main/res/drawable/jetpack_compose.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
22 |
23 |
26 |
27 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/shared/src/main/res/font/outfit_extrabold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/main/res/font/outfit_extrabold.ttf
--------------------------------------------------------------------------------
/shared/src/main/res/values/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | - @array/com_google_android_gms_fonts_certs_dev
6 | - @array/com_google_android_gms_fonts_certs_prod
7 |
8 |
9 | -
10 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
11 |
12 |
13 |
14 | -
15 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/shared/src/test/kotlin/org/splitties/compose/oclock/sample/watchfaces/BasicAnalogClockTest.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | class BasicAnalogClockTest(device: WearDevice) : DeviceClockScreenshotTest(device) {
6 | @Composable
7 | override fun Clock() {
8 | BasicAnalogClock()
9 | }
10 | }
--------------------------------------------------------------------------------
/shared/src/test/kotlin/org/splitties/compose/oclock/sample/watchfaces/ClockScreenshotTest.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalRoborazziApi::class)
2 |
3 | package org.splitties.compose.oclock.sample.watchfaces
4 |
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.test.junit4.createComposeRule
9 | import androidx.compose.ui.test.onRoot
10 | import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
11 | import com.github.takahirom.roborazzi.RoborazziOptions
12 | import com.github.takahirom.roborazzi.ThresholdValidator
13 | import com.github.takahirom.roborazzi.captureRoboImage
14 | import com.louiscad.composeoclockplayground.shared.BuildConfig
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import org.junit.Assume
17 | import org.junit.Assume.assumeFalse
18 | import org.junit.Before
19 | import org.junit.Rule
20 | import org.junit.rules.TestRule
21 | import org.robolectric.RuntimeEnvironment
22 | import org.robolectric.annotation.Config
23 | import org.robolectric.annotation.GraphicsMode
24 | import org.splitties.compose.oclock.OClockRootCanvas
25 | import java.io.File
26 |
27 | @Config(
28 | sdk = [33],
29 | qualifiers = "w227dp-h227dp-small-notlong-round-watch-xhdpi-keyshidden-nonav",
30 | )
31 | @GraphicsMode(GraphicsMode.Mode.NATIVE)
32 | abstract class ClockScreenshotTest {
33 | @get:Rule(order = 0)
34 | val debugOnly = TestRule { base, _ ->
35 | Assume.assumeTrue("Screenshot tests not supported for release", BuildConfig.DEBUG)
36 |
37 | base
38 | }
39 |
40 | @get:Rule(order = 1)
41 | val composeRule = createComposeRule()
42 |
43 | abstract val device: WearDevice
44 |
45 | // generous to allow for mac/linux differences
46 | open val tolerance = 0.02f
47 |
48 | open val roborazziOptions: RoborazziOptions
49 | get() = RoborazziOptions(
50 | compareOptions = RoborazziOptions.CompareOptions(
51 | resultValidator = ThresholdValidator(tolerance)
52 | )
53 | )
54 |
55 | @Before
56 | fun check() {
57 | // https://github.com/robolectric/robolectric/issues/8312
58 | assumeFalse("Robolectric RNG not supported on Windows", System.getProperty("os.name")?.startsWith("Windows") ?: false)
59 | }
60 |
61 | fun runTest(isAmbient: Boolean = false, clock: @Composable () -> Unit) {
62 | val filePath =
63 | File("src/test/screenshots/${this.javaClass.simpleName}_${device.id}${if (isAmbient) "_ambient" else ""}.png")
64 |
65 | RuntimeEnvironment.setQualifiers("+w${device.dp}dp-h${device.dp}dp")
66 |
67 | composeRule.setContent {
68 | OClockRootCanvas(
69 | modifier = Modifier.fillMaxSize(), isAmbientFlow = MutableStateFlow(isAmbient)
70 | ) {
71 | clock()
72 | }
73 | }
74 |
75 | composeRule.onRoot()
76 | .captureRoboImage(filePath = filePath.path, roborazziOptions = roborazziOptions)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/shared/src/test/kotlin/org/splitties/compose/oclock/sample/watchfaces/ComposeFanClockTest.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | class ComposeFanClockTest(device: WearDevice) : DeviceClockScreenshotTest(device) {
6 | @Composable
7 | override fun Clock() {
8 | ComposeFanClock()
9 | }
10 | }
--------------------------------------------------------------------------------
/shared/src/test/kotlin/org/splitties/compose/oclock/sample/watchfaces/DeviceClockScreenshotTest.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.robolectric.ParameterizedRobolectricTestRunner
7 | import org.splitties.compose.oclock.sample.watchfaces.WearDevice.Companion.GooglePixelWatch
8 | import org.splitties.compose.oclock.sample.watchfaces.WearDevice.Companion.MobvoiTicWatchPro5
9 | import org.splitties.compose.oclock.sample.watchfaces.WearDevice.Companion.SamsungGalaxyWatch5
10 |
11 | @RunWith(ParameterizedRobolectricTestRunner::class)
12 | abstract class DeviceClockScreenshotTest(override val device: WearDevice): ClockScreenshotTest() {
13 |
14 | @Test
15 | fun interactive() = runTest {
16 | Clock()
17 | }
18 |
19 | @Test
20 | fun ambient() = runTest(isAmbient = true) {
21 | Clock()
22 | }
23 |
24 | @Composable
25 | abstract fun Clock()
26 |
27 | companion object {
28 | @JvmStatic
29 | @ParameterizedRobolectricTestRunner.Parameters
30 | fun devices() = listOf(
31 | MobvoiTicWatchPro5,
32 | SamsungGalaxyWatch5,
33 | GooglePixelWatch,
34 | )
35 | }
36 | }
--------------------------------------------------------------------------------
/shared/src/test/kotlin/org/splitties/compose/oclock/sample/watchfaces/Devices.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import kotlin.math.roundToInt
4 |
5 | data class WearDevice(
6 | val id: String,
7 | val modelName: String,
8 | val px: Int,
9 | val density: Float
10 | ) {
11 | val dp: Int = (px / density).roundToInt()
12 |
13 | companion object {
14 | val MobvoiTicWatchPro5: WearDevice = WearDevice(
15 | id = "ticwatch_pro_5",
16 | modelName = "Mobvoi TicWatch Pro 5",
17 | px = 466,
18 | density = 2.0f,
19 | )
20 |
21 | val SamsungGalaxyWatch5: WearDevice = WearDevice(
22 | id = "galaxy_watch_5",
23 | modelName = "Samsung Galaxy Watch 5",
24 | px = 396,
25 | density = 2.0f,
26 | )
27 |
28 | val GooglePixelWatch: WearDevice = WearDevice(
29 | id = "pixelwatch",
30 | modelName = "Google Pixel Watch",
31 | px = 384,
32 | density = 2.0f,
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/shared/src/test/kotlin/org/splitties/compose/oclock/sample/watchfaces/KotlinFanClockTest.kt:
--------------------------------------------------------------------------------
1 | package org.splitties.compose.oclock.sample.watchfaces
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | class KotlinFanClockTest(device: WearDevice) : DeviceClockScreenshotTest(device) {
6 | @Composable
7 | override fun Clock() {
8 | KotlinFanClock()
9 | }
10 | }
--------------------------------------------------------------------------------
/shared/src/test/screenshots/BasicAnalogClockTest_galaxy_watch_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/BasicAnalogClockTest_galaxy_watch_5.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/BasicAnalogClockTest_galaxy_watch_5_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/BasicAnalogClockTest_galaxy_watch_5_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/BasicAnalogClockTest_pixelwatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/BasicAnalogClockTest_pixelwatch.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/BasicAnalogClockTest_pixelwatch_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/BasicAnalogClockTest_pixelwatch_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/BasicAnalogClockTest_ticwatch_pro_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/BasicAnalogClockTest_ticwatch_pro_5.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/BasicAnalogClockTest_ticwatch_pro_5_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/BasicAnalogClockTest_ticwatch_pro_5_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/ComposeFanClockTest_galaxy_watch_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/ComposeFanClockTest_galaxy_watch_5.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/ComposeFanClockTest_galaxy_watch_5_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/ComposeFanClockTest_galaxy_watch_5_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/ComposeFanClockTest_pixelwatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/ComposeFanClockTest_pixelwatch.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/ComposeFanClockTest_pixelwatch_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/ComposeFanClockTest_pixelwatch_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/ComposeFanClockTest_ticwatch_pro_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/ComposeFanClockTest_ticwatch_pro_5.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/ComposeFanClockTest_ticwatch_pro_5_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/ComposeFanClockTest_ticwatch_pro_5_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/KotlinFanClockTest_galaxy_watch_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/KotlinFanClockTest_galaxy_watch_5.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/KotlinFanClockTest_galaxy_watch_5_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/KotlinFanClockTest_galaxy_watch_5_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/KotlinFanClockTest_pixelwatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/KotlinFanClockTest_pixelwatch.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/KotlinFanClockTest_pixelwatch_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/KotlinFanClockTest_pixelwatch_ambient.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/KotlinFanClockTest_ticwatch_pro_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/KotlinFanClockTest_ticwatch_pro_5.png
--------------------------------------------------------------------------------
/shared/src/test/screenshots/KotlinFanClockTest_ticwatch_pro_5_ambient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Splitties/ComposeOClock/a95a16419c0df890e8941c8834d81a9d934bebae/shared/src/test/screenshots/KotlinFanClockTest_ticwatch_pro_5_ambient.png
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | 1.0
2 |
--------------------------------------------------------------------------------
/versions.properties:
--------------------------------------------------------------------------------
1 | #### Dependencies and Plugin versions with their available updates.
2 | #### Generated by `./gradlew refreshVersions` version 0.60.5
3 | ####
4 | #### Don't manually edit or split the comments that start with four hashtags (####),
5 | #### they will be overwritten by refreshVersions.
6 | ####
7 | #### suppress inspection "SpellCheckingInspection" for whole file
8 | #### suppress inspection "UnusedProperty" for whole file
9 |
10 | plugin.android=8.5.0
11 |
12 | plugin.de.fayard.refreshVersions=0.60.5
13 |
14 | plugin.org.splitties.dependencies-dsl=0.2.0
15 |
16 | version.android.tools.desugar_jdk_libs=2.0.4
17 |
18 | version.androidx.activity=1.8.2
19 | ## # available=1.9.0-alpha01
20 | ## # available=1.9.0-alpha02
21 |
22 | version.androidx.compose.foundation=1.6.0
23 | ## # available=1.7.0-alpha01
24 |
25 | version.androidx.compose.material3=1.1.2
26 | ## # available=1.2.0-alpha01
27 | ## # available=1.2.0-alpha02
28 | ## # available=1.2.0-alpha03
29 | ## # available=1.2.0-alpha04
30 | ## # available=1.2.0-alpha05
31 | ## # available=1.2.0-alpha06
32 | ## # available=1.2.0-alpha07
33 | ## # available=1.2.0-alpha08
34 | ## # available=1.2.0-alpha09
35 | ## # available=1.2.0-alpha10
36 | ## # available=1.2.0-alpha11
37 | ## # available=1.2.0-alpha12
38 | ## # available=1.2.0-beta01
39 | ## # available=1.2.0-beta02
40 | ## # available=1.2.0-rc01
41 |
42 | version.androidx.compose.runtime=1.7.0-beta03
43 |
44 | version.androidx.compose.ui=1.7.0-beta03
45 |
46 | version.androidx.core=1.12.0
47 | ## # available=1.13.0-alpha01
48 | ## # available=1.13.0-alpha02
49 | ## # available=1.13.0-alpha03
50 | ## # available=1.13.0-alpha04
51 |
52 | version.androidx.core-splashscreen=1.0.1
53 | ## # available=1.1.0-alpha01
54 | ## # available=1.1.0-alpha02
55 |
56 | version.androidx.lifecycle=2.7.0
57 | ## # available=2.8.0-alpha01
58 |
59 | version.androidx.test.espresso=3.5.1
60 | ## # available=3.6.0-alpha01
61 | ## # available=3.6.0-alpha02
62 | ## # available=3.6.0-alpha03
63 |
64 | version.androidx.test.ext.junit=1.1.5
65 | ## # available=1.2.0-alpha01
66 | ## # available=1.2.0-alpha02
67 | ## # available=1.2.0-alpha03
68 |
69 | version.androidx.test.runner=1.5.2
70 | ## # available=1.5.3-alpha01
71 | ## # available=1.6.0-alpha01
72 | ## # available=1.6.0-alpha02
73 | ## # available=1.6.0-alpha03
74 | ## # available=1.6.0-alpha04
75 | ## # available=1.6.0-alpha05
76 | ## # available=1.6.0-alpha06
77 |
78 | version.androidx.wear.compose=1.3.0
79 | ## # available=1.4.0-alpha01
80 |
81 | version.androidx.wear.watchface-editor=1.2.1
82 |
83 | version.com.google.gms..google-services=4.4.0
84 |
85 | version.firebase-crashlytics-gradle=2.9.9
86 |
87 | version.google.android.play-services-wearable=18.1.0
88 |
89 | version.junit.junit=4.13.2
90 |
91 | version.kotlin=2.0.0
92 |
93 | version.robolectric=4.11.1
94 |
95 | version.splitties=3.0.0
96 | ## # available=3.1.0-SNAPSHOT
97 |
--------------------------------------------------------------------------------