├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── myapplication │ │ ├── MainActivity.kt │ │ └── Particles.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── 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 ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.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 | # IntelliJ 36 | *.iml 37 | .idea 38 | 39 | # Keystore files 40 | # Uncomment the following line if you do not want to check your keystore files in. 41 | *.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # Freeline 47 | freeline.py 48 | freeline/ 49 | freeline_project_description.json 50 | 51 | # fastlane 52 | fastlane/report.xml 53 | fastlane/Preview.html 54 | fastlane/screenshots 55 | fastlane/test_output 56 | fastlane/readme.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![featured at androidweekly](https://androidweekly.net/issues/issue-474/badge) 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | applicationId "com.example.myapplication" 12 | minSdk 21 13 | targetSdk 31 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | useIR = true 36 | } 37 | buildFeatures { 38 | compose true 39 | } 40 | composeOptions { 41 | kotlinCompilerExtensionVersion compose_version 42 | kotlinCompilerVersion '1.6.10' 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | implementation 'androidx.core:core-ktx:1.7.0' 49 | implementation 'androidx.appcompat:appcompat:1.4.1' 50 | implementation 'com.google.android.material:material:1.5.0' 51 | implementation "androidx.compose.ui:ui:$compose_version" 52 | implementation "androidx.compose.material:material:$compose_version" 53 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' 55 | implementation 'androidx.activity:activity-compose:1.6.0-alpha01' 56 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.animation.animateContentSize 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.material.Button 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.unit.dp 17 | 18 | class MainActivity : ComponentActivity() { 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContent { 22 | Box( 23 | modifier = Modifier.fillMaxSize(), 24 | contentAlignment = Alignment.BottomCenter 25 | ) { 26 | var showParticles by remember { mutableStateOf(false) } 27 | 28 | Column( 29 | horizontalAlignment = Alignment.CenterHorizontally 30 | ) { 31 | Particles( 32 | modifier = Modifier 33 | .fillMaxWidth() 34 | .height(400.dp), 35 | quantity = 22, 36 | emoji = "\uD83D\uDD25", 37 | visible = showParticles 38 | ) 39 | 40 | Spacer(modifier = Modifier.size(100.dp)) 41 | 42 | Button( 43 | modifier = Modifier.animateContentSize(), 44 | onClick = { showParticles = !showParticles }) { 45 | Text(text = if (!showParticles) "start" else "reset") 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/Particles.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.ExperimentalAnimationApi 6 | import androidx.compose.animation.core.* 7 | import androidx.compose.animation.slideInVertically 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.wrapContentSize 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.alpha 16 | import androidx.compose.ui.draw.scale 17 | import androidx.compose.ui.layout.Layout 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import kotlin.random.Random 21 | 22 | @OptIn(ExperimentalAnimationApi::class) 23 | @Composable 24 | fun Particles( 25 | modifier: Modifier, 26 | quantity: Int, 27 | emoji: String, 28 | visible: Boolean 29 | ) { 30 | AnimatedVisibility( 31 | visible = visible, 32 | enter = slideInVertically( 33 | initialOffsetY = { it }, 34 | animationSpec = tween( 35 | durationMillis = MAX_ANIMATION_DURATION.toInt() - 300, 36 | easing = LinearEasing, 37 | delayMillis = 300 38 | ) 39 | ), 40 | exit = ExitTransition.None 41 | ) { 42 | val particles = remember { calculateParticleParams(quantity, emoji) } 43 | val transitionState = remember { 44 | MutableTransitionState(MIN_HEIGHT).apply { 45 | targetState = MAX_HEIGHT 46 | } 47 | } 48 | val transition = updateTransition(transitionState, label = "height transition") 49 | val height by transition.animateInt( 50 | transitionSpec = { 51 | tween( 52 | durationMillis = MAX_ANIMATION_DURATION.toInt(), 53 | easing = LinearOutSlowInEasing 54 | ) 55 | }, 56 | label = "height animation of particles" 57 | ) { it } 58 | 59 | Layout( 60 | modifier = modifier.padding(bottom = 50.dp), 61 | content = { 62 | for (i in 0 until quantity) { 63 | Particle(particles[i]) 64 | } 65 | } 66 | ) { measurables, constraints -> 67 | val placeables = measurables.map { it.measure(constraints) } 68 | layout(constraints.maxWidth, height) { 69 | placeables.forEachIndexed { index, placeable -> 70 | val params = particles[index] 71 | placeable.placeRelative( 72 | x = (params.horizontalFraction * constraints.maxWidth).toInt() - constraints.maxWidth / 2, 73 | y = (params.verticalFraction * height).toInt() - height / 2 74 | ) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | private fun calculateParticleParams(quantity: Int, emoji: String): List { 82 | val random = Random(System.currentTimeMillis().toInt()) 83 | val result = mutableListOf() 84 | for (i in 0 until quantity) { 85 | val verticalFraction = random.nextDouble(from = 0.0, until = 1.0).toFloat() 86 | val horizontalFraction = random.nextDouble(from = 0.0, until = 1.0).toFloat() 87 | 88 | val model = 89 | ParticleModel( 90 | verticalFraction = verticalFraction, 91 | horizontalFraction = horizontalFraction, 92 | initialScale = lerp(MIN_PARTICLE_SIZE, MAX_PARTICLE_SIZE, verticalFraction), 93 | duration = lerp( 94 | MIN_ANIMATION_DURATION, 95 | MAX_ANIMATION_DURATION, 96 | verticalFraction 97 | ).toInt(), 98 | emoji = emoji 99 | ) 100 | result.add( 101 | model 102 | ) 103 | } 104 | 105 | return result 106 | 107 | } 108 | 109 | private fun lerp(start: Float, stop: Float, fraction: Float) = 110 | (start * (1 - fraction) + stop * fraction) 111 | 112 | @Composable 113 | private fun Particle(model: ParticleModel) { 114 | val transitionState = remember { 115 | MutableTransitionState(0.1f).apply { 116 | targetState = 0f 117 | } 118 | } 119 | 120 | val targetScale = remember { model.initialScale * TARGET_PARTICLE_SCALE_MULTIPLIER } 121 | 122 | val transition = updateTransition(transitionState, label = "particle transition") 123 | 124 | val alpha by transition.animateFloat( 125 | transitionSpec = { 126 | keyframes { 127 | durationMillis = model.duration 128 | 0.1f at START_OF_ANIMATION 129 | 1f at (model.duration * 0.1f).toInt() 130 | 1f at (model.duration * 0.8f).toInt() 131 | 0f at model.duration 132 | } 133 | }, 134 | label = "alpha animation of particle" 135 | ) { it } 136 | 137 | val scale by transition.animateFloat( 138 | transitionSpec = { 139 | keyframes { 140 | durationMillis = model.duration 141 | model.initialScale at START_OF_ANIMATION 142 | model.initialScale at (model.duration * 0.7f).toInt() 143 | targetScale at model.duration 144 | } 145 | }, 146 | label = "scale animation of particle" 147 | ) { it } 148 | 149 | Text( 150 | modifier = Modifier 151 | .wrapContentSize() 152 | .scale(scale) 153 | .alpha(alpha), 154 | text = model.emoji, 155 | fontSize = PARTICLE_TEXT_SIZE.sp 156 | ) 157 | } 158 | 159 | private const val TARGET_PARTICLE_SCALE_MULTIPLIER = 1.3f 160 | private const val START_OF_ANIMATION = 0 161 | private const val MIN_PARTICLE_SIZE = 1f 162 | private const val MAX_PARTICLE_SIZE = 2.6f 163 | private const val MIN_ANIMATION_DURATION = 1200f 164 | private const val MAX_ANIMATION_DURATION = 1500f 165 | private const val PARTICLE_TEXT_SIZE = 14 166 | private const val MIN_HEIGHT = 300 167 | private const val MAX_HEIGHT = 800 168 | 169 | data class ParticleModel( 170 | val verticalFraction: Float, 171 | val horizontalFraction: Float, 172 | val initialScale: Float, 173 | val duration: Int, 174 | val emoji: String 175 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewbel31/InstagramLikeParticlesAnimationCompose/88808ad86f11d65256181efa8f0cfa06e4c45982/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |