diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/effects/Shake.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/effects/Shake.kt new file mode 100644 index 000000000..ac89da34c --- /dev/null +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/compose/effects/Shake.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.pitchedapps.frost.compose.effects + +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.platform.inspectable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch + +/** + * State for tracking shaking animation + * + * Note: Used some other material states as reference. This however will recompose the function + * holding the state during shakes. + */ +@Composable +fun rememberShakeState(): ShakeState { + val scope = rememberCoroutineScope() + val state = remember(scope) { ShakeState(scope) } + + return state +} + +class ShakeState +internal constructor( + private val animationScope: CoroutineScope, +) { + + private val job = SupervisorJob() + + private var _rotation by mutableFloatStateOf(0f) + + internal val rotation + get() = _rotation + + fun shake() { + job.cancelChildren() + animationScope.launch(job) { + animate( + _rotation, + 0f, + initialVelocity = 200f, + animationSpec = + spring( + dampingRatio = 0.3f, + stiffness = 200f, + ), + ) { value, _ -> + _rotation = value + } + } + } +} + +fun Modifier.shake(state: ShakeState, enabled: Boolean = true) = + inspectable( + inspectorInfo = + debugInspectorInfo { + name = "shake" + properties["enabled"] = enabled + }, + ) { + Modifier.rotate(state.rotation) + } diff --git a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt index 0985bec6a..8a10d6394 100644 --- a/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt +++ b/app-compose/src/main/kotlin/com/pitchedapps/frost/tabselector/TabSelectorScreen.kt @@ -16,9 +16,12 @@ */ package com.pitchedapps.frost.tabselector +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -37,6 +40,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.pitchedapps.frost.compose.effects.rememberShakeState +import com.pitchedapps.frost.compose.effects.shake import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.tab @@ -63,6 +68,7 @@ fun TabSelectorScreen(modifier: Modifier = Modifier) { ) } +@OptIn(ExperimentalFoundationApi::class) @Composable fun TabSelector( modifier: Modifier, @@ -74,7 +80,17 @@ fun TabSelector( modifier = modifier, columns = GridCells.Fixed(4), ) { - items(unselected, key = { it.key }) { TabItem(data = it) } + items(unselected, key = { it.key }) { + val shakeState = rememberShakeState() + TabItem( + modifier = + Modifier.animateItemPlacement().shake(shakeState).clickable { + shakeState.shake() + // onSelect(listOf(it)) + }, + data = it, + ) + } } } @@ -88,7 +104,7 @@ fun TabItem( horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( - modifier = Modifier.padding(4.dp), + modifier = Modifier.padding(4.dp).size(24.dp), imageVector = data.icon, contentDescription = data.title, )