├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ └── ci-konfetti.yaml ├── .gitignore ├── .idea ├── codeStyleSettings.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── dictionaries │ └── dionsegijn.xml ├── gradle.xml ├── inspectionProfiles │ ├── Project_Default.xml │ ├── ktlint.xml │ └── profiles_settings.xml └── kotlinc.xml ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Constants.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── konfetti ├── compose │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── nl │ │ │ └── dionsegijn │ │ │ └── konfetti │ │ │ └── compose │ │ │ ├── DrawShapes.kt │ │ │ ├── KonfettiView.kt │ │ │ ├── OnParticleSystemUpdateListener.kt │ │ │ └── image │ │ │ ├── DrawableImage.kt │ │ │ ├── ImageStore.kt │ │ │ └── ImageUtil.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── core │ ├── .gitignore │ ├── build.gradle.kts │ ├── lint-baseline.xml │ ├── proguard-rules.pro │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── nl │ │ │ └── dionsegijn │ │ │ └── konfetti │ │ │ └── core │ │ │ ├── Particle.kt │ │ │ ├── Party.kt │ │ │ ├── PartyFactory.kt │ │ │ ├── PartySystem.kt │ │ │ ├── emitter │ │ │ ├── BaseEmitter.kt │ │ │ ├── Confetti.kt │ │ │ ├── EmitterConfig.kt │ │ │ └── PartyEmitter.kt │ │ │ └── models │ │ │ ├── CoreImage.kt │ │ │ ├── CoreImageStore.kt │ │ │ ├── CoreRect.kt │ │ │ ├── Shape.kt │ │ │ ├── Size.kt │ │ │ └── Vector.kt │ │ └── test │ │ ├── java │ │ └── nl │ │ │ └── dionsegijn │ │ │ └── konfetti │ │ │ └── core │ │ │ ├── PartySystemTest.kt │ │ │ └── emitter │ │ │ └── PartyEmitterTest.kt │ │ └── resources │ │ └── mockito-extensions │ │ └── org.mockito.plugins.MockMaker └── xml │ ├── .gitignore │ ├── build.gradle.kts │ ├── lint-baseline.xml │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── nl │ └── dionsegijn │ └── konfetti │ └── xml │ ├── DrawShapes.kt │ ├── KonfettiView.kt │ ├── image │ ├── DrawableImage.kt │ ├── ImageStore.kt │ └── ImageUtil.kt │ └── listeners │ └── OnParticleSystemUpdateListener.kt ├── samples ├── compose-kotlin │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── nl │ │ │ └── dionsegijn │ │ │ └── xml │ │ │ └── compose │ │ │ ├── ComposeActivity.kt │ │ │ ├── KonfettiViewModel.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_heart.xml │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── 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-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml ├── shared │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── nl │ │ └── dionsegijn │ │ └── samples │ │ └── shared │ │ └── Presets.kt ├── xml-java │ ├── .gitignore │ ├── build.gradle.kts │ ├── lint-baseline.xml │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── nl │ │ │ └── dionsegijn │ │ │ └── xml │ │ │ └── java │ │ │ └── MainActivityTest.java │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── nl │ │ │ └── dionsegijn │ │ │ └── xml │ │ │ └── java │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── ic_heart.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── backup_descriptor.xml └── xml-kotlin │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── nl │ │ └── dionsegijn │ │ └── xml │ │ └── kotlin │ │ └── MainActivityTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── nl │ │ └── dionsegijn │ │ └── xml │ │ └── kotlin │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_heart.xml │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── 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-night │ └── themes.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── scripts └── publish-module.gradle.kts └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_function_naming_ignore_when_annotated_with=Composable 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @DanielMartinus 2 | -------------------------------------------------------------------------------- /.github/workflows/ci-konfetti.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | run-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - uses: actions/setup-java@v2 12 | with: 13 | java-version: 17 14 | distribution: zulu 15 | 16 | - uses: gradle/wrapper-validation-action@v1 17 | 18 | - uses: actions/cache@v2 19 | with: 20 | path: | 21 | ~/.gradle/caches 22 | ~/.gradle/wrapper 23 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 24 | restore-keys: | 25 | ${{ runner.os }}-gradle- 26 | 27 | - name: Run build and checks 28 | run: ./gradlew buildDebug 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Android Studio 36 | .idea/libraries/ 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/.name 40 | .idea/compiler.xml 41 | .idea/copyright/profiles_settings.xml 42 | .idea/file.template.settings.xml 43 | .idea/encodings.xml 44 | .idea/misc.xml 45 | .idea/modules.xml 46 | .idea/scopes/scope_settings.xml 47 | .idea/vcs.xml 48 | 49 | # Intellij 50 | *.iml 51 | 52 | 53 | # Keystore files 54 | *.jks 55 | 56 | .tasks.cache.json 57 | /.idea/caches 58 | /.DS_Store 59 | jarRepositories.xml 60 | androidTestResultsUserPreferences.xml 61 | migrations.xml 62 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | xmlns:android 19 | 20 | ^$ 21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 | xmlns:.* 30 | 31 | ^$ 32 | 33 | 34 | BY_NAME 35 | 36 |
37 |
38 | 39 | 40 | 41 | .*:id 42 | 43 | http://schemas.android.com/apk/res/android 44 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | 52 | .*:name 53 | 54 | http://schemas.android.com/apk/res/android 55 | 56 | 57 | 58 |
59 |
60 | 61 | 62 | 63 | name 64 | 65 | ^$ 66 | 67 | 68 | 69 |
70 |
71 | 72 | 73 | 74 | style 75 | 76 | ^$ 77 | 78 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | .* 86 | 87 | ^$ 88 | 89 | 90 | BY_NAME 91 | 92 |
93 |
94 | 95 | 96 | 97 | .* 98 | 99 | http://schemas.android.com/apk/res/android 100 | 101 | 102 | ANDROID_ATTRIBUTE_ORDER 103 | 104 |
105 |
106 | 107 | 108 | 109 | .* 110 | 111 | .* 112 | 113 | 114 | BY_NAME 115 | 116 |
117 |
118 |
119 |
120 | 121 | 130 |
131 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/dictionaries/dionsegijn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | konfetti 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/ktlint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017 Dion Segijn 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 |

5 | Logo 6 |

7 |

8 | Easily celebrate little and big moments in your app with this lightweight confetti library! 9 |
10 |

11 | 12 |

13 | License 14 | API level 16 15 | API level 16 16 | Build Status 17 |

18 | 19 |

20 | Getting started • 21 | How To Use • 22 | Community • 23 | Contribute • 24 | Report issue • 25 | License 26 |

27 | 28 |

29 | New version: 2.0.0 is now released! Jetpack compose support - improved animations and API - see migration guide here 30 |

31 | 32 | ## Getting started 33 | 34 | Compose project: 35 | ```groovy 36 | dependencies { 37 | implementation 'nl.dionsegijn:konfetti-compose:2.0.5' 38 | } 39 | ``` 40 | 41 | View based (XML) project: 42 | ```groovy 43 | dependencies { 44 | implementation 'nl.dionsegijn:konfetti-xml:2.0.5' 45 | } 46 | ``` 47 | 48 | Find latest version and release notes [here](https://github.com/DanielMartinus/Konfetti/releases) 49 | 50 | ## Usage 51 | 52 |

53 | Samples:
54 | compose • 55 | xml-kotlin • 56 | xml-java • 57 | presets 58 |

59 | 60 |

61 | 62 |

63 | 64 | Configure your confetti using the Party configuration object. This holds all the information on how the confetti will be generated. 65 | Almost all properties of a Party object have a default configuration! This makes it super easy to create beautiful and natural looking confetti. 66 | 67 | 68 | The bare minimum you need is an **Emitter** to tell how often and how many confetti should spawn, like this: 69 | ```kotlin 70 | Party( 71 | emitter = Emitter(duration = 5, TimeUnit.SECONDS).perSecond(30) 72 | ) 73 | ``` 74 | 75 | **But the possibilities are endless!** You can fully control how the confetti will be generated and behave by customizing the values of the Party object. 76 | An example of a customized Party configuration is this: 77 | 78 | ```kotlin 79 | Party( 80 | speed = 0f, 81 | maxSpeed = 30f, 82 | damping = 0.9f, 83 | spread = 360, 84 | colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def), 85 | position = Position.Relative(0.5, 0.3), 86 | emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(100) 87 | ) 88 | ``` 89 | _To learn more, see more samples linked at the top of [this section](#usage)_ 90 | 91 | ### Party options 92 | 93 | - `Angle` - **Int (default: 0)**: The direction the confetti will shoot. Use any integer between `0-360` or use presets like: Angle.TOP, Angle.RIGHT, Angle.BOTTOM, Angle.LEFT. Angle.RIGHT equates to `0` degrees, and larger values move clockwise from this position. 94 | - `spread` - **Int (default: 360)**: How wide the confetti will shoot in the direction of Angle. Use any integer between `0-360`. Use 1 to shoot in a straight line and 360 to form a circle 95 | - `speed` - **Float (default: 30f)**: The start speed of the confetti at the time of creation. 96 | - `maxSpeed` - **Float (default: 0f)**: Set to -1 to disable maxSpeed. A random speed between `speed` and `maxSpeed` will be chosen. Using randomness creates a more natural and realistic look to the confetti when animating.) 97 | - `damping` - **Float (default: 0.9f)**: The rate at which the speed will decrease right after shooting the confetti 98 | - `size` - **`List` (default: SMALL, MEDIUM, LARGE)**: The size of the confetti. Use: Size.SMALL, MEDIUM or LARGE for default sizes or 99 | create your custom size using a new instance of `Size`. 100 | - `colors` - **`List` (default: 0xfce18a, 0xff726d, 0xf4306d, 0xb48def)**: List of colors that will be randomly picked from 101 | - `shapes` - **`List` (default: Shape.Square, Shape.Circle)**: Or use a custom shape with `Shape.DrawableShape` 102 | - `timeToLive` - **Long (default: 2000)**: The time in milliseconds a particle will stay alive after that the confetti will disappear. 103 | - `fadeOutEnabled` - **Boolean (default: true)**: If true and a confetti disappears because it ran out of time (set with timeToLive) it will slowly fade out. If set to falls it will instantly disappear from the screen. 104 | - `position` - **Position (default: Position.Relative(0.5, 0.5))**: the location where the confetti will spawn from relative to the canvas. Use absolute 105 | coordinates with `[Position.Absolute]` or relative coordinates between 0.0 and 1.0 using `[Position.Relative]`. Spawn confetti between random locations using `[Position.between]`. 106 | - `delay` - **Int (default: 0)**: the amount of milliseconds to wait before the rendering of the confetti starts 107 | - `rotation` - **Rotation (default: Rotation)**: enable the 3D rotation of a Confetti. See [Rotation] class for the configuration 108 | options. Easily enable or disable it using [Rotation].enabled() or [Rotation].disabled() and control the speed of rotations. 109 | - `emitter` - **EmitterConfig**: Instructions how many and how often a confetti particle should spawn. Use Emitter(duration, timeUnit).max(amount) or Emitter(duration, timeUnit).perSecond(amount) to configure the Emitter. 110 | 111 | See Party implementation [here](/konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/Party.kt) 112 | 113 | ### KonfettiView 114 | 115 | Create a `KonfettiView` in your compose UI or add one to your xml layout depending on your setup. 116 | 117 | Compose 118 | ```Kotlin 119 | KonfettiView( 120 | modifier = Modifier.fillMaxSize(), 121 | parties = state.party, 122 | ) 123 | ``` 124 | 125 | View-based (xml) 126 | ```xml 127 | 131 | ``` 132 | 133 | ```kotlin 134 | Party( 135 | speed = 0f, 136 | maxSpeed = 30f, 137 | damping = 0.9f, 138 | spread = 360, 139 | colors = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def), 140 | emitter = Emitter(duration = 100, TimeUnit.MILLISECONDS).max(100), 141 | position = Position.Relative(0.5, 0.3) 142 | ) 143 | viewKonfetti.start(party) 144 | ``` 145 | 146 | And that's it! There are endless possibilities to configure the confetti. If you want to learn more on how to implement Konfetti in a java, xml or compose project then see the samples linked at the top of [this section](#usage) 147 | 148 | ## Community 149 | 150 | - Follow me on twitter for updates [here](https://twitter.com/dionsegijn) 151 | - Do you have any questions or need help implementing this library? Search if your question is already asked [here](https://github.com/DanielMartinus/Konfetti/issues?q=is%3Aissue) 152 | - Or join our telegram chat group and maybe someone can help you out [here](https://t.me/konfetti_chat) 153 | 154 | ## Contribute 155 | 156 | Do you see any improvements or want to implement a missing feature? Contributions are very welcome! 157 | - Is your contribution relatively small? You can, make your changes, run the code checks, open a PR and make sure the CI is green. That's it! 158 | - Are the changes big and do they make a lot of impact? Please open an issue [here](https://github.com/DanielMartinus/Konfetti/issues?q=is%3Aissue) or reach out and let's discuss. 159 | 160 | Take into account that changes and requests can be rejected if they don't align with the **purpose of the library**. To not waste any time you can always open an issue [here](https://github.com/DanielMartinus/Konfetti/issues?q=is%3Aissue) to talk before you start making any changes. 161 | 162 | ### What is the purpose of this library? 163 | To have a lightweight particle system to easily generate confetti particles to celebrate little and big moments. Even though this is a particle system the purpose is not to be a fully fledged particle system. Changes and features are meant to be in line with being a confetti library. A great example is the implementation of custom shapes by @mattprecious [here](https://github.com/DanielMartinus/Konfetti/pull/129). 164 | 165 | ## Report an issue 166 | 167 | - Did you find an issue and want to fix it yourself? See [Contribute](#contribute) for more information 168 | - Want to report an issue? You can do that [here](https://github.com/DanielMartinus/Konfetti/issues?q=is%3Aissue). By adding as much details when reporting the issue and steps to reproduce you improve the change it will be solved quickly. 169 | 170 | ## License 171 | 172 | Konfetti is released under the ISC license. See [LICENSE](https://github.com/DanielMartinus/Konfetti/blob/main/LICENSE) for details. 173 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("io.github.gradle-nexus.publish-plugin") version "1.3.0" 3 | id("org.jetbrains.dokka") version "1.7.0" 4 | } 5 | 6 | buildscript { 7 | repositories { 8 | gradlePluginPortal() 9 | google() 10 | mavenCentral() 11 | } 12 | dependencies { 13 | classpath("com.android.tools.build:gradle:8.0.2") 14 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Constants.kotlinVersion}") 15 | classpath("com.github.dcendents:android-maven-gradle-plugin:2.1") 16 | classpath("com.diffplug.spotless:spotless-plugin-gradle:6.23.3") 17 | classpath("io.github.gradle-nexus:publish-plugin:1.3.0") 18 | classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.7.0") 19 | } 20 | } 21 | 22 | // Connect with the repository using properties from local.properties in the root of the project 23 | val properties = File(rootDir, "local.properties") 24 | if(properties.exists()) { 25 | val localProperties = properties.inputStream().use { java.util.Properties().apply { load(it) } } 26 | // Set up Sonatype repository 27 | nexusPublishing { 28 | repositories { 29 | sonatype { 30 | stagingProfileId.set(localProperties["sonatypeStagingProfileId"] as String?) 31 | username.set(localProperties["ossrhUsername"] as String?) 32 | password.set(localProperties["ossrhPassword"] as String?) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation("com.android.tools.build:gradle:8.0.2") 12 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") 13 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Constants.kt: -------------------------------------------------------------------------------- 1 | object buildVersions { 2 | const val compileSdk = 34 3 | const val targetSdk = 34 4 | } 5 | 6 | object Constants { 7 | const val composeVersion = "1.4.3" 8 | const val konfettiVersion = "2.0.5" 9 | const val kotlinVersion = "1.8.10" 10 | } 11 | 12 | object NexusConfig { 13 | const val PUBLISH_GROUP_ID = "nl.dionsegijn" 14 | const val PUBLISH_VERSION = Constants.konfettiVersion 15 | var PUBLISH_ARTIFACT_ID = "" 16 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.defaults.buildfeatures.buildconfig=true 13 | android.enableJetifier=true 14 | android.nonFinalResIds=false 15 | android.nonTransitiveRClass=false 16 | android.useAndroidX=true 17 | org.gradle.jvmargs=-Xmx1536m 18 | 19 | # When configured, Gradle will run in incubating parallel mode. 20 | # This option should only be used with decoupled projects. More details, visit 21 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 22 | # org.gradle.parallel=true 23 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | compose = "1.4.3" 3 | core-ktx = "1.7.0" 4 | appcompat = "1.4.0" 5 | constraintlayout = "2.1.3" 6 | activity-compose = "1.4.0" 7 | androidx-lifecycle = "1.4.0" 8 | espresso = "3.5.1" 9 | tracing = "1.1.0" 10 | material = "1.4.0" 11 | mockito = "3.11.2" 12 | junit = "4.13.2" 13 | junit-ext = "1.1.5" 14 | 15 | [libraries] 16 | compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "compose" } 17 | compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" } 18 | compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" } 19 | compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose" } 20 | compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" } 21 | compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "compose" } 22 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } 23 | androidx-appcomat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 24 | androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } 25 | androidx-tracing = { group = "androidx.tracing", name = "tracing", version.ref = "tracing" } 26 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "appcompat" } 27 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } 28 | androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "androidx-lifecycle" } 29 | androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } 30 | android-material = { group = "com.google.android.material", name = "material", version.ref = "material" } 31 | test-mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } 32 | test-junit = { group = "junit", name = "junit", version.ref = "junit" } 33 | test-junit-ext = { group = "androidx.test.ext", name = "junit", version.ref = "junit-ext" } 34 | test-compose-ui = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" } 35 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/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.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /konfetti/compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /konfetti/compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("com.diffplug.spotless") 5 | } 6 | 7 | NexusConfig.PUBLISH_ARTIFACT_ID = "konfetti-compose" 8 | apply(from = "../../scripts/publish-module.gradle.kts") 9 | 10 | spotless { 11 | kotlin { 12 | ktlint("1.1.0") 13 | target("src/**/*.kt") 14 | } 15 | java { 16 | removeUnusedImports() 17 | googleJavaFormat("1.15.0") 18 | target("**/*.java") 19 | } 20 | } 21 | 22 | android { 23 | compileSdk = buildVersions.compileSdk 24 | 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | 34 | defaultConfig { 35 | minSdk = 21 36 | 37 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 38 | } 39 | 40 | buildTypes { 41 | release { 42 | isMinifyEnabled = false 43 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 44 | } 45 | } 46 | 47 | buildFeatures { 48 | compose = true 49 | } 50 | 51 | composeOptions { 52 | kotlinCompilerExtensionVersion = Constants.composeVersion 53 | } 54 | namespace = "nl.dionsegijn.konfetti.compose" 55 | } 56 | 57 | dependencies { 58 | val composeVersion: String = Constants.composeVersion 59 | 60 | debugApi(project(path = ":konfetti:core")) 61 | releaseApi("nl.dionsegijn:konfetti-core:${Constants.konfettiVersion}") 62 | 63 | implementation(libs.compose.foundation) 64 | implementation(libs.compose.ui) 65 | 66 | testImplementation(libs.test.junit) 67 | androidTestImplementation(libs.test.junit.ext) 68 | androidTestImplementation(libs.compose.ui) 69 | } 70 | -------------------------------------------------------------------------------- /konfetti/compose/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.kts. 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 -------------------------------------------------------------------------------- /konfetti/compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/java/nl/dionsegijn/konfetti/compose/DrawShapes.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.compose 2 | 3 | import android.graphics.BlendMode 4 | import android.graphics.BlendModeColorFilter 5 | import android.graphics.PorterDuff 6 | import android.os.Build 7 | import androidx.compose.ui.geometry.Offset 8 | import androidx.compose.ui.geometry.Size 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.ImageBitmap 11 | import androidx.compose.ui.graphics.drawscope.DrawScope 12 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 13 | import androidx.compose.ui.graphics.nativeCanvas 14 | import nl.dionsegijn.konfetti.core.Particle 15 | import nl.dionsegijn.konfetti.core.models.ReferenceImage 16 | import nl.dionsegijn.konfetti.core.models.Shape 17 | import nl.dionsegijn.konfetti.core.models.Shape.Circle 18 | import nl.dionsegijn.konfetti.core.models.Shape.DrawableShape 19 | import nl.dionsegijn.konfetti.core.models.Shape.Rectangle 20 | import nl.dionsegijn.konfetti.core.models.Shape.Square 21 | import nl.dionsegijn.konfetti.xml.image.ImageStore 22 | 23 | /** 24 | * Draw a shape to `compose canvas`. Implementations are expected to draw within a square of size 25 | * `size` and must vertically/horizontally center their asset if it does not have an equal width 26 | * and height. 27 | */ 28 | fun Shape.draw( 29 | drawScope: DrawScope, 30 | particle: Particle, 31 | imageResource: ImageBitmap? = null, 32 | imageStore: ImageStore, 33 | ) { 34 | when (this) { 35 | Circle -> { 36 | val offsetMiddle = particle.width / 2 37 | drawScope.drawCircle( 38 | color = Color(particle.color), 39 | center = Offset(particle.x + offsetMiddle, particle.y + offsetMiddle), 40 | radius = particle.width / 2, 41 | ) 42 | } 43 | Square -> { 44 | drawScope.drawRect( 45 | color = Color(particle.color), 46 | topLeft = Offset(particle.x, particle.y), 47 | size = Size(particle.width, particle.height), 48 | ) 49 | } 50 | is Rectangle -> { 51 | val size = particle.width 52 | val height = size * heightRatio 53 | drawScope.drawRect( 54 | color = Color(particle.color), 55 | topLeft = Offset(particle.x, particle.y), 56 | size = Size(size, height), 57 | ) 58 | } 59 | is DrawableShape -> { 60 | val referenceImage = image 61 | if (referenceImage is ReferenceImage) { 62 | val drawable = imageStore.getImage(referenceImage.reference) ?: return 63 | 64 | drawScope.drawIntoCanvas { 65 | // Making use of the ImageStore for performance reasons, see ImageStore for more info 66 | if (tint) { 67 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 68 | drawable.colorFilter = BlendModeColorFilter(particle.color, BlendMode.SRC_IN) 69 | } else { 70 | drawable.setColorFilter(particle.color, PorterDuff.Mode.SRC_IN) 71 | } 72 | } else if (applyAlpha) { 73 | drawable.alpha = particle.alpha 74 | } 75 | 76 | val size = particle.width 77 | val height = (size * heightRatio).toInt() 78 | val top = ((size - height) / 2f).toInt() 79 | 80 | val x = particle.y.toInt() 81 | val y = particle.x.toInt() 82 | drawable.setBounds(y, top + x, size.toInt() + y, top + height + x) 83 | drawable.draw(it.nativeCanvas) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/java/nl/dionsegijn/konfetti/compose/KonfettiView.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.compose 2 | 3 | import android.content.res.Resources 4 | import android.graphics.drawable.Drawable 5 | import androidx.compose.animation.core.withInfiniteAnimationFrameMillis 6 | import androidx.compose.foundation.Canvas 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.graphics.drawscope.withTransform 14 | import androidx.compose.ui.layout.onGloballyPositioned 15 | import nl.dionsegijn.konfetti.core.Particle 16 | import nl.dionsegijn.konfetti.core.Party 17 | import nl.dionsegijn.konfetti.core.PartySystem 18 | import nl.dionsegijn.konfetti.core.models.CoreRectImpl 19 | import nl.dionsegijn.konfetti.core.models.ReferenceImage 20 | import nl.dionsegijn.konfetti.core.models.Shape 21 | import nl.dionsegijn.konfetti.xml.image.DrawableImage 22 | import nl.dionsegijn.konfetti.xml.image.ImageStore 23 | 24 | @Composable 25 | fun KonfettiView( 26 | modifier: Modifier = Modifier, 27 | parties: List, 28 | updateListener: OnParticleSystemUpdateListener? = null, 29 | ) { 30 | lateinit var partySystems: List 31 | 32 | /** 33 | * Particles to draw 34 | */ 35 | val particles = remember { mutableStateOf(emptyList()) } 36 | 37 | /** 38 | * Latest stored frameTimeMilliseconds 39 | */ 40 | val frameTime = remember { mutableStateOf(0L) } 41 | 42 | /** 43 | * Area in which the particles are being drawn 44 | */ 45 | val drawArea = remember { mutableStateOf(CoreRectImpl()) } 46 | 47 | /** 48 | * Store for drawable images 49 | */ 50 | val imageStore = remember { ImageStore() } 51 | 52 | LaunchedEffect(Unit) { 53 | partySystems = 54 | parties.map { 55 | PartySystem( 56 | party = storeImages(it, imageStore), 57 | pixelDensity = Resources.getSystem().displayMetrics.density, 58 | ) 59 | } 60 | while (true) { 61 | withInfiniteAnimationFrameMillis { frameMs -> 62 | // Calculate time between frames, fallback to 0 when previous frame doesn't exist 63 | val deltaMs = if (frameTime.value > 0) (frameMs - frameTime.value) else 0 64 | frameTime.value = frameMs 65 | 66 | particles.value = 67 | partySystems.map { particleSystem -> 68 | 69 | val totalTimeRunning = getTotalTimeRunning(particleSystem.createdAt) 70 | // Do not start particleSystem yet if totalTimeRunning is below delay 71 | if (totalTimeRunning < particleSystem.party.delay) return@map listOf() 72 | 73 | if (particleSystem.isDoneEmitting()) { 74 | updateListener?.onParticleSystemEnded( 75 | system = particleSystem, 76 | activeSystems = partySystems.count { !it.isDoneEmitting() }, 77 | ) 78 | } 79 | 80 | particleSystem.render(deltaMs.div(1000f), drawArea.value) 81 | }.flatten() 82 | } 83 | } 84 | } 85 | 86 | Canvas( 87 | modifier = 88 | modifier 89 | .onGloballyPositioned { 90 | drawArea.value = 91 | CoreRectImpl(0f, 0f, it.size.width.toFloat(), it.size.height.toFloat()) 92 | }, 93 | onDraw = { 94 | particles.value.forEach { particle -> 95 | withTransform({ 96 | rotate( 97 | degrees = particle.rotation, 98 | pivot = 99 | Offset( 100 | x = particle.x + (particle.width / 2), 101 | y = particle.y + (particle.height / 2), 102 | ), 103 | ) 104 | scale( 105 | scaleX = particle.scaleX, 106 | scaleY = 1f, 107 | pivot = Offset(particle.x + (particle.width / 2), particle.y), 108 | ) 109 | }) { 110 | particle.shape.draw(drawScope = this, particle = particle, imageStore = imageStore) 111 | } 112 | } 113 | }, 114 | ) 115 | } 116 | 117 | /** 118 | * Transforms the shapes in the given [Party] object. If a shape is a [Shape.DrawableShape], 119 | * it replaces the [DrawableImage] with a [ReferenceImage] and stores the [Drawable] in the [ImageStore]. 120 | * 121 | * @param party The Party object containing the shapes to be transformed. 122 | * @return A new Party object with the transformed shapes. 123 | */ 124 | fun storeImages( 125 | party: Party, 126 | imageStore: ImageStore, 127 | ): Party { 128 | val transformedShapes = 129 | party.shapes.map { shape -> 130 | when (shape) { 131 | is Shape.DrawableShape -> { 132 | val referenceImage = drawableToReferenceImage(shape.image as DrawableImage, imageStore) 133 | shape.copy(image = referenceImage) 134 | } 135 | else -> shape 136 | } 137 | } 138 | return party.copy(shapes = transformedShapes) 139 | } 140 | 141 | /** 142 | * Converts a [DrawableImage] to a [ReferenceImage] and stores the [Drawable] in the [ImageStore]. 143 | * 144 | * @param drawableImage The DrawableImage to be converted. 145 | * @return A ReferenceImage with the same dimensions as the DrawableImage and a reference to the stored Drawable. 146 | */ 147 | fun drawableToReferenceImage( 148 | drawableImage: DrawableImage, 149 | imageStore: ImageStore, 150 | ): ReferenceImage { 151 | val id = imageStore.storeImage(drawableImage.drawable) 152 | return ReferenceImage(id, drawableImage.width, drawableImage.height) 153 | } 154 | 155 | fun getTotalTimeRunning(startTime: Long): Long { 156 | val currentTime = System.currentTimeMillis() 157 | return (currentTime - startTime) 158 | } 159 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/java/nl/dionsegijn/konfetti/compose/OnParticleSystemUpdateListener.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.compose 2 | 3 | import nl.dionsegijn.konfetti.core.PartySystem 4 | 5 | interface OnParticleSystemUpdateListener { 6 | fun onParticleSystemEnded( 7 | system: PartySystem, 8 | activeSystems: Int, 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/java/nl/dionsegijn/konfetti/compose/image/DrawableImage.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.image 2 | 3 | import android.graphics.drawable.Drawable 4 | import nl.dionsegijn.konfetti.core.models.CoreImage 5 | 6 | data class DrawableImage( 7 | val drawable: Drawable, 8 | override val width: Int, 9 | override val height: Int, 10 | ) : CoreImage 11 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/java/nl/dionsegijn/konfetti/compose/image/ImageStore.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.image 2 | 3 | import android.graphics.drawable.Drawable 4 | import nl.dionsegijn.konfetti.core.models.CoreImageStore 5 | 6 | /** 7 | * The ImageStore class is used to store Drawable objects and provide a way to reference them. 8 | * This is done for performance reasons and to allow the core library, which can't use Android Drawables, 9 | * to work with images. 10 | * 11 | * Instead of converting a Drawable to a ByteBuffer, then to a Bitmap, and then back to a Drawable, 12 | * which is inefficient for the render code, the Drawable is stored in the ImageStore. 13 | * The rest of the application can then work with a simple integer reference to the Drawable. 14 | * 15 | * The ImageStore provides methods to store a Drawable and retrieve it using its reference. 16 | */ 17 | class ImageStore : CoreImageStore { 18 | private val images = mutableMapOf() 19 | 20 | override fun storeImage(image: Drawable): Int { 21 | val id = image.hashCode() 22 | images[id] = image 23 | return id 24 | } 25 | 26 | override fun getImage(id: Int): Drawable? { 27 | return images[id] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/java/nl/dionsegijn/konfetti/compose/image/ImageUtil.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.image 2 | 3 | import android.graphics.drawable.Drawable 4 | import nl.dionsegijn.konfetti.core.models.Shape 5 | 6 | object ImageUtil { 7 | @JvmStatic 8 | fun loadDrawable( 9 | drawable: Drawable, 10 | tint: Boolean = true, 11 | applyAlpha: Boolean = true, 12 | ): Shape.DrawableShape { 13 | val width = drawable.intrinsicWidth 14 | val height = drawable.intrinsicHeight 15 | val drawableImage = DrawableImage(drawable, width, height) 16 | return Shape.DrawableShape(drawableImage, tint, applyAlpha) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /konfetti/compose/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /konfetti/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /konfetti/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("kotlin") 4 | id("com.diffplug.spotless") 5 | } 6 | 7 | NexusConfig.PUBLISH_ARTIFACT_ID = "konfetti-core" 8 | apply(from = "../../scripts/publish-module.gradle.kts") 9 | 10 | spotless { 11 | kotlin { 12 | ktlint("1.1.0") 13 | target("src/**/*.kt") 14 | } 15 | java { 16 | removeUnusedImports() 17 | googleJavaFormat("1.15.0") 18 | target("**/*.java") 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Constants.kotlinVersion}") 24 | testImplementation(libs.test.junit) 25 | testImplementation(libs.test.mockito) 26 | } 27 | -------------------------------------------------------------------------------- /konfetti/core/lint-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /konfetti/core/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.kts. 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 -------------------------------------------------------------------------------- /konfetti/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/Particle.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core 2 | 3 | import nl.dionsegijn.konfetti.core.models.Shape 4 | 5 | /** 6 | * Particle holding exact data to instruct where and how to draw a particle 7 | * @param x the absolute x position on the canvas 8 | * @param y the absolute y position on the canvas 9 | * @param width the current width of the confetti 10 | * @param height the current height of a confetti 11 | * @param color the color that will be used to paint the confetti 12 | * @param rotation the current rotation of the confetti in degrees 13 | * @param scaleX the current scale of the confetti used to create a 3D rotation 14 | * @param shape the Shape of the confetti such as a circle, square of custom shape 15 | * @param alpha the transparency of the confetti between 0 - 255 16 | */ 17 | data class Particle( 18 | val x: Float, 19 | val y: Float, 20 | val width: Float, 21 | val height: Float, 22 | val color: Int, 23 | val rotation: Float, 24 | val scaleX: Float, 25 | val shape: Shape, 26 | val alpha: Int, 27 | ) 28 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/Party.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core 2 | 3 | import nl.dionsegijn.konfetti.core.Angle.Companion.BOTTOM 4 | import nl.dionsegijn.konfetti.core.Angle.Companion.LEFT 5 | import nl.dionsegijn.konfetti.core.Angle.Companion.RIGHT 6 | import nl.dionsegijn.konfetti.core.Angle.Companion.TOP 7 | import nl.dionsegijn.konfetti.core.emitter.EmitterConfig 8 | import nl.dionsegijn.konfetti.core.models.Shape 9 | import nl.dionsegijn.konfetti.core.models.Size 10 | 11 | /** 12 | * Configuration how to confetti should be rendered 13 | * @property angle the direction the confetti will shoot 14 | * @property spread how wide the confetti will shoot in degrees. Use 1 to shoot in a straight line 15 | * and 360 to form a circle 16 | * @property speed The start speed of the confetti at the time of creation. Also set [maxSpeed] to 17 | * apply a random speed between speed and maxSpeed. 18 | * @property maxSpeed when [maxSpeed] is set a random speed between [speed] and [maxSpeed] will be 19 | * chosen. Using randomness creates a more natural and realistic look to the confetti when animating. 20 | * @property damping The rate at which the speed will decrease right after shooting the confetti 21 | * @property size The size of the confetti. Use: Size.SMALL, MEDIUM or LARGE for default sizes or 22 | * create your custom size using a new instance of [Size]. 23 | * @property colors List of colors that will be randomly picked from 24 | * @property shapes Set the shape of the confetti. Set multiple shapes and it will be randomly 25 | * assigned upon creation of the confetti. See [Shape] for possible shapes and custom drawables. 26 | * @property timeToLive the amount of time in milliseconds before a particle will stop rendering 27 | * or fade out if [fadeOutEnabled] is set to true. 28 | * @property fadeOutEnabled If true and a confetti disappears because it ran out of time (set with timeToLive) 29 | * it will slowly fade out. If set to falls it will instantly disappear from the screen. 30 | * @property position the point where the confetti will spawn relative to the canvas. Use absolute 31 | * coordinates with [Position.Absolute] or relative coordinates between 0.0 and 1.0 using [Position.Relative]. 32 | * Spawn confetti on random positions using [Position.Between]. 33 | * @property delay the amount of milliseconds to wait before the rendering of the confetti starts 34 | * @property rotation enable the 3D rotation of a Confetti. See [Rotation] class for the configuration 35 | * options. Easily enable or disable it using [Rotation].enabled() or [Rotation].disabled() and 36 | * control the speed of rotations. 37 | * @property emitter instructions how many and often a confetti particle should spawn. 38 | * Use Emitter(duration, timeUnit).max(amount) or Emitter(duration, timeUnit).perSecond(amount) to 39 | * configure the Emitter. 40 | */ 41 | data class Party( 42 | val angle: Int = 0, 43 | val spread: Int = 360, 44 | val speed: Float = 30f, 45 | val maxSpeed: Float = 0f, 46 | val damping: Float = 0.9f, 47 | val size: List = listOf(Size.SMALL, Size.MEDIUM, Size.LARGE), 48 | val colors: List = listOf(0xfce18a, 0xff726d, 0xf4306d, 0xb48def), 49 | val shapes: List = listOf(Shape.Square, Shape.Circle), 50 | val timeToLive: Long = 2000, 51 | val fadeOutEnabled: Boolean = true, 52 | val position: Position = Position.Relative(0.5, 0.5), 53 | val delay: Int = 0, 54 | val rotation: Rotation = Rotation(), 55 | val emitter: EmitterConfig, 56 | ) 57 | 58 | /** 59 | * Helper class for easily setting an angle based on easy understandable constants 60 | * [TOP] 270 degrees 61 | * [RIGHT] 0 degrees 62 | * [BOTTOM] 90 degrees 63 | * [LEFT] 180 degrees 64 | */ 65 | class Angle { 66 | companion object { 67 | const val TOP: Int = 270 68 | const val RIGHT: Int = 0 69 | const val BOTTOM: Int = 90 70 | const val LEFT: Int = 180 71 | } 72 | } 73 | 74 | /** 75 | * Helper class for for easily configuring [Spread] when creating a [Party] 76 | * These are presets but any custom amount will work within 0-360 degrees 77 | */ 78 | class Spread { 79 | companion object { 80 | const val SMALL: Int = 30 81 | const val WIDE: Int = 100 82 | const val ROUND: Int = 360 83 | } 84 | } 85 | 86 | sealed class Position { 87 | /** 88 | * Set absolute position on the x and y axis of the KonfettiView 89 | * @property x the x coordinate in pixels 90 | * @property y the y coordinate in pixels 91 | */ 92 | data class Absolute(val x: Float, val y: Float) : Position() { 93 | fun between(value: Absolute): Position = Between(this, value) 94 | } 95 | 96 | /** 97 | * Set relative position on the x and y axis of the KonfettiView. Some examples: 98 | * [x: 0.0, y: 0.0] Top left corner 99 | * [x: 1.0, y: 0.0] Top right corner 100 | * [x: 0.0, y: 1.0] Bottom left corner 101 | * [x: 1.0, y: 1.0] Bottom right corner 102 | * [x: 0.5, y: 0.5] Center of the view 103 | * 104 | * @property x the relative x coordinate as a double 105 | * @property y the relative y coordinate as a double 106 | */ 107 | data class Relative(val x: Double, val y: Double) : Position() { 108 | fun between(value: Relative): Position = Between(this, value) 109 | } 110 | 111 | /** 112 | * Use this if you want to spawn confetti between multiple locations. Use this with [Absolute] 113 | * and [Relative] to connect two points 114 | * Example: Relative(0.0, 0.0).between(Relative(1.0, 0.0)) 115 | * This will spawn confetti from the full width and top of the view 116 | */ 117 | internal data class Between(val min: Position, val max: Position) : Position() 118 | } 119 | 120 | /** 121 | * @property enabled by default true. Set to false to prevent the confetti from rotating 122 | * @property speed the rate at which the confetti will rotate per frame. Control the 2D and 3D rotation 123 | * separately using [multiplier2D] and [multiplier3D] 124 | * @property variance the margin in which the rotationSpeed can differ to add randomness 125 | * to the rotation speed of each confetti. 126 | * @property multiplier2D Multiplier controlling the speed of the rotation around the center of the 127 | * confetti. Set this value to 0 to disable the 2D rotation effect. 128 | * @property multiplier3D Multiplier controlling the 3D rotation of the confetti. 129 | */ 130 | data class Rotation( 131 | val enabled: Boolean = true, 132 | val speed: Float = 1f, 133 | val variance: Float = 0.5f, 134 | val multiplier2D: Float = 8f, 135 | val multiplier3D: Float = 1.5f, 136 | ) { 137 | companion object { 138 | fun enabled() = Rotation(enabled = true) 139 | 140 | fun disabled() = Rotation(enabled = false) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/PartyFactory.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core 2 | 3 | import nl.dionsegijn.konfetti.core.emitter.EmitterConfig 4 | import nl.dionsegijn.konfetti.core.models.Shape 5 | import nl.dionsegijn.konfetti.core.models.Size 6 | 7 | /** 8 | * Factory class to enable builder methods for Java implementations 9 | * See [Party] for documentation on the configuration settings 10 | */ 11 | class PartyFactory(val emitter: EmitterConfig) { 12 | private var party: Party = Party(emitter = emitter) 13 | 14 | fun angle(angle: Int): PartyFactory { 15 | party = party.copy(angle = angle) 16 | return this 17 | } 18 | 19 | fun spread(spread: Int): PartyFactory { 20 | party = party.copy(spread = spread) 21 | return this 22 | } 23 | 24 | fun setSpeed(speed: Float): PartyFactory { 25 | party = party.copy(speed = speed) 26 | return this 27 | } 28 | 29 | fun setSpeedBetween( 30 | minSpeed: Float, 31 | maxSpeed: Float, 32 | ): PartyFactory { 33 | party = party.copy(speed = minSpeed, maxSpeed = maxSpeed) 34 | return this 35 | } 36 | 37 | fun setDamping(damping: Float): PartyFactory { 38 | party = party.copy(damping = damping) 39 | return this 40 | } 41 | 42 | fun position(position: Position): PartyFactory { 43 | party = party.copy(position = position) 44 | return this 45 | } 46 | 47 | fun position( 48 | x: Float, 49 | y: Float, 50 | ): PartyFactory { 51 | party = party.copy(position = Position.Absolute(x, y)) 52 | return this 53 | } 54 | 55 | fun position( 56 | minX: Float, 57 | minY: Float, 58 | maxX: Float, 59 | maxY: Float, 60 | ): PartyFactory { 61 | party = 62 | party.copy( 63 | position = 64 | Position.Absolute(minX, minY) 65 | .between(Position.Absolute(maxX, maxY)), 66 | ) 67 | return this 68 | } 69 | 70 | fun position( 71 | x: Double, 72 | y: Double, 73 | ): PartyFactory { 74 | party = party.copy(position = Position.Relative(x, y)) 75 | return this 76 | } 77 | 78 | fun position( 79 | minX: Double, 80 | minY: Double, 81 | maxX: Double, 82 | maxY: Double, 83 | ): PartyFactory { 84 | party = 85 | party.copy( 86 | position = Position.Relative(minX, minY).between(Position.Relative(maxX, maxY)), 87 | ) 88 | return this 89 | } 90 | 91 | fun sizes(vararg sizes: Size): PartyFactory { 92 | party = party.copy(size = sizes.toList()) 93 | return this 94 | } 95 | 96 | fun sizes(size: List): PartyFactory { 97 | party = party.copy(size = size) 98 | return this 99 | } 100 | 101 | fun colors(colors: List): PartyFactory { 102 | party = party.copy(colors = colors) 103 | return this 104 | } 105 | 106 | fun shapes(shapes: List): PartyFactory { 107 | party = party.copy(shapes = shapes) 108 | return this 109 | } 110 | 111 | fun shapes(vararg shapes: Shape): PartyFactory { 112 | party = party.copy(shapes = shapes.toList()) 113 | return this 114 | } 115 | 116 | fun timeToLive(timeToLive: Long): PartyFactory { 117 | party = party.copy(timeToLive = timeToLive) 118 | return this 119 | } 120 | 121 | fun fadeOutEnabled(fadeOutEnabled: Boolean): PartyFactory { 122 | party = party.copy(fadeOutEnabled = fadeOutEnabled) 123 | return this 124 | } 125 | 126 | fun delay(delay: Int): PartyFactory { 127 | party = party.copy(delay = delay) 128 | return this 129 | } 130 | 131 | fun rotation(rotation: Rotation): PartyFactory { 132 | party = party.copy(rotation = rotation) 133 | return this 134 | } 135 | 136 | fun build(): Party = party 137 | } 138 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/PartySystem.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core 2 | 3 | import nl.dionsegijn.konfetti.core.emitter.BaseEmitter 4 | import nl.dionsegijn.konfetti.core.emitter.Confetti 5 | import nl.dionsegijn.konfetti.core.emitter.PartyEmitter 6 | import nl.dionsegijn.konfetti.core.models.CoreRect 7 | 8 | /** 9 | * PartySystem is responsible for requesting particles from the emitter and updating the particles 10 | * everytime a new frame is requested. 11 | * @param party configuration class with instructions on how to create the particles for the Emitter 12 | * @param createdAt timestamp of when the partySystem is created 13 | * @param pixelDensity default value taken from resources to measure based on pixelDensity 14 | */ 15 | class PartySystem( 16 | val party: Party, 17 | val createdAt: Long = System.currentTimeMillis(), 18 | pixelDensity: Float, 19 | ) { 20 | var enabled = true 21 | 22 | private var emitter: BaseEmitter = PartyEmitter(party.emitter, pixelDensity) 23 | 24 | private val activeParticles = mutableListOf() 25 | 26 | // Called every frame to create and update the particles state 27 | // returns a list of particles that are ready to be rendered 28 | fun render( 29 | deltaTime: Float, 30 | drawArea: CoreRect, 31 | ): List { 32 | if (enabled) { 33 | activeParticles.addAll(emitter.createConfetti(deltaTime, party, drawArea)) 34 | } 35 | 36 | activeParticles.forEach { it.render(deltaTime, drawArea) } 37 | 38 | activeParticles.removeAll { it.isDead() } 39 | 40 | return activeParticles.filter { it.drawParticle }.map { it.toParticle() } 41 | } 42 | 43 | /** 44 | * When the emitter is done emitting. 45 | * @return true if the emitter is done emitting or false when it's still busy or needs to start 46 | * based on the delay 47 | */ 48 | fun isDoneEmitting(): Boolean = (emitter.isFinished() && activeParticles.size == 0) || (!enabled && activeParticles.size == 0) 49 | 50 | fun getActiveParticleAmount() = activeParticles.size 51 | } 52 | 53 | /** 54 | * Convert a confetti object to a particle object with instructions on how to draw 55 | * the confetti to a canvas 56 | */ 57 | fun Confetti.toParticle(): Particle { 58 | return Particle( 59 | location.x, 60 | location.y, 61 | width, 62 | width, 63 | alphaColor, 64 | rotation, 65 | scaleX, 66 | shape, 67 | alpha, 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/emitter/BaseEmitter.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.emitter 2 | 3 | import nl.dionsegijn.konfetti.core.Party 4 | import nl.dionsegijn.konfetti.core.models.CoreRect 5 | 6 | /** 7 | * An abstract class for creating a custom emitter 8 | * The emitter decides if a particle should be created and when the emitter is finished 9 | */ 10 | abstract class BaseEmitter { 11 | /** 12 | * This function is called on each update when the [RenderSystem] is active 13 | * Keep this function as light as possible otherwise you'll slow down the render system 14 | */ 15 | abstract fun createConfetti( 16 | deltaTime: Float, 17 | party: Party, 18 | drawArea: CoreRect, 19 | ): List 20 | 21 | /** 22 | * @return true if the emitter is no longer creating any particles 23 | * false if is still busy 24 | */ 25 | abstract fun isFinished(): Boolean 26 | } 27 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/emitter/Confetti.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.emitter 2 | 3 | import nl.dionsegijn.konfetti.core.models.CoreRect 4 | import nl.dionsegijn.konfetti.core.models.Shape 5 | import nl.dionsegijn.konfetti.core.models.Vector 6 | import kotlin.math.abs 7 | 8 | /** 9 | * Confetti holds all data to the current state of the particle 10 | * Each frame update triggers the `render` method, which recalculates the particle's properties based on its current state. 11 | * 12 | * @property location The current position of the particle as a Vector object that contains x and y coordinates. 13 | * @property color The color of the particle, represented as an integer. (AARRGGBB) 14 | * @property width The width of the particle in pixels. 15 | * @property mass The mass of the particle, affecting how forces like gravity influence it. A particle with more mass will move slower under the same force. 16 | * @property shape The geometric shape of the particle. 17 | * @property lifespan The duration the particle should exist for in milliseconds. 18 | * @property fadeOut If true, the particle will gradually become transparent over its lifespan. 19 | * @property acceleration The current acceleration of the particle. 20 | * @property velocity The current velocity of the particle. 21 | * @property damping A factor that reduces the particle's velocity over time, simulating air resistance. A higher damping value will slow down the particle faster. 22 | * @property rotationSpeed3D The speed at which the particle rotates. 23 | * @property rotationSpeed2D The speed at which the particle rotates in 2D space. 24 | * @property pixelDensity The pixel density of the device's screen. This is used to ensure that the particle's movement looks consistent across devices with different screen densities. 25 | */ 26 | class Confetti( 27 | var location: Vector, 28 | private val color: Int, 29 | val width: Float, 30 | private val mass: Float, 31 | val shape: Shape, 32 | var lifespan: Long = -1L, 33 | val fadeOut: Boolean = true, 34 | private var acceleration: Vector = Vector(0f, 0f), 35 | var velocity: Vector = Vector(), 36 | var damping: Float, 37 | val rotationSpeed3D: Float = 1f, 38 | val rotationSpeed2D: Float = 1f, 39 | val pixelDensity: Float, 40 | ) { 41 | companion object { 42 | private const val DEFAULT_FRAME_RATE = 60f 43 | private const val GRAVITY = 0.02f 44 | private const val ALPHA_DECREMENT = 5 45 | private const val MILLIS_IN_SECOND = 1000 46 | private const val FULL_CIRCLE = 360f 47 | } 48 | 49 | var rotation = 0f 50 | private var rotationWidth = width 51 | 52 | // Expected frame rate 53 | private var frameRate = DEFAULT_FRAME_RATE 54 | private var gravity = Vector(0f, GRAVITY) 55 | 56 | var alpha: Int = 255 57 | var scaleX = 0f 58 | 59 | /** 60 | * The color of the particle with the current alpha value applied 61 | */ 62 | var alphaColor: Int = 0 63 | 64 | /** 65 | * Determines whether the particle should be drawn. 66 | * Set to false when the particle moves out of the view 67 | */ 68 | var drawParticle = true 69 | private set 70 | 71 | /** 72 | * Returns the size of the particle in pixels 73 | */ 74 | fun getSize(): Float = width 75 | 76 | /** 77 | * Checks if the particle is "dead", i.e., its alpha value has reached 0 78 | */ 79 | fun isDead(): Boolean = alpha <= 0 80 | 81 | /** 82 | * Applies a force to the particle, which affects its acceleration 83 | */ 84 | fun applyForce(force: Vector) { 85 | acceleration.addScaled(force, 1f / mass) 86 | } 87 | 88 | /** 89 | * Updates the state of the particle for each frame of the animation. 90 | */ 91 | fun render( 92 | deltaTime: Float, 93 | drawArea: CoreRect, 94 | ) { 95 | applyForce(gravity) 96 | update(deltaTime, drawArea) 97 | } 98 | 99 | /** 100 | * Updates the state of the particle based on its current acceleration, velocity, and location. 101 | * Also handles the fading out of the particle when its lifespan is over. 102 | */ 103 | private fun update( 104 | deltaTime: Float, 105 | drawArea: CoreRect, 106 | ) { 107 | // Calculate frameRate dynamically, fallback to 60fps in case deltaTime is 0 108 | frameRate = if (deltaTime > 0) 1f / deltaTime else DEFAULT_FRAME_RATE 109 | 110 | if (location.y > drawArea.height) { 111 | alpha = 0 112 | return 113 | } 114 | 115 | velocity.add(acceleration) 116 | velocity.mult(damping) 117 | 118 | location.addScaled(velocity, deltaTime * frameRate * pixelDensity) 119 | 120 | lifespan -= (deltaTime * MILLIS_IN_SECOND).toLong() 121 | if (lifespan <= 0) updateAlpha(deltaTime) 122 | 123 | // 2D rotation around the center of the confetti 124 | rotation += rotationSpeed2D * deltaTime * frameRate 125 | if (rotation >= FULL_CIRCLE) rotation = 0f 126 | 127 | // 3D rotation effect by decreasing the width and make sure that rotationSpeed is always 128 | // positive by using abs 129 | rotationWidth -= abs(rotationSpeed3D) * deltaTime * frameRate 130 | if (rotationWidth < 0) rotationWidth = width 131 | 132 | scaleX = abs(rotationWidth / width - 0.5f) * 2 133 | alphaColor = (alpha shl 24) or (color and 0xffffff) 134 | 135 | drawParticle = drawArea.contains(location.x.toInt(), location.y.toInt()) 136 | } 137 | 138 | private fun updateAlpha(deltaTime: Float) { 139 | alpha = 140 | if (fadeOut) { 141 | val interval = ALPHA_DECREMENT * deltaTime * frameRate 142 | (alpha - interval.toInt()).coerceAtLeast(0) 143 | } else { 144 | 0 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/emitter/EmitterConfig.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.emitter 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | /** 6 | * Emitter class that holds the duration that the emitter will create confetti particles 7 | */ 8 | data class Emitter( 9 | val duration: Long, 10 | val timeUnit: TimeUnit = TimeUnit.MILLISECONDS, 11 | ) { 12 | /** 13 | * Max amount of particles that will be created over the duration that is set 14 | */ 15 | fun max(amount: Int): EmitterConfig = EmitterConfig(this).max(amount) 16 | 17 | /** 18 | * Amount of particles that will be created per second 19 | */ 20 | fun perSecond(amount: Int): EmitterConfig = EmitterConfig(this).perSecond(amount) 21 | } 22 | 23 | /** 24 | * EmitterConfig class that will gold the Emitter configuration and amount of particles that 25 | * will be created over certain time 26 | */ 27 | class EmitterConfig( 28 | emitter: Emitter, 29 | ) { 30 | /** Max time allowed to emit in milliseconds */ 31 | var emittingTime: Long = 0 32 | 33 | /** Amount of time needed for each particle creation in milliseconds */ 34 | var amountPerMs: Float = 0f 35 | 36 | init { 37 | val (duration, timeUnit) = emitter 38 | this.emittingTime = TimeUnit.MILLISECONDS.convert(duration, timeUnit) 39 | } 40 | 41 | /** 42 | * Amount of particles created over the duration that is set 43 | */ 44 | fun max(amount: Int): EmitterConfig { 45 | this.amountPerMs = (emittingTime / amount) / 1000f 46 | return this 47 | } 48 | 49 | /** 50 | * Amount of particles that will be created per second 51 | */ 52 | fun perSecond(amount: Int): EmitterConfig { 53 | this.amountPerMs = 1f / amount 54 | return this 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/emitter/PartyEmitter.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.emitter 2 | 3 | import nl.dionsegijn.konfetti.core.Party 4 | import nl.dionsegijn.konfetti.core.Position 5 | import nl.dionsegijn.konfetti.core.Rotation 6 | import nl.dionsegijn.konfetti.core.models.CoreRect 7 | import nl.dionsegijn.konfetti.core.models.Shape 8 | import nl.dionsegijn.konfetti.core.models.Size 9 | import nl.dionsegijn.konfetti.core.models.Vector 10 | import java.lang.Math.toRadians 11 | import java.util.Random 12 | import kotlin.math.cos 13 | import kotlin.math.sin 14 | 15 | /** 16 | * Emitter is responsible for creating a certain amount of particles per tick. 17 | * - Creating x amount of particles in a certain time frame 18 | * - Creating x amount of particles until the threshold [maxParticles] is met 19 | */ 20 | class PartyEmitter( 21 | private val emitterConfig: EmitterConfig, 22 | private val pixelDensity: Float, 23 | private val random: Random = Random(), 24 | ) : BaseEmitter() { 25 | // Keeping count of how many particles are created whilst running the emitter 26 | private var particlesCreated = 0 27 | 28 | /** Elapsed time in milliseconds */ 29 | private var elapsedTime: Float = 0f 30 | 31 | /** Amount of time elapsed since last particle creation in milliseconds */ 32 | private var createParticleMs: Float = 0f 33 | 34 | /** 35 | * If timer isn't started yet, set initial start time 36 | * Create the first confetti immediately and update the last emitting time 37 | */ 38 | override fun createConfetti( 39 | deltaTime: Float, 40 | party: Party, 41 | drawArea: CoreRect, 42 | ): List { 43 | createParticleMs += deltaTime 44 | 45 | // Initial deltaTime can't be higher than the emittingTime, if so calculate 46 | // amount of particles based on max emittingTime 47 | val emittingTime = emitterConfig.emittingTime / 1000f 48 | if (elapsedTime == 0f && deltaTime > emittingTime) { 49 | createParticleMs = emittingTime 50 | } 51 | 52 | var particles = listOf() 53 | 54 | // Check if particle should be created 55 | if (createParticleMs >= emitterConfig.amountPerMs && !isTimeElapsed()) { 56 | // Calculate how many particle to create in the elapsed time 57 | val amount: Int = (createParticleMs / emitterConfig.amountPerMs).toInt() 58 | 59 | particles = (1..amount).map { createParticle(party, drawArea) } 60 | 61 | // Reset timer and add left over time for next cycle 62 | createParticleMs %= emitterConfig.amountPerMs 63 | } 64 | 65 | elapsedTime += deltaTime * 1000 66 | return particles 67 | } 68 | 69 | /** 70 | * Create particle based on the [Party] configuration 71 | * @param party Configurations used for creating the initial Confetti states 72 | * @param drawArea the area and size of the canvas 73 | */ 74 | private fun createParticle( 75 | party: Party, 76 | drawArea: CoreRect, 77 | ): Confetti { 78 | particlesCreated++ 79 | with(party) { 80 | val randomSize = size[random.nextInt(size.size)] 81 | return Confetti( 82 | location = position.get(drawArea).run { Vector(x, y) }, 83 | width = randomSize.sizeInDp * pixelDensity, 84 | mass = randomSize.massWithVariance(), 85 | shape = getRandomShape(shapes), 86 | color = colors[random.nextInt(colors.size)], 87 | lifespan = timeToLive, 88 | fadeOut = fadeOutEnabled, 89 | velocity = getVelocity(), 90 | damping = party.damping, 91 | rotationSpeed2D = rotation.rotationSpeed() * party.rotation.multiplier2D, 92 | rotationSpeed3D = rotation.rotationSpeed() * party.rotation.multiplier3D, 93 | pixelDensity = pixelDensity, 94 | ) 95 | } 96 | } 97 | 98 | /** 99 | * Calculate a rotation speed multiplier based on the base and variance 100 | * @return rotation speed and return 0 when rotation is disabled 101 | */ 102 | private fun Rotation.rotationSpeed(): Float { 103 | if (!enabled) return 0f 104 | val randomValue = random.nextFloat() * 2f - 1f 105 | return speed + (speed * variance * randomValue) 106 | } 107 | 108 | private fun Party.getSpeed(): Float = 109 | if (maxSpeed == -1f) { 110 | speed 111 | } else { 112 | ((maxSpeed - speed) * random.nextFloat()) + speed 113 | } 114 | 115 | /** 116 | * Get the mass with a slight variance added to create randomness between how each particle 117 | * will react in speed when moving up or down 118 | */ 119 | private fun Size.massWithVariance(): Float = mass + (mass * (random.nextFloat() * massVariance)) 120 | 121 | /** 122 | * Calculate velocity based on radian and speed 123 | * @return [Vector] velocity 124 | */ 125 | private fun Party.getVelocity(): Vector { 126 | val speed = getSpeed() 127 | val radian = toRadians(getAngle()) 128 | val vx = speed * cos(radian).toFloat() 129 | val vy = speed * sin(radian).toFloat() 130 | return Vector(vx, vy) 131 | } 132 | 133 | private fun Party.getAngle(): Double { 134 | if (spread == 0) return angle.toDouble() 135 | 136 | val minAngle = angle - (spread / 2) 137 | val maxAngle = angle + (spread / 2) 138 | return (maxAngle - minAngle) * random.nextDouble() + minAngle 139 | } 140 | 141 | private fun Position.get(drawArea: CoreRect): Position.Absolute { 142 | return when (this) { 143 | is Position.Absolute -> Position.Absolute(x, y) 144 | is Position.Relative -> { 145 | Position.Absolute( 146 | drawArea.width * x.toFloat(), 147 | drawArea.height * y.toFloat(), 148 | ) 149 | } 150 | is Position.Between -> { 151 | val minPos = min.get(drawArea) 152 | val maxPos = max.get(drawArea) 153 | return Position.Absolute( 154 | x = random.nextFloat().times(maxPos.x.minus(minPos.x)) + minPos.x, 155 | y = random.nextFloat().times(maxPos.y.minus(minPos.y)) + minPos.y, 156 | ) 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Get a random shape from the list of shapes 163 | */ 164 | private fun getRandomShape(shapes: List): Shape { 165 | return shapes[random.nextInt(shapes.size)] 166 | } 167 | 168 | /** 169 | * If the [duration] is 0 it's not set and not relevant 170 | * If the emitting time is set check if [elapsedTime] exceeded the emittingTime 171 | */ 172 | private fun isTimeElapsed(): Boolean { 173 | return when (emitterConfig.emittingTime) { 174 | 0L -> false 175 | else -> elapsedTime >= emitterConfig.emittingTime 176 | } 177 | } 178 | 179 | override fun isFinished(): Boolean { 180 | return if (emitterConfig.emittingTime > 0L) { 181 | elapsedTime >= emitterConfig.emittingTime 182 | } else { 183 | false 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/models/CoreImage.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.models 2 | 3 | interface CoreImage { 4 | val width: Int 5 | val height: Int 6 | } 7 | 8 | data class ReferenceImage( 9 | val reference: Int, 10 | override val width: Int, 11 | override val height: Int, 12 | ) : CoreImage 13 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/models/CoreImageStore.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.models 2 | 3 | interface CoreImageStore { 4 | fun storeImage(image: T): Int 5 | 6 | fun getImage(id: Int): T? 7 | } 8 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/models/CoreRect.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.models 2 | 3 | interface CoreRect { 4 | var x: Float 5 | var y: Float 6 | var width: Float 7 | var height: Float 8 | 9 | fun set( 10 | x: Float, 11 | y: Float, 12 | width: Float, 13 | height: Float, 14 | ) { 15 | this.x = x 16 | this.y = y 17 | this.width = width 18 | this.height = height 19 | } 20 | 21 | fun contains( 22 | px: Int, 23 | py: Int, 24 | ): Boolean { 25 | return px >= x && px <= x + width && py >= y && py <= y + height 26 | } 27 | } 28 | 29 | class CoreRectImpl( 30 | override var x: Float = 0f, 31 | override var y: Float = 0f, 32 | override var width: Float = 0f, 33 | override var height: Float = 0f, 34 | ) : CoreRect { 35 | override fun set( 36 | x: Float, 37 | y: Float, 38 | width: Float, 39 | height: Float, 40 | ) { 41 | super.set(x, y, width, height) 42 | } 43 | 44 | override fun contains( 45 | px: Int, 46 | py: Int, 47 | ): Boolean { 48 | return super.contains(px, py) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/models/Shape.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.models 2 | 3 | sealed interface Shape { 4 | object Circle : Shape { 5 | // Default replacement for RectF 6 | val rect = CoreRectImpl() 7 | } 8 | 9 | object Square : Shape 10 | 11 | class Rectangle( 12 | /** The ratio of height to width. Must be within range [0, 1] */ 13 | val heightRatio: Float, 14 | ) : Shape { 15 | init { 16 | require(heightRatio in 0f..1f) 17 | } 18 | } 19 | 20 | /** 21 | * A drawable shape 22 | * @param image CoreImage 23 | * @param tint Set to `false` to opt out of tinting the drawable, keeping its original colors. 24 | * @param applyAlpha Set to false to not apply alpha to drawables 25 | */ 26 | data class DrawableShape( 27 | val image: CoreImage, 28 | val tint: Boolean = true, 29 | val applyAlpha: Boolean = true, 30 | ) : Shape { 31 | val heightRatio = 32 | if (image.height == -1 && image.width == -1) { 33 | // If the image has no intrinsic size, fill the available space. 34 | 1f 35 | } else if (image.height == -1 || image.width == -1) { 36 | // Currently cannot handle an image with only one intrinsic dimension. 37 | 0f 38 | } else { 39 | image.height.toFloat() / image.width.toFloat() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/models/Size.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.models 2 | 3 | /** 4 | * @property sizeInDp the size of the confetti in dip 5 | * @property mass each size can have its own mass for slightly different behavior. For example, the closer 6 | * the mass is to zero the easier it will accelerate but the slower it will will fall down due to gravity. 7 | * @property massVariance create slight randomness how particles react to gravity. This variance 8 | * is a percentage based on [mass]. The higher the variance the bigger the difference in mass between 9 | * each particle is. Default is 0.2f for a slight difference in mass for each particle. 10 | */ 11 | data class Size(val sizeInDp: Int, val mass: Float = 5f, val massVariance: Float = 0.2f) { 12 | init { 13 | require(mass != 0F) { "mass=$mass must be != 0" } 14 | } 15 | 16 | companion object { 17 | val SMALL: Size = Size(sizeInDp = 6, mass = 4f) 18 | val MEDIUM: Size = Size(8) 19 | val LARGE: Size = Size(10, mass = 6f) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /konfetti/core/src/main/java/nl/dionsegijn/konfetti/core/models/Vector.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.models 2 | 3 | data class Vector(var x: Float = 0f, var y: Float = 0f) { 4 | fun add(v: Vector) { 5 | x += v.x 6 | y += v.y 7 | } 8 | 9 | fun addScaled( 10 | v: Vector, 11 | s: Float, 12 | ) { 13 | x += v.x * s 14 | y += v.y * s 15 | } 16 | 17 | fun mult(n: Float) { 18 | x *= n 19 | y *= n 20 | } 21 | 22 | fun div(n: Float) { 23 | x /= n 24 | y /= n 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /konfetti/core/src/test/java/nl/dionsegijn/konfetti/core/PartySystemTest.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core 2 | 3 | import nl.dionsegijn.konfetti.core.emitter.Emitter 4 | import nl.dionsegijn.konfetti.core.models.CoreRect 5 | import nl.dionsegijn.konfetti.core.models.CoreRectImpl 6 | import org.junit.Assert 7 | import org.junit.Test 8 | import org.mockito.ArgumentMatchers.anyInt 9 | import org.mockito.Mockito 10 | 11 | class PartySystemTest { 12 | private val rect: CoreRect = 13 | Mockito.mock(CoreRectImpl::class.java).apply { 14 | Mockito.`when`(height).thenReturn(1000f) 15 | Mockito.`when`(contains(anyInt(), anyInt())).thenReturn(true) 16 | } 17 | 18 | // Average between for each frame 19 | private val deltaTime = 0.017f 20 | 21 | @Test 22 | fun `Test creating particle every 25ms`() { 23 | val party = 24 | Party( 25 | emitter = Emitter(100L).max(4), 26 | ) 27 | val system = PartySystem(party, pixelDensity = 1f) 28 | 29 | Assert.assertTrue(system.enabled) 30 | Assert.assertFalse(system.isDoneEmitting()) 31 | 32 | val r1 = system.render(deltaTime, rect) // render 2, total deltaTime = 0.017f 33 | Assert.assertEquals(0, r1.size) // Expected 0, Every 0.025ms a new particle should be created 34 | 35 | val r2 = system.render(deltaTime, rect) // render 2, total deltaTime = 2 * 0.017f = 0.034f 36 | Assert.assertEquals(1, r2.size) // Expected 1, one for every 0.025ms 37 | 38 | val r3 = system.render(deltaTime, rect) // render 3, total deltaTime = 3 * 0.017f = 0.051f 39 | Assert.assertEquals(2, r3.size) // expected 2, one for every 0.025ms 40 | } 41 | 42 | @Test 43 | fun `Test PartySystem set to disabled stops generating particles`() { 44 | val party = 45 | Party( 46 | emitter = Emitter(100L).max(4), 47 | ) 48 | val system = PartySystem(party, pixelDensity = 1f) 49 | 50 | Assert.assertTrue(system.enabled) 51 | Assert.assertFalse(system.isDoneEmitting()) 52 | 53 | val r1 = system.render(deltaTime, rect) // render 2, total deltaTime = 0.017f 54 | Assert.assertEquals(0, r1.size) // Expected 0, Every 0.025ms a new particle should be created 55 | 56 | val r2 = system.render(deltaTime, rect) // render 2, total deltaTime = 2 * 0.017f = 0.034f 57 | Assert.assertEquals(1, r2.size) // Expected 1, one for every 0.025ms 58 | 59 | // System set to false, emitter will no longer asked for new particles 60 | system.enabled = false 61 | Assert.assertFalse(system.enabled) 62 | 63 | // Should not longer create new particles even though time has passed 64 | val r3 = system.render(deltaTime, rect) 65 | Assert.assertEquals(1, r3.size) 66 | } 67 | 68 | @Test 69 | fun `Test PartySystem is done Emitting`() { 70 | val party = 71 | Party( 72 | timeToLive = 1L, 73 | fadeOutEnabled = false, 74 | emitter = Emitter(100).max(2), 75 | ) 76 | val system = PartySystem(party, pixelDensity = 1f) 77 | 78 | // Set drawArea to 1 pixel to let every particle directly disappear for this test 79 | Mockito.`when`(rect.height).thenReturn(1f) 80 | Assert.assertTrue(system.enabled) 81 | 82 | system.render(deltaTime, rect) // dt: 0.017f 83 | system.render(deltaTime, rect) // dt: 0.034f 84 | system.render(deltaTime, rect) // dt: 0.051f 85 | system.render(deltaTime, rect) // dt: 0.068f 86 | system.render(deltaTime, rect) // dt: 0.085f 87 | 88 | // should still run because emitter isn't done yet, total delta time is < 100ms 89 | Assert.assertFalse(system.isDoneEmitting()) 90 | 91 | system.render(deltaTime, rect) // dt: 0.102f // duration is higher than 100ms 92 | 93 | Assert.assertEquals(0, system.getActiveParticleAmount()) 94 | Assert.assertTrue(system.isDoneEmitting()) 95 | } 96 | 97 | @Test 98 | fun `Test PartySystem remove dead particles`() { 99 | // removes particles after two frames 100 | // Create particle every 20ms 101 | val party = 102 | Party( 103 | timeToLive = 18L, 104 | fadeOutEnabled = false, 105 | emitter = Emitter(100L).max(5), 106 | ) 107 | val system = PartySystem(party, pixelDensity = 1f) 108 | 109 | // Every 20ms a new particle is created and every two frames they're removed 110 | 111 | system.render(deltaTime, rect) // dt: 0.017f 112 | system.render(deltaTime, rect) // dt: 0.034f 113 | Assert.assertEquals(1, system.getActiveParticleAmount()) 114 | 115 | system.render(deltaTime, rect) // dt: 0.051f 116 | system.render(deltaTime, rect) // dt: 0.068f 117 | system.render(deltaTime, rect) // dt: 0.085f 118 | system.render(deltaTime, rect) // dt: 0.102f 119 | 120 | // All particles are created and one extra frame is executed to remove the last one 121 | system.render(deltaTime, rect) // dt: 0.119f 122 | Assert.assertEquals(0, system.getActiveParticleAmount()) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /konfetti/core/src/test/java/nl/dionsegijn/konfetti/core/emitter/PartyEmitterTest.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.core.emitter 2 | 3 | import nl.dionsegijn.konfetti.core.Angle 4 | import nl.dionsegijn.konfetti.core.Party 5 | import nl.dionsegijn.konfetti.core.Position 6 | import nl.dionsegijn.konfetti.core.Rotation 7 | import nl.dionsegijn.konfetti.core.models.CoreRect 8 | import nl.dionsegijn.konfetti.core.models.CoreRectImpl 9 | import nl.dionsegijn.konfetti.core.models.Shape 10 | import nl.dionsegijn.konfetti.core.models.Size 11 | import nl.dionsegijn.konfetti.core.models.Vector 12 | import org.junit.Assert 13 | import org.junit.Test 14 | import org.mockito.ArgumentMatchers 15 | import org.mockito.Mockito 16 | import java.util.Random 17 | 18 | class PartyEmitterTest { 19 | private val drawArea: CoreRect = 20 | Mockito.mock(CoreRectImpl::class.java).apply { 21 | Mockito.`when`(height).thenReturn(1000f) 22 | Mockito.`when`(width).thenReturn(1000f) 23 | Mockito.`when`(contains(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) 24 | .thenReturn(true) 25 | } 26 | 27 | // Average time between for each frame 28 | private val deltaTime = 0.017f 29 | 30 | // Test Party object 31 | // Create confetti every 10ms 32 | private val party = 33 | Party( 34 | angle = Angle.TOP, 35 | spread = 0, 36 | speed = 30f, 37 | maxSpeed = -1f, 38 | damping = 0.9f, 39 | size = listOf(Size(sizeInDp = 6, mass = 5f, massVariance = 0f)), 40 | colors = listOf(0xFF0000), 41 | shapes = listOf(Shape.Square), 42 | timeToLive = 1000L, 43 | fadeOutEnabled = false, 44 | position = Position.Absolute(100f, 100f), 45 | delay = 0, 46 | rotation = Rotation(), 47 | emitter = Emitter(100L).max(10), 48 | ) 49 | 50 | @Test 51 | fun `Create confetti every 25ms and then finish`() { 52 | // Create confetti every 25ms 53 | val party = 54 | Party( 55 | emitter = Emitter(100L).max(4), 56 | ) 57 | val emitter = PartyEmitter(party.emitter, 1f) 58 | 59 | val r1 = emitter.createConfetti(deltaTime, party, drawArea) // 0.017f 60 | Assert.assertEquals(0, r1.size) 61 | 62 | val r2 = emitter.createConfetti(deltaTime, party, drawArea) // 0.034f 63 | Assert.assertEquals(1, r2.size) 64 | 65 | val r3 = emitter.createConfetti(deltaTime, party, drawArea) // 0.051f 66 | Assert.assertEquals(1, r3.size) 67 | 68 | val r4 = emitter.createConfetti(deltaTime, party, drawArea) // 0.068f 69 | Assert.assertEquals(0, r4.size) 70 | 71 | val r5 = emitter.createConfetti(deltaTime, party, drawArea) // 0.085f 72 | Assert.assertEquals(1, r5.size) 73 | Assert.assertFalse(emitter.isFinished()) 74 | 75 | val r6 = emitter.createConfetti(deltaTime, party, drawArea) // 0.102f 76 | Assert.assertEquals(1, r6.size) 77 | Assert.assertTrue(emitter.isFinished()) 78 | } 79 | 80 | @Test 81 | fun `Create confetti and check its initial state`() { 82 | val emitter = PartyEmitter(party.emitter, 1f, Random(1L)) 83 | 84 | val r1 = emitter.createConfetti(deltaTime, party, drawArea) // 0.017f 85 | with(r1.first()) { 86 | Assert.assertEquals(Vector(100f, 100f), location) 87 | Assert.assertEquals(6f, width) 88 | Assert.assertEquals(Shape.Square, shape) 89 | Assert.assertEquals(1000L, lifespan) 90 | Assert.assertEquals(0.9f, damping) 91 | Assert.assertEquals(5.6617184f, rotationSpeed2D) 92 | Assert.assertEquals(0.804353f, rotationSpeed3D) 93 | } 94 | } 95 | 96 | @Test 97 | fun `Initial state confetti with rotation disabled`() { 98 | val emitter = PartyEmitter(party.emitter, 1f) 99 | 100 | val r1 = 101 | emitter.createConfetti( 102 | deltaTime, 103 | party.copy(rotation = Rotation.disabled()), 104 | drawArea, 105 | ) // 0.017f 106 | 107 | with(r1.first()) { 108 | Assert.assertEquals(0.0f, rotationSpeed2D) 109 | Assert.assertEquals(0.0f, rotationSpeed3D) 110 | } 111 | } 112 | 113 | @Test 114 | fun `Initial state confetti with relative position`() { 115 | val emitter = PartyEmitter(party.emitter, 1f) 116 | 117 | val r1 = 118 | emitter.createConfetti( 119 | deltaTime, 120 | party.copy(position = Position.Relative(0.5, 0.5)), 121 | drawArea, 122 | ) // 0.017f 123 | 124 | with(r1.first()) { 125 | Assert.assertEquals(Vector(500f, 500f), location) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /konfetti/core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /konfetti/xml/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /konfetti/xml/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("com.diffplug.spotless") 5 | } 6 | 7 | NexusConfig.PUBLISH_ARTIFACT_ID = "konfetti-xml" 8 | apply(from = "../../scripts/publish-module.gradle.kts") 9 | 10 | spotless { 11 | kotlin { 12 | ktlint("1.1.0") 13 | target("src/**/*.kt") 14 | } 15 | java { 16 | removeUnusedImports() 17 | googleJavaFormat("1.15.0") 18 | target("**/*.java") 19 | } 20 | } 21 | 22 | android { 23 | compileSdk = buildVersions.compileSdk 24 | 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | 34 | defaultConfig { 35 | minSdk = 15 36 | } 37 | buildTypes { 38 | release { 39 | isMinifyEnabled = false 40 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 41 | } 42 | } 43 | namespace = "nl.dionsegijn.konfetti.xml" 44 | lint { 45 | abortOnError = true 46 | baseline = file("lint-baseline.xml") 47 | } 48 | } 49 | 50 | dependencies { 51 | debugApi(project(path = ":konfetti:core")) 52 | releaseApi("nl.dionsegijn:konfetti-core:${Constants.konfettiVersion}") 53 | 54 | implementation("org.jetbrains.kotlin:kotlin-stdlib:${Constants.kotlinVersion}") 55 | 56 | testImplementation(libs.test.junit) 57 | testImplementation(libs.test.mockito) 58 | } 59 | -------------------------------------------------------------------------------- /konfetti/xml/lint-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /konfetti/xml/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/dionsegijn/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle.kts. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/java/nl/dionsegijn/konfetti/xml/DrawShapes.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml 2 | 3 | import android.graphics.BlendMode 4 | import android.graphics.BlendModeColorFilter 5 | import android.graphics.Canvas 6 | import android.graphics.Paint 7 | import android.graphics.PorterDuff 8 | import android.graphics.RectF 9 | import android.os.Build 10 | import nl.dionsegijn.konfetti.core.models.ReferenceImage 11 | import nl.dionsegijn.konfetti.core.models.Shape 12 | import nl.dionsegijn.konfetti.core.models.Shape.Circle 13 | import nl.dionsegijn.konfetti.core.models.Shape.Circle.rect 14 | import nl.dionsegijn.konfetti.core.models.Shape.DrawableShape 15 | import nl.dionsegijn.konfetti.core.models.Shape.Rectangle 16 | import nl.dionsegijn.konfetti.core.models.Shape.Square 17 | import nl.dionsegijn.konfetti.xml.image.ImageStore 18 | 19 | /** 20 | * Draw a shape to `canvas`. Implementations are expected to draw within a square of size 21 | * `size` and must vertically/horizontally center their asset if it does not have an equal width 22 | * and height. 23 | */ 24 | fun Shape.draw( 25 | canvas: Canvas, 26 | paint: Paint, 27 | size: Float, 28 | imageStore: ImageStore, 29 | ) { 30 | when (this) { 31 | Square -> canvas.drawRect(0f, 0f, size, size, paint) 32 | Circle -> { 33 | rect.set(0f, 0f, size, size) 34 | canvas.drawOval(RectF(rect.x, rect.y, rect.width, rect.height), paint) 35 | } 36 | is Rectangle -> { 37 | val height = size * heightRatio 38 | val top = (size - height) / 2f 39 | canvas.drawRect(0f, top, size, top + height, paint) 40 | } 41 | is DrawableShape -> { 42 | val referenceImage = image 43 | if (referenceImage is ReferenceImage) { 44 | // Making use of the ImageStore for performance reasons, see ImageStore for more info 45 | val drawable = imageStore.getImage(referenceImage.reference) ?: return 46 | 47 | if (tint) { 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 49 | drawable.colorFilter = BlendModeColorFilter(paint.color, BlendMode.SRC_IN) 50 | } else { 51 | drawable.setColorFilter(paint.color, PorterDuff.Mode.SRC_IN) 52 | } 53 | } else if (applyAlpha) { 54 | drawable.alpha = paint.alpha 55 | } 56 | 57 | val height = (size * heightRatio).toInt() 58 | val top = ((size - height) / 2f).toInt() 59 | 60 | drawable.setBounds(0, top, size.toInt(), top + height) 61 | drawable.draw(canvas) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/java/nl/dionsegijn/konfetti/xml/KonfettiView.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.Canvas 6 | import android.graphics.Paint 7 | import android.graphics.drawable.Drawable 8 | import android.util.AttributeSet 9 | import android.view.View 10 | import nl.dionsegijn.konfetti.core.Particle 11 | import nl.dionsegijn.konfetti.core.Party 12 | import nl.dionsegijn.konfetti.core.PartySystem 13 | import nl.dionsegijn.konfetti.core.models.CoreRectImpl 14 | import nl.dionsegijn.konfetti.core.models.ReferenceImage 15 | import nl.dionsegijn.konfetti.core.models.Shape 16 | import nl.dionsegijn.konfetti.xml.image.DrawableImage 17 | import nl.dionsegijn.konfetti.xml.image.ImageStore 18 | import nl.dionsegijn.konfetti.xml.listeners.OnParticleSystemUpdateListener 19 | 20 | /** 21 | * Implement this view to render the particles on. 22 | * Call [build] to setup a particle system. KonfettiView will then invalidate 23 | * pass the canvas to each system where each system will handle the rendering. 24 | */ 25 | open class KonfettiView : View { 26 | constructor(context: Context?) : super(context) 27 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 28 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) 29 | 30 | /** 31 | * Active particle systems 32 | */ 33 | private val systems: MutableList = mutableListOf() 34 | 35 | /** 36 | * Keeping track of the delta time between frame rendering 37 | */ 38 | private var timer: TimerIntegration = TimerIntegration() 39 | 40 | private var drawArea = CoreRectImpl() 41 | 42 | /** 43 | * [OnParticleSystemUpdateListener] listener to notify when a new particle system 44 | * starts rendering and when a particle system stopped rendering 45 | */ 46 | var onParticleSystemUpdateListener: OnParticleSystemUpdateListener? = null 47 | 48 | fun getActiveSystems() = systems 49 | 50 | private val imageStore = ImageStore() 51 | 52 | /** 53 | * Check if current systems are active rendering particles. 54 | * @return true if konfetti is actively rendering 55 | * false if everything stopped rendering 56 | */ 57 | fun isActive() = systems.isNotEmpty() 58 | 59 | override fun onDraw(canvas: Canvas) { 60 | super.onDraw(canvas) 61 | val deltaTime = timer.getDeltaTime() 62 | for (i in systems.size - 1 downTo 0) { 63 | val partySystem = systems[i] 64 | 65 | val totalTimeRunning = timer.getTotalTimeRunning(partySystem.createdAt) 66 | if (totalTimeRunning >= partySystem.party.delay) { 67 | partySystem.render(deltaTime, drawArea).forEach { 68 | it.display(canvas) 69 | } 70 | } 71 | 72 | if (partySystem.isDoneEmitting()) { 73 | systems.removeAt(i) 74 | onParticleSystemUpdateListener?.onParticleSystemEnded(this, partySystem.party, systems.size) 75 | } 76 | } 77 | 78 | if (systems.size != 0) { 79 | invalidate() 80 | } else { 81 | timer.reset() 82 | } 83 | } 84 | 85 | private val paint: Paint = Paint() 86 | 87 | private fun Particle.display(canvas: Canvas) { 88 | // setting alpha via paint.setAlpha allocates a temporary "ColorSpace$Named" object 89 | // it is more efficient via setColor 90 | paint.color = color 91 | 92 | val centerX = scaleX * width / 2 93 | 94 | val saveCount = canvas.save() 95 | canvas.translate(x - centerX, y) 96 | canvas.rotate(rotation, centerX, width / 2) 97 | canvas.scale(scaleX, 1f) 98 | 99 | shape.draw(canvas, paint, width, imageStore) 100 | canvas.restoreToCount(saveCount) 101 | } 102 | 103 | fun start(vararg party: Party) { 104 | systems.addAll( 105 | party.map { 106 | onParticleSystemUpdateListener?.onParticleSystemStarted(this, it, systems.size) 107 | PartySystem(party = storeImages(it), pixelDensity = Resources.getSystem().displayMetrics.density) 108 | }, 109 | ) 110 | invalidate() 111 | } 112 | 113 | fun start(party: List) { 114 | systems.addAll( 115 | party.map { 116 | storeImages(it) 117 | onParticleSystemUpdateListener?.onParticleSystemStarted(this, it, systems.size) 118 | PartySystem(party = storeImages(it), pixelDensity = Resources.getSystem().displayMetrics.density) 119 | }, 120 | ) 121 | invalidate() 122 | } 123 | 124 | fun start(party: Party) { 125 | onParticleSystemUpdateListener?.onParticleSystemStarted(this, party, systems.size) 126 | systems.add(PartySystem(party = storeImages(party), pixelDensity = Resources.getSystem().displayMetrics.density)) 127 | invalidate() 128 | } 129 | 130 | /** 131 | * Transforms the shapes in the given [Party] object. If a shape is a [Shape.DrawableShape], 132 | * it replaces the [DrawableImage] with a [ReferenceImage] and stores the [Drawable] in the [ImageStore]. 133 | * 134 | * @param party The Party object containing the shapes to be transformed. 135 | * @return A new Party object with the transformed shapes. 136 | */ 137 | private fun storeImages(party: Party): Party { 138 | val transformedShapes = 139 | party.shapes.map { shape -> 140 | when (shape) { 141 | is Shape.DrawableShape -> { 142 | val referenceImage = drawableToReferenceImage(shape.image as DrawableImage) 143 | shape.copy(image = referenceImage) 144 | } 145 | else -> shape 146 | } 147 | } 148 | return party.copy(shapes = transformedShapes) 149 | } 150 | 151 | /** 152 | * Converts a [DrawableImage] to a [ReferenceImage] and stores the [Drawable] in the [ImageStore]. 153 | * 154 | * @param drawableImage The DrawableImage to be converted. 155 | * @return A ReferenceImage with the same dimensions as the DrawableImage and a reference to the stored Drawable. 156 | */ 157 | fun drawableToReferenceImage(drawableImage: DrawableImage): ReferenceImage { 158 | val id = imageStore.storeImage(drawableImage.drawable) 159 | return ReferenceImage(id, drawableImage.width, drawableImage.height) 160 | } 161 | 162 | /** 163 | * Stop a particular particle system. All particles belonging to this system will directly disappear from the view. 164 | */ 165 | fun stop(party: Party) { 166 | systems.removeAll { it.party == party } 167 | onParticleSystemUpdateListener?.onParticleSystemEnded(this, party, systems.size) 168 | } 169 | 170 | /** 171 | * Abruptly stop all particle systems from rendering 172 | * The canvas will stop drawing all particles. Everything that's being rendered will directly 173 | * disappear from the view. 174 | */ 175 | fun reset() { 176 | systems.clear() 177 | } 178 | 179 | /** 180 | * Stop the particle systems from rendering new particles. All particles already visible 181 | * will continue rendering until they're done. When all particles are done rendering the system 182 | * will be removed. 183 | */ 184 | fun stopGracefully() { 185 | systems.forEach { it.enabled = false } 186 | } 187 | 188 | /** 189 | * TimerIntegration retrieves the delta time since the rendering of the previous frame. 190 | * Delta time is used to draw the confetti correctly if any frame drops occur. 191 | */ 192 | class TimerIntegration { 193 | private var previousTime: Long = -1L 194 | 195 | fun reset() { 196 | previousTime = -1L 197 | } 198 | 199 | fun getDeltaTime(): Float { 200 | if (previousTime == -1L) previousTime = System.nanoTime() 201 | 202 | val currentTime = System.nanoTime() 203 | val dt = (currentTime - previousTime) / 1000000f 204 | previousTime = currentTime 205 | return dt / 1000 206 | } 207 | 208 | fun getTotalTimeRunning(startTime: Long): Long { 209 | val currentTime = System.currentTimeMillis() 210 | return (currentTime - startTime) 211 | } 212 | } 213 | 214 | override fun onSizeChanged( 215 | w: Int, 216 | h: Int, 217 | oldw: Int, 218 | oldh: Int, 219 | ) { 220 | super.onSizeChanged(w, h, oldw, oldh) 221 | drawArea = CoreRectImpl(0f, 0f, w.toFloat(), h.toFloat()) 222 | } 223 | 224 | override fun onVisibilityChanged( 225 | changedView: View, 226 | visibility: Int, 227 | ) { 228 | super.onVisibilityChanged(changedView, visibility) 229 | timer.reset() 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/java/nl/dionsegijn/konfetti/xml/image/DrawableImage.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.image 2 | 3 | import android.graphics.drawable.Drawable 4 | import nl.dionsegijn.konfetti.core.models.CoreImage 5 | 6 | data class DrawableImage( 7 | val drawable: Drawable, 8 | override val width: Int, 9 | override val height: Int, 10 | ) : CoreImage 11 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/java/nl/dionsegijn/konfetti/xml/image/ImageStore.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.image 2 | 3 | import android.graphics.drawable.Drawable 4 | import nl.dionsegijn.konfetti.core.models.CoreImageStore 5 | 6 | /** 7 | * The ImageStore class is used to store Drawable objects and provide a way to reference them. 8 | * This is done for performance reasons and to allow the core library, which can't use Android Drawables, 9 | * to work with images. 10 | * 11 | * Instead of converting a Drawable to a ByteBuffer, then to a Bitmap, and then back to a Drawable, 12 | * which is inefficient for the render code, the Drawable is stored in the ImageStore. 13 | * The rest of the application can then work with a simple integer reference to the Drawable. 14 | * 15 | * The ImageStore provides methods to store a Drawable and retrieve it using its reference. 16 | */ 17 | class ImageStore : CoreImageStore { 18 | private val images = mutableMapOf() 19 | 20 | override fun storeImage(image: Drawable): Int { 21 | val id = image.hashCode() 22 | images[id] = image 23 | return id 24 | } 25 | 26 | override fun getImage(id: Int): Drawable? { 27 | return images[id] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/java/nl/dionsegijn/konfetti/xml/image/ImageUtil.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.image 2 | 3 | import android.graphics.drawable.Drawable 4 | import nl.dionsegijn.konfetti.core.models.Shape 5 | 6 | object ImageUtil { 7 | @JvmStatic 8 | fun loadDrawable( 9 | drawable: Drawable, 10 | tint: Boolean = true, 11 | applyAlpha: Boolean = true, 12 | ): Shape.DrawableShape { 13 | val width = drawable.intrinsicWidth 14 | val height = drawable.intrinsicHeight 15 | val drawableImage = DrawableImage(drawable, width, height) 16 | return Shape.DrawableShape(drawableImage, tint, applyAlpha) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /konfetti/xml/src/main/java/nl/dionsegijn/konfetti/xml/listeners/OnParticleSystemUpdateListener.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.konfetti.xml.listeners 2 | 3 | import nl.dionsegijn.konfetti.core.Party 4 | import nl.dionsegijn.konfetti.xml.KonfettiView 5 | 6 | /** 7 | * Created by dionsegijn on 5/31/17. 8 | */ 9 | interface OnParticleSystemUpdateListener { 10 | fun onParticleSystemStarted( 11 | view: KonfettiView, 12 | party: Party, 13 | activeSystems: Int, 14 | ) 15 | 16 | fun onParticleSystemEnded( 17 | view: KonfettiView, 18 | party: Party, 19 | activeSystems: Int, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /samples/compose-kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /samples/compose-kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("com.diffplug.spotless") 5 | } 6 | 7 | spotless { 8 | kotlin { 9 | ktlint("1.1.0") 10 | target("src/**/*.kt") 11 | } 12 | java { 13 | removeUnusedImports() 14 | googleJavaFormat("1.15.0") 15 | target("**/*.java") 16 | } 17 | } 18 | 19 | android { 20 | compileSdk = buildVersions.compileSdk 21 | 22 | defaultConfig { 23 | minSdk = 21 24 | targetSdk = buildVersions.targetSdk 25 | 26 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 27 | vectorDrawables { 28 | useSupportLibrary = true 29 | } 30 | } 31 | 32 | buildTypes { 33 | release { 34 | isMinifyEnabled = false 35 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 36 | } 37 | } 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_1_8 40 | targetCompatibility = JavaVersion.VERSION_1_8 41 | } 42 | kotlinOptions { 43 | jvmTarget = "1.8" 44 | } 45 | buildFeatures { 46 | compose = true 47 | } 48 | composeOptions { 49 | kotlinCompilerExtensionVersion = Constants.composeVersion 50 | } 51 | namespace = "nl.dionsegijn.xml.compose" 52 | } 53 | 54 | dependencies { 55 | val composeVersion: String = Constants.composeVersion 56 | 57 | implementation(project(path = ":konfetti:compose")) 58 | implementation(project(path = ":samples:shared")) 59 | 60 | implementation(libs.androidx.core.ktx) 61 | implementation(libs.androidx.appcomat) 62 | implementation(libs.android.material) 63 | implementation(libs.androidx.activity.compose) 64 | 65 | implementation(libs.androidx.lifecycle.runtime) 66 | implementation(libs.androidx.lifecycle.livedata.ktx) 67 | 68 | implementation(libs.compose.ui) 69 | implementation(libs.compose.ui.tooling) 70 | implementation(libs.compose.material) 71 | implementation(libs.compose.runtime) 72 | implementation(libs.compose.runtime.livedata) 73 | } 74 | -------------------------------------------------------------------------------- /samples/compose-kotlin/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.kts. 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 -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/java/nl/dionsegijn/xml/compose/ComposeActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.compose 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.viewModels 7 | import androidx.appcompat.content.res.AppCompatResources 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxHeight 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.material.Button 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Surface 16 | import androidx.compose.material.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.livedata.observeAsState 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import nl.dionsegijn.konfetti.compose.KonfettiView 24 | import nl.dionsegijn.konfetti.compose.OnParticleSystemUpdateListener 25 | import nl.dionsegijn.konfetti.core.PartySystem 26 | import nl.dionsegijn.konfetti.xml.image.ImageUtil 27 | import nl.dionsegijn.xml.compose.ui.theme.KonfettiTheme 28 | 29 | class ComposeActivity : ComponentActivity() { 30 | private val viewModel by viewModels() 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | 35 | setContent { 36 | KonfettiTheme { 37 | // A surface container using the 'background' color from the theme 38 | Surface(color = MaterialTheme.colors.background) { 39 | KonfettiUI(viewModel) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | @Composable 47 | fun KonfettiUI(viewModel: KonfettiViewModel = KonfettiViewModel()) { 48 | val state: KonfettiViewModel.State by viewModel.state.observeAsState( 49 | KonfettiViewModel.State.Idle, 50 | ) 51 | val drawable = AppCompatResources.getDrawable(LocalContext.current, R.drawable.ic_heart) 52 | when (val newState = state) { 53 | KonfettiViewModel.State.Idle -> { 54 | Column( 55 | modifier = 56 | Modifier 57 | .fillMaxWidth() 58 | .fillMaxHeight(), 59 | verticalArrangement = Arrangement.Center, 60 | horizontalAlignment = Alignment.CenterHorizontally, 61 | ) { 62 | Button(onClick = { viewModel.festive(ImageUtil.loadDrawable(drawable!!)) }) { 63 | Text( 64 | text = "Festive", 65 | ) 66 | } 67 | Button(onClick = { viewModel.explode() }) { 68 | Text( 69 | text = "Explode", 70 | ) 71 | } 72 | Button(onClick = { viewModel.parade() }) { 73 | Text( 74 | text = "Parade", 75 | ) 76 | } 77 | Button(onClick = { viewModel.rain() }) { 78 | Text( 79 | text = "Rain", 80 | ) 81 | } 82 | } 83 | } 84 | is KonfettiViewModel.State.Started -> 85 | KonfettiView( 86 | modifier = Modifier.fillMaxSize(), 87 | parties = newState.party, 88 | updateListener = 89 | object : OnParticleSystemUpdateListener { 90 | override fun onParticleSystemEnded( 91 | system: PartySystem, 92 | activeSystems: Int, 93 | ) { 94 | if (activeSystems == 0) viewModel.ended() 95 | } 96 | }, 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/java/nl/dionsegijn/xml/compose/KonfettiViewModel.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.compose 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import nl.dionsegijn.konfetti.core.Party 7 | import nl.dionsegijn.konfetti.core.models.Shape 8 | import nl.dionsegijn.samples.shared.Presets 9 | 10 | class KonfettiViewModel : ViewModel() { 11 | private val _state = MutableLiveData(State.Idle) 12 | val state: LiveData = _state 13 | 14 | fun festive(drawable: Shape.DrawableShape) { 15 | /** 16 | * See [Presets] for this configuration 17 | */ 18 | _state.value = State.Started(Presets.festive(drawable)) 19 | } 20 | 21 | fun explode() { 22 | /** 23 | * See [Presets] for this configuration 24 | */ 25 | _state.value = State.Started(Presets.explode()) 26 | } 27 | 28 | fun parade() { 29 | /** 30 | * See [Presets] for this configuration 31 | */ 32 | _state.value = State.Started(Presets.parade()) 33 | } 34 | 35 | fun rain() { 36 | /** 37 | * See [Presets] for this configuration 38 | */ 39 | _state.value = State.Started(Presets.rain()) 40 | } 41 | 42 | fun ended() { 43 | _state.value = State.Idle 44 | } 45 | 46 | sealed class State { 47 | class Started(val party: List) : State() 48 | 49 | object Idle : State() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/java/nl/dionsegijn/xml/compose/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.compose.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) 9 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/java/nl/dionsegijn/xml/compose/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.compose.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = 8 | Shapes( 9 | small = RoundedCornerShape(4.dp), 10 | medium = RoundedCornerShape(4.dp), 11 | large = RoundedCornerShape(0.dp), 12 | ) 13 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/java/nl/dionsegijn/xml/compose/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.compose.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = 10 | darkColors( 11 | primary = Purple200, 12 | primaryVariant = Purple700, 13 | secondary = Teal200, 14 | ) 15 | 16 | private val LightColorPalette = 17 | lightColors( 18 | primary = Purple500, 19 | primaryVariant = Purple700, 20 | secondary = Teal200, 21 | /* Other default colors to override 22 | background = Color.White, 23 | surface = Color.White, 24 | onPrimary = Color.White, 25 | onSecondary = Color.Black, 26 | onBackground = Color.Black, 27 | onSurface = Color.Black, 28 | */ 29 | ) 30 | 31 | @Composable 32 | fun KonfettiTheme( 33 | darkTheme: Boolean = isSystemInDarkTheme(), 34 | content: 35 | @Composable() 36 | () -> Unit, 37 | ) { 38 | val colors = 39 | if (darkTheme) { 40 | DarkColorPalette 41 | } else { 42 | LightColorPalette 43 | } 44 | 45 | MaterialTheme( 46 | colors = colors, 47 | typography = Typography, 48 | shapes = Shapes, 49 | content = content, 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/java/nl/dionsegijn/xml/compose/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.compose.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = 11 | Typography( 12 | body1 = 13 | TextStyle( 14 | fontFamily = FontFamily.Default, 15 | fontWeight = FontWeight.Normal, 16 | fontSize = 16.sp, 17 | ), 18 | /* Other default text styles to override 19 | button = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.W500, 22 | fontSize = 14.sp 23 | ), 24 | caption = TextStyle( 25 | fontFamily = FontFamily.Default, 26 | fontWeight = FontWeight.Normal, 27 | fontSize = 12.sp 28 | ) 29 | */ 30 | ) 31 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/drawable/ic_heart.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /samples/compose-kotlin/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 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanielMartinus/Konfetti/2c1401366c3dd12a51d89c22733ce6db54f69eaf/samples/compose-kotlin/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Konfetti sample Compose 3 | 4 | -------------------------------------------------------------------------------- /samples/compose-kotlin/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 | 16 | 17 | -------------------------------------------------------------------------------- /samples/xml-java/src/main/res/xml/backup_descriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/xml-kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /samples/xml-kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("com.diffplug.spotless") 5 | } 6 | 7 | spotless { 8 | kotlin { 9 | ktlint("1.1.0") 10 | target("src/**/*.kt") 11 | } 12 | java { 13 | removeUnusedImports() 14 | googleJavaFormat("1.15.0") 15 | target("**/*.java") 16 | } 17 | } 18 | 19 | android { 20 | compileSdk = buildVersions.compileSdk 21 | 22 | defaultConfig { 23 | minSdk = 23 24 | targetSdk = buildVersions.targetSdk 25 | 26 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 27 | } 28 | 29 | buildTypes { 30 | release { 31 | isMinifyEnabled = false 32 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 33 | } 34 | } 35 | compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_1_8 37 | targetCompatibility = JavaVersion.VERSION_1_8 38 | } 39 | kotlinOptions { 40 | jvmTarget = "1.8" 41 | } 42 | namespace = "nl.dionsegijn.xml.kotlin" 43 | } 44 | 45 | dependencies { 46 | implementation(project(path = ":konfetti:xml")) 47 | implementation(project(path = ":samples:shared")) 48 | implementation(libs.androidx.core.ktx) 49 | implementation(libs.androidx.appcomat) 50 | implementation(libs.android.material) 51 | implementation(libs.androidx.constraintlayout) 52 | 53 | debugImplementation(libs.androidx.tracing) 54 | androidTestImplementation(libs.androidx.test.espresso) 55 | androidTestImplementation(libs.test.junit.ext) 56 | } 57 | -------------------------------------------------------------------------------- /samples/xml-kotlin/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.kts. 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 -------------------------------------------------------------------------------- /samples/xml-kotlin/src/androidTest/java/nl/dionsegijn/xml/kotlin/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | import android.view.View 2 | import android.view.ViewGroup 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.action.ViewActions.click 5 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 6 | import androidx.test.espresso.matcher.ViewMatchers.withId 7 | import androidx.test.espresso.matcher.ViewMatchers.withText 8 | import androidx.test.ext.junit.rules.ActivityScenarioRule 9 | import androidx.test.ext.junit.runners.AndroidJUnit4 10 | import androidx.test.filters.LargeTest 11 | import nl.dionsegijn.xml.kotlin.MainActivity 12 | import nl.dionsegijn.xml.kotlin.R 13 | import org.hamcrest.Description 14 | import org.hamcrest.Matcher 15 | import org.hamcrest.Matchers.allOf 16 | import org.hamcrest.TypeSafeMatcher 17 | import org.junit.Rule 18 | import org.junit.Test 19 | import org.junit.runner.RunWith 20 | 21 | @LargeTest 22 | @RunWith(AndroidJUnit4::class) 23 | class MainActivityTest { 24 | @Rule 25 | @JvmField 26 | var mActivityScenarioRule = ActivityScenarioRule(MainActivity::class.java) 27 | 28 | private val testButtons = 29 | listOf( 30 | TestButton(R.id.btnFestive, "Festive"), 31 | TestButton(R.id.btnExplode, "Explode"), 32 | TestButton(R.id.btnParade, "Parade"), 33 | TestButton(R.id.btnRain, "Rain"), 34 | ) 35 | 36 | @Test 37 | fun mainActivityTest() { 38 | testButtons.forEachIndexed { pos, button -> 39 | runTestForButtonWithText(button.viewId, button.buttonText, pos + 1) 40 | } 41 | } 42 | 43 | private fun runTestForButtonWithText( 44 | viewId: Int, 45 | buttonText: String, 46 | position: Int, 47 | ) { 48 | val materialButton = 49 | onView( 50 | allOf( 51 | withId(viewId), 52 | withText(buttonText), 53 | childAtPosition( 54 | childAtPosition( 55 | withId(android.R.id.content), 56 | 0, 57 | ), 58 | position, 59 | ), 60 | isDisplayed(), 61 | ), 62 | ) 63 | materialButton.perform(click()) 64 | } 65 | 66 | private fun childAtPosition( 67 | parentMatcher: Matcher, 68 | position: Int, 69 | ): Matcher { 70 | return object : TypeSafeMatcher() { 71 | override fun describeTo(description: Description) { 72 | description.appendText("Child at position $position in parent ") 73 | parentMatcher.describeTo(description) 74 | } 75 | 76 | public override fun matchesSafely(view: View): Boolean { 77 | val parent = view.parent 78 | return parent is ViewGroup && parentMatcher.matches(parent) && 79 | view == parent.getChildAt(position) 80 | } 81 | } 82 | } 83 | 84 | data class TestButton(val viewId: Int, val buttonText: String) 85 | } 86 | -------------------------------------------------------------------------------- /samples/xml-kotlin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/xml-kotlin/src/main/java/nl/dionsegijn/xml/kotlin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package nl.dionsegijn.xml.kotlin 2 | 3 | import android.os.Bundle 4 | import android.widget.Button 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.appcompat.content.res.AppCompatResources 7 | import nl.dionsegijn.konfetti.xml.KonfettiView 8 | import nl.dionsegijn.konfetti.xml.image.ImageUtil 9 | import nl.dionsegijn.samples.shared.Presets 10 | 11 | class MainActivity : AppCompatActivity() { 12 | private lateinit var viewKonfetti: KonfettiView 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_main) 17 | 18 | viewKonfetti = findViewById(R.id.konfettiView) 19 | findViewById