├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml └── misc.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── exyte │ │ └── wave │ │ ├── MainActivity.kt │ │ ├── Utils.kt │ │ ├── animating │ │ └── AnimateState.kt │ │ ├── ui │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── waterdrops │ │ ├── Animation.kt │ │ ├── LevelState.kt │ │ ├── Path.kt │ │ ├── Points.kt │ │ ├── WaterDrops.kt │ │ ├── canvas │ │ └── DrawPaths.kt │ │ ├── plottedpoints │ │ ├── CreateParabolaAsState.kt │ │ └── Points.kt │ │ ├── text │ │ ├── CreateTextParamsAsState.kt │ │ └── TextParams.kt │ │ └── wave │ │ ├── AnimationAsState.kt │ │ └── WaveParams.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 │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.idea/ 17 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |       4 | 5 | 6 | 7 | 8 |

Android Waves Progress Bar

9 | A depth gauge progress bar, inspired by Apple Watch dive widget. 10 |
11 | Read Article » 12 |
13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.exyte.wave' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "com.exyte.wave" 12 | minSdk 23 13 | targetSdk 33 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 | signingConfig signingConfigs.debug 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | buildFeatures { 38 | compose true 39 | } 40 | composeOptions { 41 | kotlinCompilerExtensionVersion '1.4.2' 42 | } 43 | packagingOptions { 44 | resources { 45 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 46 | } 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation 'androidx.core:core-ktx:1.10.1' 52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 53 | implementation 'androidx.activity:activity-compose:1.7.1' 54 | implementation "androidx.compose.ui:ui:$compose_ui_version" 55 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" 56 | implementation 'androidx.compose.material:material:1.4.3' 57 | implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0" 58 | } -------------------------------------------------------------------------------- /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 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.Surface 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.platform.LocalConfiguration 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.unit.sp 19 | import androidx.core.view.WindowCompat 20 | import com.exyte.wave.animating.WaterLevelState 21 | import com.exyte.wave.ui.theme.WaveTheme 22 | import com.exyte.wave.waterdrops.WaterDropLayout 23 | import com.exyte.wave.waterdrops.wave.WaterDropText 24 | import com.exyte.wave.waterdrops.wave.WaveParams 25 | 26 | class MainActivity : ComponentActivity() { 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | WindowCompat.setDecorFitsSystemWindows(window, false) 30 | setContent { 31 | WaveTheme { 32 | Surface( 33 | modifier = Modifier.fillMaxSize(), 34 | ) { 35 | val screenWidth = LocalConfiguration.current.screenWidthDp 36 | val points = remember { screenWidth / waveGap } 37 | var waterLevelState by remember { mutableStateOf(WaterLevelState.StartReady) } 38 | WaterDropLayout( 39 | modifier = Modifier, 40 | waveDurationInMills = 10000L, 41 | waterLevelState = waterLevelState, 42 | onWavesClick = { 43 | waterLevelState = if (waterLevelState == WaterLevelState.Animating) { 44 | WaterLevelState.StartReady 45 | } else { 46 | WaterLevelState.Animating 47 | } 48 | } 49 | ) { 50 | WaterDropText( 51 | modifier = Modifier, 52 | align = Alignment.Center, 53 | textStyle = TextStyle( 54 | color = Color.Black, 55 | fontSize = 80.sp, 56 | fontWeight = FontWeight.Bold, 57 | ), 58 | waveParams = WaveParams( 59 | pointsQuantity = points, 60 | maxWaveHeight = 30f 61 | ) 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | const val waveGap = 30 -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.ui.geometry.Offset 5 | import androidx.compose.ui.unit.Density 6 | import androidx.compose.ui.unit.TextUnit 7 | import com.exyte.wave.waterdrops.ElementParams 8 | import kotlin.math.pow 9 | 10 | fun isAboveElement(waterLevel: Int, bufferY: Float, position: Offset) = 11 | waterLevel < position.y - bufferY 12 | 13 | fun atElementLevel( 14 | waterLevel: Int, 15 | buffer: Float, 16 | elementParams: ElementParams, 17 | ) = (waterLevel >= (elementParams.position.y - buffer)) && 18 | (waterLevel < (elementParams.position.y + elementParams.size.height * 0.33)) 19 | 20 | fun isWaterFalls( 21 | waterLevel: Int, 22 | elementParams: ElementParams, 23 | ) = waterLevel >= (elementParams.position.y + elementParams.size.height * 0.33) && 24 | waterLevel <= (elementParams.position.y + elementParams.size.height) 25 | 26 | @Stable 27 | data class PointF( 28 | var x: Float, 29 | var y: Float 30 | ) 31 | 32 | fun List.copy(): List = map { 33 | it.copy() 34 | }.toMutableList() 35 | 36 | @Stable 37 | class Parabola( 38 | point1: PointF, 39 | point2: PointF, 40 | point3: PointF 41 | ) { 42 | private val a: Float 43 | private val b: Float 44 | private val c: Float 45 | 46 | init { 47 | val denom = (point1.x - point2.x) * (point1.x - point3.x) * (point2.x - point3.x) 48 | a = 49 | (point3.x * (point2.y - point1.y) + point2.x * (point1.y - point3.y) + point1.x * (point3.y - point2.y)) / denom 50 | b = 51 | (point3.x.pow(2) * (point1.y - point2.y) + point2.x.pow(2) * (point3.y - point1.y) + point1.x.pow( 52 | 2 53 | ) * (point2.y - point3.y)) / denom 54 | c = 55 | (point2.x * point3.x * (point2.x - point3.x) * point1.y + point3.x * point1.x * (point3.x - point1.x) * point2.y + point1.x * point2.x * (point1.x - point2.x) * point3.y) / denom 56 | } 57 | 58 | fun calculate(x: Float): Float { 59 | return a * x.pow(2) + (b * x) + c 60 | } 61 | 62 | } 63 | 64 | fun lerpF(start: Float, stop: Float, fraction: Float): Float = 65 | (1 - fraction) * start + fraction * stop 66 | 67 | fun parabolaInterpolation(fraction: Float): Float { 68 | return ((-40) * (fraction - 0.5).pow(2) + 11).toFloat() 69 | } 70 | 71 | fun Int.toBoolean(): Boolean = this != 0 72 | 73 | @Stable 74 | fun TextUnit.toPx(density: Density): Float = with(density) { this@toPx.roundToPx().toFloat() } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/animating/AnimateState.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.animating 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.spring 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.State 10 | import androidx.compose.runtime.produceState 11 | import androidx.compose.runtime.remember 12 | 13 | enum class WaterLevelState { 14 | StartReady, 15 | Animating, 16 | } 17 | 18 | @Composable 19 | fun waveProgressAsState( 20 | timerState: WaterLevelState, 21 | timerDurationInMillis: Long 22 | ): State { 23 | val animatable = remember { Animatable(initialValue = 0f) } 24 | 25 | LaunchedEffect(timerState) { 26 | when (timerState) { 27 | WaterLevelState.StartReady -> { 28 | animatable.animateTo( 29 | targetValue = 0.0f, 30 | animationSpec = spring(stiffness = 100f) 31 | ) 32 | } 33 | 34 | WaterLevelState.Animating -> { 35 | animatable.animateTo( 36 | targetValue = 0.7f, 37 | animationSpec = tween( 38 | durationMillis = timerDurationInMillis.toInt(), 39 | easing = LinearEasing 40 | ) 41 | ) 42 | } 43 | } 44 | } 45 | 46 | return produceState(initialValue = animatable.value, key1 = animatable.value) { 47 | this.value = animatable.value 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.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 | val Water = Color(0xff58e2ff) 10 | val Blue = Color(0xFF3289a7) -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.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 = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.ui.theme 2 | 3 | import android.app.Activity 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.material.darkColors 7 | import androidx.compose.material.lightColors 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.SideEffect 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.platform.LocalView 12 | import androidx.core.view.WindowCompat 13 | import androidx.core.view.WindowInsetsCompat 14 | import androidx.core.view.WindowInsetsControllerCompat 15 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 16 | 17 | private val DarkColorPalette = darkColors( 18 | primary = Purple200, 19 | primaryVariant = Purple700, 20 | secondary = Teal200 21 | ) 22 | 23 | private val LightColorPalette = lightColors( 24 | primary = Purple500, 25 | primaryVariant = Purple700, 26 | secondary = Teal200 27 | 28 | ) 29 | 30 | @Composable 31 | fun WaveTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | 39 | val systemUiController = rememberSystemUiController() 40 | val view = LocalView.current 41 | if (!view.isInEditMode) { 42 | val currentWindow = (view.context as? Activity)?.window 43 | 44 | if (currentWindow != null) { 45 | SideEffect { 46 | 47 | systemUiController.setSystemBarsColor( 48 | color = Color.Transparent, 49 | darkIcons = false 50 | ) 51 | val windowInsetsController = WindowCompat.getInsetsController(currentWindow, view) 52 | windowInsetsController.systemBarsBehavior = 53 | WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 54 | windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) 55 | } 56 | } 57 | } 58 | MaterialTheme( 59 | colors = colors, 60 | typography = Typography, 61 | shapes = Shapes, 62 | content = content 63 | ) 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.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 | val Typography = Typography( 10 | body1 = TextStyle( 11 | fontFamily = FontFamily.Default, 12 | fontWeight = FontWeight.Normal, 13 | fontSize = 16.sp 14 | ) 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/Animation.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.unit.IntSize 6 | 7 | @Composable 8 | fun rememberDropWaterDuration( 9 | elementSize: IntSize, 10 | containerSize: IntSize, 11 | duration: Long, 12 | ): Int { 13 | return remember(elementSize, containerSize) { 14 | if ((elementSize.height == 0) || containerSize.height == 0) { 15 | 0 16 | } else { 17 | (((duration * elementSize.height * 0.66) / (containerSize.height))).toInt() 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/LevelState.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import com.exyte.wave.isAboveElement 8 | import com.exyte.wave.atElementLevel 9 | import com.exyte.wave.isWaterFalls 10 | 11 | @Composable 12 | fun createLevelAsState( 13 | waterLevelProvider: () -> Int, 14 | bufferY: Float, 15 | elementParams: ElementParams, 16 | ): MutableState { 17 | return remember(elementParams.position, waterLevelProvider()) { 18 | when { 19 | isAboveElement(waterLevelProvider(), bufferY, elementParams.position) -> { 20 | mutableStateOf(LevelState.PlainMoving) 21 | } 22 | 23 | atElementLevel( 24 | waterLevelProvider(), 25 | bufferY, 26 | elementParams 27 | ) -> { 28 | mutableStateOf(LevelState.FlowsAround) 29 | } 30 | 31 | isWaterFalls( 32 | waterLevelProvider(), 33 | elementParams 34 | ) -> { 35 | mutableStateOf(LevelState.WaveIsComing) 36 | } 37 | 38 | else -> { 39 | mutableStateOf(LevelState.WaveIsComing) 40 | } 41 | } 42 | } 43 | } 44 | 45 | 46 | sealed class LevelState { 47 | object PlainMoving : LevelState() 48 | object FlowsAround : LevelState() 49 | object WaveIsComing: LevelState() 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/Path.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.keyframes 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.State 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.geometry.Offset 11 | import androidx.compose.ui.graphics.Path 12 | import androidx.compose.ui.unit.IntSize 13 | import com.exyte.wave.PointF 14 | import com.exyte.wave.copy 15 | import com.exyte.wave.lerpF 16 | import com.exyte.wave.parabolaInterpolation 17 | import com.exyte.wave.toBoolean 18 | import com.exyte.wave.waterdrops.plottedpoints.createInitialMultipliersAsState 19 | import com.exyte.wave.waterdrops.plottedpoints.createParabolaAsState 20 | import com.exyte.wave.waterdrops.wave.WaveParams 21 | 22 | 23 | @Composable 24 | fun createPathsAsState( 25 | levelState: LevelState, 26 | containerSize: IntSize, 27 | waterLevelProvider: () -> Float, 28 | dropWaterDuration: Int, 29 | animations: MutableList>, 30 | waveParams: WaveParams, 31 | elementParams: ElementParams, 32 | ): Paths { 33 | val parabola = createParabolaAsState( 34 | position = elementParams.position, 35 | elementSize = elementParams.size, 36 | waterLevel = waterLevelProvider(), 37 | buffer = waveParams.bufferY, 38 | dropWaterDuration = dropWaterDuration, 39 | levelState = levelState 40 | ) 41 | 42 | val plottedPoints = createPlottedPointsAsState( 43 | waterLevel = waterLevelProvider(), 44 | containerSize = containerSize, 45 | levelState = levelState, 46 | position = elementParams.position, 47 | buffer = waveParams.bufferY, 48 | elementSize = elementParams.size, 49 | parabola = parabola.value, 50 | pointsQuantity = waveParams.pointsQuantity 51 | ) 52 | 53 | val initialMultipliers = 54 | createInitialMultipliersAsState(pointsQuantity = waveParams.pointsQuantity) 55 | val waveMultiplier = animateFloatAsState( 56 | targetValue = if (levelState == LevelState.WaveIsComing) 1f else 0f, 57 | animationSpec = keyframes { 58 | durationMillis = dropWaterDuration 59 | (0.7f).at((0.2f * dropWaterDuration).toInt()) 60 | (0.8f).at((0.4f * dropWaterDuration).toInt()) 61 | }, 62 | ) 63 | 64 | val paths by remember { 65 | mutableStateOf(Paths()) 66 | } 67 | 68 | createPaths( 69 | animations, 70 | initialMultipliers, 71 | waveParams.maxWaveHeight, 72 | levelState, 73 | waveParams.bufferX, 74 | parabolaInterpolation(waveMultiplier.value), 75 | containerSize, 76 | plottedPoints, 77 | paths, 78 | elementParams 79 | ) 80 | return paths 81 | } 82 | 83 | fun createPaths( 84 | animations: MutableList>, 85 | initialMultipliers: MutableList, 86 | maxHeight: Float, 87 | levelState: LevelState, 88 | bufferX: Float, 89 | waveMultiplier: Float = 1f, 90 | containerSize: IntSize, 91 | points: List, 92 | paths: Paths, 93 | elementParams: ElementParams, 94 | ): Paths { 95 | 96 | for (i in 0..1) { 97 | var wavePoints = points.copy() 98 | val divider = i % 2 99 | wavePoints = addWaves( 100 | points = wavePoints, 101 | animations = animations, 102 | initialMultipliers = initialMultipliers, 103 | maxHeight = maxHeight, 104 | pointsInversion = divider.toBoolean(), 105 | levelState = levelState, 106 | position = elementParams.position, 107 | elementSize = elementParams.size, 108 | waveMultiplier = if (divider == 0) waveMultiplier / 2 else waveMultiplier, 109 | bufferX = bufferX, 110 | ) 111 | paths.pathList[i].reset() 112 | paths.pathList[i] = createPath(containerSize, wavePoints, paths.pathList[i]) 113 | } 114 | return paths 115 | } 116 | 117 | fun createPath( 118 | containerSize: IntSize, 119 | wavePoints: List, 120 | path: Path 121 | ): Path { 122 | path.moveTo(0f, containerSize.height.toFloat()) 123 | wavePoints.forEach { 124 | path.lineTo(it.x, it.y) 125 | } 126 | path.lineTo(containerSize.width.toFloat(), containerSize.height.toFloat()) 127 | return path 128 | } 129 | 130 | fun addWaves( 131 | points: List, 132 | animations: MutableList>, 133 | initialMultipliers: MutableList, 134 | maxHeight: Float, 135 | pointsInversion: Boolean, 136 | levelState: LevelState, 137 | position: Offset, 138 | elementSize: IntSize, 139 | bufferX: Float, 140 | waveMultiplier: Float, 141 | ): List { 142 | val elementRangeX = (position.x - bufferX)..(position.x + elementSize.width + bufferX) 143 | points.forEachIndexed { index, pointF -> 144 | val newIndex = if (pointsInversion) { 145 | index % animations.size 146 | } else { 147 | (animations.size - index % animations.size) - 1 148 | } 149 | val initialMultipliersNewIndex = if (pointsInversion) { 150 | index 151 | } else { 152 | initialMultipliers.size - index - 1 153 | } 154 | var waveHeight = calculateWaveHeight( 155 | animations[newIndex].value, 156 | initialMultipliers[initialMultipliersNewIndex], 157 | maxHeight 158 | ) 159 | 160 | if (levelState is LevelState.WaveIsComing && pointF.x in elementRangeX) { 161 | waveHeight *= waveMultiplier 162 | } 163 | 164 | pointF.y = pointF.y - waveHeight 165 | } 166 | return points 167 | } 168 | 169 | private fun calculateWaveHeight( 170 | currentSize: Float, 171 | initialMultipliers: Float, 172 | maxHeight: Float 173 | ): Float { 174 | var waveHeightPercent = initialMultipliers + currentSize 175 | if (waveHeightPercent > 1.0f) { 176 | val diff = waveHeightPercent - 1.0f 177 | waveHeightPercent = 1.0f - diff 178 | } 179 | 180 | return lerpF(maxHeight, 0f, waveHeightPercent) 181 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/Points.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.derivedStateOf 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.geometry.Offset 7 | import androidx.compose.ui.unit.IntSize 8 | import com.exyte.wave.Parabola 9 | import com.exyte.wave.PointF 10 | 11 | @Composable 12 | fun createPlottedPointsAsState( 13 | waterLevel: Float, 14 | containerSize: IntSize, 15 | levelState: LevelState, 16 | position: Offset, 17 | buffer: Float, 18 | elementSize: IntSize, 19 | parabola: Parabola, 20 | pointsQuantity: Int 21 | ): List { 22 | 23 | val pointSpacing = remember(containerSize, pointsQuantity) { 24 | derivedStateOf { 25 | if (containerSize.width == 0) 26 | 0f 27 | else 28 | containerSize.width.toFloat() / pointsQuantity 29 | } 30 | } 31 | 32 | val spacing = pointSpacing.value.toInt() 33 | if (spacing == 0) { 34 | return mutableListOf() 35 | } 36 | 37 | val points = remember(spacing, containerSize) { 38 | derivedStateOf { 39 | (-spacing..containerSize.width + spacing step spacing).map { x -> 40 | PointF(x.toFloat(), waterLevel) 41 | } 42 | } 43 | } 44 | 45 | val plottedPoints = remember(waterLevel, levelState) { 46 | when (levelState) { 47 | is LevelState.FlowsAround -> { 48 | val point1 = PointF( 49 | position.x, 50 | position.y - buffer / 5 51 | ) 52 | val point2 = point1.copy(x = position.x + elementSize.width) 53 | val point3 = PointF( 54 | position.x + elementSize.width / 2, 55 | position.y - buffer 56 | ) 57 | val p = Parabola(point1, point2, point3) 58 | points.value.forEach { 59 | val pointAtParabola = p.calculate(it.x) 60 | if (pointAtParabola > waterLevel) { 61 | it.y = waterLevel 62 | } else { 63 | it.y = pointAtParabola 64 | } 65 | } 66 | } 67 | 68 | is LevelState.WaveIsComing -> { 69 | val centerPointValue = 70 | parabola.calculate(position.x + elementSize.width / 2) 71 | points.value.forEach { 72 | val pr = parabola.calculate(it.x) 73 | if (centerPointValue > waterLevel) { 74 | if (pr < waterLevel) { 75 | it.y = waterLevel 76 | } else { 77 | it.y = pr 78 | } 79 | } else { 80 | if (pr > waterLevel) { 81 | it.y = waterLevel 82 | } else { 83 | it.y = pr 84 | } 85 | } 86 | } 87 | } 88 | 89 | LevelState.PlainMoving -> { 90 | points.value.map { 91 | it.y = waterLevel 92 | } 93 | } 94 | } 95 | points 96 | } 97 | return plottedPoints.value 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/WaterDrops.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.Stable 11 | import androidx.compose.runtime.State 12 | import androidx.compose.runtime.derivedStateOf 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.saveable.rememberSaveable 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.drawWithContent 20 | import androidx.compose.ui.geometry.Offset 21 | import androidx.compose.ui.graphics.CompositingStrategy 22 | import androidx.compose.ui.graphics.Path 23 | import androidx.compose.ui.graphics.graphicsLayer 24 | import androidx.compose.ui.layout.onGloballyPositioned 25 | import androidx.compose.ui.layout.positionInParent 26 | import androidx.compose.ui.unit.IntSize 27 | import com.exyte.wave.animating.WaterLevelState 28 | import com.exyte.wave.animating.waveProgressAsState 29 | import com.exyte.wave.ui.theme.Water 30 | import com.exyte.wave.waterdrops.canvas.drawTextWithBlendMode 31 | import com.exyte.wave.waterdrops.canvas.drawWaves 32 | import com.exyte.wave.waterdrops.text.createTextParamsAsState 33 | import com.exyte.wave.waterdrops.wave.WaterDropText 34 | import com.exyte.wave.waterdrops.wave.WaveParams 35 | import com.exyte.wave.waterdrops.wave.createAnimationsAsState 36 | 37 | @Composable 38 | fun WaterDropLayout( 39 | modifier: Modifier = Modifier, 40 | waveDurationInMills: Long = 6000L, 41 | waterLevelState: WaterLevelState, 42 | onWavesClick: () -> Unit, 43 | content: () -> WaterDropText, 44 | ) { 45 | val waveParams = remember { content().waveParams } 46 | val animations = createAnimationsAsState(pointsQuantity = waveParams.pointsQuantity) 47 | WaterLevelDrawing( 48 | modifier = modifier, 49 | waveDurationInMills = waveDurationInMills, 50 | waveParams = waveParams, 51 | animations = animations, 52 | waterLevelState = waterLevelState, 53 | onWavesClick = onWavesClick, 54 | content = content, 55 | ) 56 | } 57 | 58 | @Composable 59 | fun WaterLevelDrawing( 60 | modifier: Modifier = Modifier, 61 | waveDurationInMills: Long, 62 | waveParams: WaveParams, 63 | animations: MutableList>, 64 | waterLevelState: WaterLevelState, 65 | onWavesClick: () -> Unit, 66 | content: () -> WaterDropText, 67 | ) { 68 | val waveDuration by rememberSaveable { mutableStateOf(waveDurationInMills) } 69 | val waveProgress by waveProgressAsState( 70 | timerState = waterLevelState, 71 | timerDurationInMillis = waveDuration 72 | ) 73 | WavesDrawing( 74 | modifier = modifier, 75 | waveDuration = waveDuration, 76 | animations = animations, 77 | waveProgress = waveProgress, 78 | waveParams = waveParams, 79 | onWavesClick = onWavesClick, 80 | content = content, 81 | ) 82 | } 83 | 84 | @Composable 85 | fun WavesDrawing( 86 | modifier: Modifier = Modifier, 87 | waveDuration: Long, 88 | waveParams: WaveParams, 89 | animations: MutableList>, 90 | waveProgress: Float, 91 | onWavesClick: () -> Unit, 92 | content: () -> WaterDropText, 93 | ) { 94 | val elementParams by remember { mutableStateOf(ElementParams()) } 95 | var containerSize by remember { mutableStateOf(IntSize(0, 0)) } 96 | 97 | val dropWaterDuration = rememberDropWaterDuration( 98 | elementSize = elementParams.size, 99 | containerSize = containerSize, 100 | duration = waveDuration 101 | ) 102 | 103 | val waterLevel by remember(waveProgress, containerSize.height) { 104 | derivedStateOf { 105 | (waveProgress * containerSize.height).toInt() 106 | } 107 | } 108 | 109 | val levelState = createLevelAsState( 110 | waterLevelProvider = { waterLevel }, 111 | bufferY = waveParams.bufferY, 112 | elementParams = elementParams 113 | ) 114 | 115 | val paths = createPathsAsState( 116 | containerSize = containerSize, 117 | elementParams = elementParams, 118 | levelState = levelState.value, 119 | waterLevelProvider = { waterLevel.toFloat() }, 120 | dropWaterDuration = dropWaterDuration, 121 | animations = animations, 122 | waveParams = waveParams 123 | ) 124 | 125 | val textParams = createTextParamsAsState( 126 | textStyle = content().textStyle, 127 | waveProgress = waveProgress, 128 | elementParams = elementParams 129 | ) 130 | 131 | Canvas( 132 | modifier = Modifier 133 | .background(Water) 134 | .fillMaxSize() 135 | ) { 136 | drawWaves(paths) 137 | } 138 | 139 | Box( 140 | modifier = modifier 141 | .clickable(onClick = onWavesClick) 142 | .onGloballyPositioned { containerSize = IntSize(it.size.width, it.size.height) } 143 | .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) 144 | .drawWithContent { 145 | drawTextWithBlendMode(mask = paths.pathList[0], textParams = textParams.value) 146 | } 147 | ) { 148 | Text( 149 | modifier = content().modifier 150 | .align(content().align) 151 | .onGloballyPositioned { 152 | elementParams.position = it.positionInParent() 153 | elementParams.size = it.size 154 | }, 155 | text = "46FT", 156 | style = content().textStyle 157 | ) 158 | } 159 | } 160 | 161 | @Stable 162 | data class ElementParams( 163 | var size: IntSize = IntSize.Zero, 164 | var position: Offset = Offset(0f, 0f), 165 | ) 166 | 167 | data class Paths( 168 | val pathList: MutableList = mutableListOf(Path(), Path()) 169 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/canvas/DrawPaths.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops.canvas 2 | 3 | import androidx.compose.ui.graphics.BlendMode 4 | import androidx.compose.ui.graphics.Paint 5 | import androidx.compose.ui.graphics.Path 6 | import androidx.compose.ui.graphics.PathEffect 7 | import androidx.compose.ui.graphics.drawscope.DrawScope 8 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 9 | import androidx.compose.ui.text.ExperimentalTextApi 10 | import androidx.compose.ui.text.drawText 11 | import com.exyte.wave.ui.theme.Blue 12 | import com.exyte.wave.ui.theme.Water 13 | import com.exyte.wave.waterdrops.Paths 14 | import com.exyte.wave.waterdrops.text.TextParams 15 | 16 | fun DrawScope.drawWaves( 17 | paths: Paths, 18 | ) { 19 | drawIntoCanvas { 20 | it.drawPath(paths.pathList[1], paint.apply { 21 | color = Blue 22 | }) 23 | it.drawPath(paths.pathList[0], paint.apply { 24 | color = androidx.compose.ui.graphics.Color.Black 25 | alpha = 0.9f 26 | }) 27 | } 28 | } 29 | 30 | @OptIn(ExperimentalTextApi::class) 31 | fun DrawScope.drawTextWithBlendMode( 32 | mask: Path, 33 | textParams: TextParams, 34 | ) { 35 | drawText( 36 | textMeasurer = textParams.textMeasurer, 37 | topLeft = textParams.textOffset, 38 | text = textParams.text, 39 | style = textParams.textStyle, 40 | ) 41 | drawText( 42 | textMeasurer = textParams.textMeasurer, 43 | topLeft = textParams.unitTextOffset, 44 | text = "FT", 45 | style = textParams.unitTextStyle, 46 | ) 47 | 48 | drawPath( 49 | path = mask, 50 | color = Water, 51 | blendMode = BlendMode.SrcIn 52 | ) 53 | } 54 | 55 | val paint = Paint().apply { 56 | this.color = androidx.compose.ui.graphics.Color.Blue 57 | pathEffect = PathEffect.cornerPathEffect(100f) 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/plottedpoints/CreateParabolaAsState.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops.plottedpoints 2 | 3 | import android.view.animation.OvershootInterpolator 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.State 8 | import androidx.compose.runtime.derivedStateOf 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.unit.IntSize 14 | import com.exyte.wave.Parabola 15 | import com.exyte.wave.PointF 16 | import com.exyte.wave.waterdrops.LevelState 17 | 18 | @Composable 19 | fun createParabolaAsState( 20 | position: Offset, 21 | elementSize: IntSize, 22 | waterLevel: Float, 23 | buffer: Float, 24 | levelState: LevelState, 25 | dropWaterDuration: Int, 26 | ): State { 27 | 28 | val parabolaHeightMultiplier = animateFloatAsState( 29 | targetValue = if (levelState == LevelState.WaveIsComing) 0f else -1f, 30 | animationSpec = tween( 31 | durationMillis = dropWaterDuration, 32 | easing = { OvershootInterpolator(6f).getInterpolation(it) } 33 | ), 34 | ) 35 | 36 | val point1 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) { 37 | mutableStateOf( 38 | PointF( 39 | position.x, 40 | waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value 41 | ) 42 | ) 43 | } 44 | 45 | val point2 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) { 46 | mutableStateOf( 47 | PointF( 48 | position.x + elementSize.width, 49 | waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value 50 | ) 51 | ) 52 | } 53 | 54 | val point3 by remember(position, elementSize, parabolaHeightMultiplier, waterLevel) { 55 | mutableStateOf( 56 | PointF( 57 | position.x + elementSize.width / 2, 58 | waterLevel + (elementSize.height / 3f + buffer) * parabolaHeightMultiplier.value 59 | ) 60 | ) 61 | } 62 | 63 | return remember(point1, point2, point3) { 64 | derivedStateOf { 65 | Parabola(point1, point2, point3) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/plottedpoints/Points.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops.plottedpoints 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import kotlin.random.Random 6 | 7 | @Composable 8 | fun createInitialMultipliersAsState(pointsQuantity: Int): MutableList { 9 | val random = remember { Random(System.currentTimeMillis()) } 10 | return remember { 11 | mutableListOf().apply { 12 | repeat(pointsQuantity + 4) { this += random.nextFloat() } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/text/CreateTextParamsAsState.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalTextApi::class) 2 | 3 | package com.exyte.wave.waterdrops.text 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.State 7 | import androidx.compose.runtime.derivedStateOf 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.produceState 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.geometry.Offset 12 | import androidx.compose.ui.platform.LocalDensity 13 | import androidx.compose.ui.text.AnnotatedString 14 | import androidx.compose.ui.text.ExperimentalTextApi 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.text.rememberTextMeasurer 17 | import com.exyte.wave.toPx 18 | import com.exyte.wave.waterdrops.ElementParams 19 | 20 | @Composable 21 | fun createTextParamsAsState( 22 | textStyle: TextStyle, 23 | waveProgress: Float, 24 | elementParams: ElementParams, 25 | ): State { 26 | val textMeasurer = rememberTextMeasurer(100) 27 | val unitTextStyle = remember(textStyle) { textStyle.copy(fontSize = textStyle.fontSize / 2) } 28 | 29 | val text by remember(waveProgress) { 30 | derivedStateOf { 31 | (waveProgress * 100).toInt().toString() 32 | } 33 | } 34 | 35 | val textProgressSize by remember(text) { 36 | derivedStateOf { 37 | textMeasurer.measure( 38 | text = AnnotatedString(text), 39 | style = textStyle, 40 | ).size 41 | } 42 | } 43 | 44 | val unitTextSize by remember(text) { 45 | derivedStateOf { 46 | textMeasurer.measure( 47 | text = AnnotatedString(text), 48 | style = unitTextStyle, 49 | ).size 50 | } 51 | } 52 | val textOffset = remember(elementParams.position, unitTextSize, textProgressSize) { 53 | derivedStateOf { 54 | Offset( 55 | elementParams.position.x + (elementParams.size.width - (unitTextSize.width + textProgressSize.width)) / 2, 56 | elementParams.position.y - 50f 57 | ) 58 | } 59 | } 60 | 61 | val density = LocalDensity.current 62 | val unitTextOffset by remember(textOffset) { 63 | derivedStateOf { 64 | Offset( 65 | textOffset.value.x + textProgressSize.width, 66 | textOffset.value.y + (textStyle.fontSize / 2).toPx(density) 67 | ) 68 | } 69 | } 70 | return produceState( 71 | initialValue = TextParams( 72 | textStyle = textStyle, 73 | unitTextStyle = unitTextStyle, 74 | textOffset = textOffset.value, 75 | unitTextOffset = unitTextOffset, 76 | text = text, 77 | textMeasurer = textMeasurer 78 | ), 79 | key1 = waveProgress, 80 | key2 = textOffset, 81 | key3 = textStyle, 82 | ) { 83 | this.value = TextParams( 84 | textStyle = textStyle, 85 | unitTextStyle = unitTextStyle, 86 | textOffset = textOffset.value, 87 | unitTextOffset = unitTextOffset, 88 | text = text, 89 | textMeasurer = textMeasurer 90 | ) 91 | } 92 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/text/TextParams.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalTextApi::class) 2 | 3 | package com.exyte.wave.waterdrops.text 4 | 5 | import androidx.compose.runtime.Stable 6 | import androidx.compose.ui.geometry.Offset 7 | import androidx.compose.ui.text.ExperimentalTextApi 8 | import androidx.compose.ui.text.TextMeasurer 9 | import androidx.compose.ui.text.TextStyle 10 | 11 | @Stable 12 | data class TextParams( 13 | val textStyle: TextStyle, 14 | val unitTextStyle: TextStyle, 15 | val textOffset: Offset, 16 | val unitTextOffset: Offset, 17 | val text: String, 18 | val textMeasurer: TextMeasurer, 19 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/wave/AnimationAsState.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops.wave 2 | 3 | import androidx.compose.animation.core.RepeatMode 4 | import androidx.compose.animation.core.animateFloat 5 | import androidx.compose.animation.core.infiniteRepeatable 6 | import androidx.compose.animation.core.rememberInfiniteTransition 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.State 10 | import androidx.compose.runtime.remember 11 | import kotlin.random.Random 12 | 13 | @Composable 14 | fun createAnimationsAsState( 15 | pointsQuantity: Int, 16 | ): MutableList> { 17 | val animations = remember { 18 | mutableListOf>() 19 | } 20 | val random = remember { Random(System.currentTimeMillis()) } 21 | val infiniteAnimation = rememberInfiniteTransition() 22 | 23 | if (animations.size == 0) { 24 | repeat(pointsQuantity / 2) { 25 | val durationMillis = random.nextInt(2000, 6000) 26 | animations += infiniteAnimation.animateFloat( 27 | initialValue = 0f, 28 | targetValue = 1f, 29 | animationSpec = infiniteRepeatable( 30 | animation = tween(durationMillis), 31 | repeatMode = RepeatMode.Reverse, 32 | ) 33 | ) 34 | } 35 | } else { 36 | repeat(pointsQuantity / 2) { 37 | val durationMillis = random.nextInt(2000, 6000) 38 | animations[it] = infiniteAnimation.animateFloat( 39 | initialValue = 0f, 40 | targetValue = 1f, 41 | animationSpec = infiniteRepeatable( 42 | animation = tween(durationMillis), 43 | repeatMode = RepeatMode.Reverse, 44 | ) 45 | ) 46 | } 47 | } 48 | return animations 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/exyte/wave/waterdrops/wave/WaveParams.kt: -------------------------------------------------------------------------------- 1 | package com.exyte.wave.waterdrops.wave 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.ui.Alignment 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.text.TextStyle 7 | 8 | 9 | @Stable 10 | data class WaterDropText( 11 | val modifier: Modifier = Modifier, 12 | val align: Alignment, 13 | val textStyle: TextStyle, 14 | val waveParams: WaveParams 15 | ) 16 | 17 | @Stable 18 | data class WaveParams( 19 | val pointsQuantity: Int = 10, 20 | val maxWaveHeight: Float = 20f, 21 | val bufferY: Float = 60f, 22 | val bufferX: Float = 50f, 23 | ) -------------------------------------------------------------------------------- /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 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /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 | Wave 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | buildscript { 4 | ext { 5 | compose_ui_version = '1.4.3' 6 | } 7 | } 8 | plugins { 9 | id 'com.android.application' version '7.4.0' apply false 10 | id 'com.android.library' version '7.4.0' apply false 11 | id 'org.jetbrains.kotlin.android' version '1.8.10' apply false 12 | } 13 | 14 | subprojects { 15 | tasks.withType(KotlinCompile).configureEach { 16 | kotlinOptions { 17 | if (project.findProperty("waveApp.enableComposeCompilerReports") == "true") { 18 | freeCompilerArgs += [ 19 | "-P", 20 | "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + 21 | project.buildDir.absolutePath + "/compose_metrics" 22 | ] 23 | freeCompilerArgs += [ 24 | "-P", 25 | "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + 26 | project.buildDir.absolutePath + "/compose_metrics" 27 | ] 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaults.buildfeatures.buildconfig=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exyte/android-waves-progressbar/a8b08417f22f1fd858348d0a7a45c24800399594/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Oct 25 13:39:08 ALMT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Wave" 16 | include ':app' 17 | --------------------------------------------------------------------------------