From df492c92f2dc2d4bcddcba48e492ab3b365e6a98 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 17 Oct 2021 19:13:01 +0200 Subject: [PATCH] Implement battery charging animation in Compose --- .../launcher2/ui/component/NavBarEffects.kt | 163 ++++++++++++++++++ .../ui/screens/LauncherMainScreen.kt | 4 + 2 files changed, 167 insertions(+) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/component/NavBarEffects.kt diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/NavBarEffects.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/NavBarEffects.kt new file mode 100644 index 00000000..12269b0a --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/NavBarEffects.kt @@ -0,0 +1,163 @@ +package de.mm20.launcher2.ui.component + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.util.Log +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +@Composable +fun NavBarEffects( + modifier: Modifier +) { + val context = LocalContext.current + + var intensity by remember { mutableStateOf(0) } + val scope = rememberCoroutineScope() + + DisposableEffect(null) { + + suspend fun update(intent: Intent?, retryOnZeroCurrent: Boolean = false) { + if (intent == null) return + val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + val charging = + status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL + if (charging) { + val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager + val current = bm.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW) + if (current <= 0) { + intensity = 5 + //Workaround for delayed current updates + if (retryOnZeroCurrent) { + delay(1000) + update(intent) + } + return + } + intensity = (current / 100000f).roundToInt().takeIf { it > 0 } ?: 1 + } else { + intensity = 0 + } + } + + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + scope.launch { + update(intent, false) + } + } + } + + val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + + val intent = context.registerReceiver(receiver, intentFilter) + scope.launch { + update(intent, true) + } + + onDispose { + context.unregisterReceiver(receiver) + } + } + + var bubbles by remember { mutableStateOf(arrayOf()) } + + val dp = LocalDensity.current.density + + LaunchedEffect(intensity) { + bubbles = Array(intensity) { + FloatArray(6) { 0f } + } + if (intensity == 0) return@LaunchedEffect + while (isActive) { + val newBubbles = Array(intensity) { FloatArray(6) { 0f } } + withInfiniteAnimationFrameMillis {} + for (i in 0 until intensity) { + val bubble = newBubbles[i] + val oldBubble = bubbles[i] + if (oldBubble.lifetime <= 0f) { + bubble.posX = (Math.random() * 64 - 32).toFloat() * dp + bubble.posY = 0f + bubble.deltaX = (Math.random() - 0.5).toFloat() * 1f * dp + bubble.deltaY = Math.random().toFloat() * 3f * dp + bubble.radius = (Math.random() * 2 + 2).toFloat() * dp + bubble.lifetime = (Math.random() * 80 + 40).toInt().toFloat() + } else { + bubble.posX = oldBubble.deltaX + oldBubble.posX + bubble.posY = oldBubble.deltaY + oldBubble.posY + bubble.lifetime = oldBubble.lifetime - 1 + bubble.deltaX = oldBubble.deltaX + bubble.deltaY = oldBubble.deltaY + bubble.radius = oldBubble.radius + } + } + bubbles = newBubbles + } + } + + Canvas(modifier = modifier) { + for (bubble in bubbles) { + val x = size.width / 2 + bubble.posX + val y = size.height - bubble.posY + drawCircle( + color = Color.White, + radius = bubble.radius, + alpha = bubble.lifetime / 255, + center = Offset(x, y) + ) + } + } +} + +typealias Bubble = FloatArray + +inline var Bubble.posX: Float + get() = this[0] + set(value) { + this[0] = value + } + +inline var Bubble.posY: Float + get() = this[1] + set(value) { + this[1] = value + } + +inline var Bubble.deltaX: Float + get() = this[2] + set(value) { + this[2] = value + } + +inline var Bubble.deltaY: Float + get() = this[3] + set(value) { + this[3] = value + } + +inline var Bubble.radius: Float + get() = this[4] + set(value) { + this[4] = value + } + +inline var Bubble.lifetime: Float + get() = this[5] + set(value) { + this[5] = value + } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/screens/LauncherMainScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/screens/LauncherMainScreen.kt index 9f51c2d0..016b8bb0 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/screens/LauncherMainScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/screens/LauncherMainScreen.kt @@ -19,6 +19,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.systemuicontroller.rememberSystemUiController +import de.mm20.launcher2.ui.component.NavBarEffects import de.mm20.launcher2.ui.component.SearchBar import de.mm20.launcher2.ui.component.SearchColumn import de.mm20.launcher2.ui.component.WidgetColumn @@ -102,6 +103,9 @@ fun LauncherMainScreen() { } } } + NavBarEffects( + modifier = Modifier.fillMaxSize() + ) HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(),