From 69bee4ba8014a5846fc94608ba459c37e9a432f1 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Sat, 23 Nov 2024 21:15:17 +0200 Subject: [PATCH] feat: Improve text fields and FABs Improved the keyboard actions for text fields, making them more user-friendly. Started working on entry animations for FABs to provide a better user experience. Added some small code improvements to enhance overall code quality. --- app/build.gradle.kts | 4 +- .../ui/components/FloatingActionButton.kt | 75 ++++++++----------- .../ui/components/Preferences.kt | 14 ++-- .../ui/screens/cart/CartScreen.kt | 4 +- .../ui/screens/help/HelpScreen.kt | 6 +- .../ui/screens/help/HelpViewModel.kt | 15 +++- .../ui/screens/home/HomeViewModel.kt | 1 + .../screens/home/repository/HomeRepository.kt | 2 +- .../ui/viewmodel/BaseViewModel.kt | 16 ++-- .../utils/OpenSourceLicensesUtils.kt | 2 +- 10 files changed, 73 insertions(+), 66 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b0c2408..ff06d6b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "com.d4rk.cartcalculator" minSdk = 23 targetSdk = 35 - versionCode = 70 - versionName = "1.1.1" + versionCode = 73 + versionName = "1.1.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" resourceConfigurations += listOf( "en" , diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/FloatingActionButton.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/FloatingActionButton.kt index acb8148..f79932d 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/FloatingActionButton.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/FloatingActionButton.kt @@ -1,63 +1,52 @@ package com.d4rk.cartcalculator.ui.components +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import com.d4rk.cartcalculator.ui.components.animations.bounceClick @Composable fun AnimatedExtendedFloatingActionButton( - visible: Boolean = true, - onClick: () -> Unit, - icon: @Composable () -> Unit, - text: (@Composable () -> Unit)? = null, -) { - var isInitiallyVisible by rememberSaveable { mutableStateOf(false) } - - SideEffect { - if (!isInitiallyVisible) { - isInitiallyVisible = true - } - } - - val animationSpec = tween( - durationMillis = 300, delayMillis = 0, easing = FastOutSlowInEasing + visible : Boolean = true , + onClick : () -> Unit , + icon : @Composable () -> Unit , + text : (@Composable () -> Unit)? = null , + offsetX : Float = 50f , + offsetY : Float = 50f , + scale : Float = 0.8f , + animationSpec : AnimationSpec = tween( + durationMillis = 300 , easing = FastOutSlowInEasing ) - - val alpha by animateFloatAsState( - targetValue = if (visible) 1f else 0f, animationSpec = animationSpec, label = "Alpha" - ) - val offsetX by animateFloatAsState( - targetValue = if (visible) 0f else 50f, animationSpec = animationSpec, label = "OffsetX" +) { + val animatedOffsetX by animateFloatAsState( + targetValue = if (visible) 0f else offsetX , + animationSpec = animationSpec , + label = "OffsetX" ) - val offsetY by animateFloatAsState( - targetValue = if (visible) 0f else 50f, animationSpec = animationSpec, label = "OffsetY" + val animatedOffsetY by animateFloatAsState( + targetValue = if (visible) 0f else offsetY , + animationSpec = animationSpec , + label = "OffsetY" ) - val scale by animateFloatAsState( - targetValue = if (visible) 1f else 0.8f, animationSpec = animationSpec, label = "Scale" + val animatedScale by animateFloatAsState( + targetValue = if (visible) 1f else scale , animationSpec = animationSpec , label = "Scale" ) - ExtendedFloatingActionButton( - onClick = onClick, - icon = icon, - text = text ?: {}, - modifier = Modifier - .bounceClick() - .graphicsLayer { - this.alpha = alpha - this.translationX = offsetX - this.translationY = offsetY - this.scaleX = scale - this.scaleY = scale - } - ) + ExtendedFloatingActionButton(onClick = onClick , + icon = icon , + text = text ?: {} , + modifier = Modifier + .bounceClick() + .graphicsLayer { + scaleX = animatedScale + scaleY = animatedScale + translationX = animatedOffsetX + translationY = animatedOffsetY + }) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/Preferences.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/Preferences.kt index 67bc8b8..5fbc9ba 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/Preferences.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/components/Preferences.kt @@ -51,7 +51,7 @@ fun SwitchCardComposable( val view : View = LocalView.current Card(modifier = Modifier .fillMaxWidth() - .padding(24.dp) + .padding(all = 24.dp) .clip(RoundedCornerShape(28.dp)) .clickable { view.playSoundEffect(SoundEffectConstants.CLICK) @@ -60,7 +60,7 @@ fun SwitchCardComposable( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp) , + .padding(all = 16.dp) , horizontalArrangement = Arrangement.SpaceBetween , verticalAlignment = Alignment.CenterVertically ) { @@ -161,7 +161,7 @@ fun PreferenceItem( Icon(it , contentDescription = null) } Column( - modifier = Modifier.padding(16.dp) + modifier = Modifier.padding(all = 16.dp) ) { title?.let { Text( @@ -218,7 +218,7 @@ fun SwitchPreferenceItem( } Column( modifier = Modifier - .padding(16.dp) + .padding(all = 16.dp) .weight(1f) ) { Text(text = title , style = MaterialTheme.typography.titleLarge) @@ -229,7 +229,7 @@ fun SwitchPreferenceItem( Switch( checked = checked , onCheckedChange = { isChecked -> onCheckedChange(isChecked) - } , modifier = Modifier.padding(16.dp) + } , modifier = Modifier.padding(all = 16.dp) ) } } @@ -276,7 +276,7 @@ fun SwitchPreferenceItemWithDivider( } Column( modifier = Modifier - .padding(16.dp) + .padding(all = 16.dp) .weight(1f) ) { Text(text = title , style = MaterialTheme.typography.titleLarge) @@ -293,6 +293,6 @@ fun SwitchPreferenceItemWithDivider( Switch(checked = checked , onCheckedChange = { isChecked -> onCheckedChange(isChecked) onSwitchClick(isChecked) - } , modifier = Modifier.padding(16.dp)) + } , modifier = Modifier.padding(all = 16.dp)) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt index db09b41..c251d37 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/cart/CartScreen.kt @@ -209,7 +209,7 @@ fun CartScreen(activity : CartActivity , cartId : Int) { } AdBanner( - modifier = Modifier.padding(12.dp) , dataStore = dataStore + modifier = Modifier.padding(all = 12.dp) , dataStore = dataStore ) Card( @@ -364,7 +364,7 @@ fun CartItemComposable( Box( modifier = modifier .fillMaxWidth() - .padding(24.dp) + .padding(all = 24.dp) ) { Row( modifier = Modifier.fillMaxSize() , diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpScreen.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpScreen.kt index f8f820c..37108c1 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpScreen.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -65,6 +66,7 @@ fun HelpScreen(activity : HelpActivity , viewModel : HelpViewModel) { var showMenu : Boolean by remember { mutableStateOf(value = false) } val context : Context = LocalContext.current val view : View = LocalView.current + val isFabVisible by viewModel.isFabVisible.collectAsState() val showDialog : MutableState = remember { mutableStateOf(value = false) } val reviewInfo : ReviewInfo? = viewModel.reviewInfo.value @@ -171,7 +173,9 @@ fun HelpScreen(activity : HelpActivity , viewModel : HelpViewModel) { ) } , floatingActionButton = { - AnimatedExtendedFloatingActionButton(onClick = { + AnimatedExtendedFloatingActionButton( + visible = isFabVisible, + onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) viewModel.reviewInfo.value?.let { safeReviewInfo -> viewModel.launchReviewFlow( diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpViewModel.kt index b0c4667..7a9d08a 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/help/HelpViewModel.kt @@ -6,19 +6,32 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.d4rk.cartcalculator.ui.viewmodel.BaseViewModel import com.d4rk.cartcalculator.utils.IntentUtils import com.google.android.gms.tasks.Task import com.google.android.play.core.review.ReviewInfo import com.google.android.play.core.review.ReviewManager import com.google.android.play.core.review.ReviewManagerFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -class HelpViewModel(application: Application) : AndroidViewModel(application) { +class HelpViewModel(application: Application) : BaseViewModel(application) { private var _reviewInfo: MutableState = mutableStateOf(value = null) val reviewInfo: State = _reviewInfo + init { + initializeVisibilityStates() + } + + private fun initializeVisibilityStates() { + viewModelScope.launch(coroutineExceptionHandler) { + delay(timeMillis = 50L) + showFab() + } + } + fun requestReviewFlow() { viewModelScope.launch(Dispatchers.IO) { val reviewManager: ReviewManager = ReviewManagerFactory.create(getApplication()) diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt index 622c6ac..7012414 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/HomeViewModel.kt @@ -45,6 +45,7 @@ class HomeViewModel(application : Application) : BaseViewModel(application) { lessonIndex == index || _visibilityStates.value[lessonIndex] } } + delay(timeMillis = 50L) showFab() } } diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/repository/HomeRepository.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/repository/HomeRepository.kt index b692ac8..1752a33 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/repository/HomeRepository.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/screens/home/repository/HomeRepository.kt @@ -26,7 +26,7 @@ class HomeRepository(application : Application) : } } - suspend fun addCartRepository(cart : ShoppingCartTable , onSuccess : (ShoppingCartTable) -> Unit , ) { + suspend fun addCartRepository(cart : ShoppingCartTable , onSuccess : (ShoppingCartTable) -> Unit) { withContext(Dispatchers.IO) { val addedCart = addCartImplementation(cart = cart) withContext(Dispatchers.Main) { diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt index 279102a..2408818 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/ui/viewmodel/BaseViewModel.kt @@ -24,7 +24,7 @@ open class BaseViewModel(application : Application) : AndroidViewModel(applicati private val _uiErrorModel = MutableStateFlow(UiErrorModel()) val uiErrorModel : StateFlow = _uiErrorModel.asStateFlow() - protected val coroutineExceptionHandler = CoroutineExceptionHandler { _ , exception -> + protected val coroutineExceptionHandler = CoroutineExceptionHandler { _ , exception : Throwable -> Log.e("BaseViewModel" , "Coroutine Exception: " , exception) handleError(exception) } @@ -32,15 +32,9 @@ open class BaseViewModel(application : Application) : AndroidViewModel(applicati val _visibilityStates = MutableStateFlow>(emptyList()) val visibilityStates : StateFlow> = _visibilityStates.asStateFlow() - private val _isFabVisible = MutableStateFlow(false) + private val _isFabVisible = MutableStateFlow(value= false) val isFabVisible : StateFlow = _isFabVisible.asStateFlow() - protected fun showFab() { - viewModelScope.launch(coroutineExceptionHandler) { - _isFabVisible.value = true - } - } - private fun handleError(exception : Throwable) { viewModelScope.launch(coroutineExceptionHandler) { val errorType : ErrorType = when (exception) { @@ -83,4 +77,10 @@ open class BaseViewModel(application : Application) : AndroidViewModel(applicati _isLoading.value = false } } + + protected fun showFab() { + viewModelScope.launch(coroutineExceptionHandler) { + _isFabVisible.value = true + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/d4rk/cartcalculator/utils/OpenSourceLicensesUtils.kt b/app/src/main/kotlin/com/d4rk/cartcalculator/utils/OpenSourceLicensesUtils.kt index 2e47f63..d8bf855 100644 --- a/app/src/main/kotlin/com/d4rk/cartcalculator/utils/OpenSourceLicensesUtils.kt +++ b/app/src/main/kotlin/com/d4rk/cartcalculator/utils/OpenSourceLicensesUtils.kt @@ -19,7 +19,7 @@ import java.net.HttpURLConnection import java.net.URL object OpenSourceLicensesUtils { - val packageName = BuildConfig.APPLICATION_ID + const val packageName = BuildConfig.APPLICATION_ID private suspend fun getChangelogMarkdown(): String { return withContext(Dispatchers.IO) {