From 58d4fc0e436364aced11e03e824d4bdc9f9b869b Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 27 Jan 2023 18:07:16 +0100 Subject: [PATCH] Show blinking cursor in keyboard view. Give focus back to last focused input. closes #12 --- .../me/zobrist/tichucounter/MainActivity.kt | 2 +- .../tichucounter/ui/counter/CounterView.kt | 39 ++--- .../ui/counter/CounterViewModel.kt | 92 ++++++++--- .../tichucounter/ui/counter/KeyboardView.kt | 156 ++++++++++++------ 4 files changed, 187 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 3d62e81..9b1b0e3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -269,7 +269,7 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { scope, navController, counterViewModel.keyboardHidden && (currentDestination?.hierarchy?.any { it.route == "counter" } == true) - ) { counterViewModel.keyboardHidden = false } + ) { counterViewModel.showKeyboard() } } } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index 04fc179..1113d76 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt @@ -51,20 +51,7 @@ fun Landscape(viewModel: ICounterViewModel) { } if (!viewModel.keyboardHidden) { Column(Modifier.weight(1f)) { - KeyboardView( - viewModel.currentScoreA, - viewModel.currentScoreB, - viewModel.requestFocusA, - viewModel.enableSubmit, - { viewModel.updateFocusStateA(it) }, - { viewModel.updateFocusStateB(it) }, - { viewModel.digitClicked(it) }, - { viewModel.addSub100Clicked(it) }, - { viewModel.deleteClicked() }, - { viewModel.negateClicked() }, - { viewModel.submitClicked() }, - { viewModel.keyboardHidden = true }, - { viewModel.swapInputScores() }) + KeyBoardView(viewModel = viewModel) } } } @@ -92,20 +79,7 @@ fun Portrait(viewModel: ICounterViewModel) { ) if (!viewModel.keyboardHidden) { - KeyboardView( - viewModel.currentScoreA, - viewModel.currentScoreB, - viewModel.requestFocusA, - viewModel.enableSubmit, - { viewModel.updateFocusStateA(it) }, - { viewModel.updateFocusStateB(it) }, - { viewModel.digitClicked(it) }, - { viewModel.addSub100Clicked(it) }, - { viewModel.deleteClicked() }, - { viewModel.negateClicked() }, - { viewModel.submitClicked() }, - { viewModel.keyboardHidden = true }, - { viewModel.swapInputScores() }) + KeyBoardView(viewModel = viewModel) } } } @@ -133,11 +107,12 @@ internal class PreviewViewModel : ICounterViewModel { override var isAFocused: Boolean = false override var isBFocused: Boolean = false override var requestFocusA: FocusRequester = FocusRequester() + override var requestFocusB: FocusRequester = FocusRequester() override var activeValue: String = currentScoreA override var inactiveValue: String = currentScoreB override var keyboardHidden: Boolean = false - override fun giveFocusToAIfNone() { + override fun focusLastInput() { } override fun updateOtherScore() { @@ -180,4 +155,10 @@ internal class PreviewViewModel : ICounterViewModel { override fun swapInputScores() { } + override fun hideKeyboard() { + } + + override fun showKeyboard() { + } + } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index c16a86d..cc8d3d1 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -14,23 +14,22 @@ import me.zobrist.tichucounter.domain.getTotalPoints import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject -interface ICounterViewModel { - var roundScoreList: List - var totalScoreA: Int - var totalScoreB: Int - var teamNameA: String - var teamNameB: String - var currentScoreA: String - var currentScoreB: String - var enableSubmit: Boolean - var isAFocused: Boolean - var isBFocused: Boolean - var requestFocusA: FocusRequester - var activeValue: String - var inactiveValue: String - var keyboardHidden: Boolean +private enum class Focused { TEAM_A, TEAM_B } - fun giveFocusToAIfNone() +interface IKeyBoardViewModel { + + val currentScoreA: String + val currentScoreB: String + val enableSubmit: Boolean + val isAFocused: Boolean + val isBFocused: Boolean + val requestFocusA: FocusRequester + val requestFocusB: FocusRequester + val activeValue: String + val inactiveValue: String + val keyboardHidden: Boolean + + fun focusLastInput() fun updateOtherScore() fun isValidTichuRound(): Boolean fun updateSubmitButton() @@ -39,11 +38,23 @@ interface ICounterViewModel { fun negateClicked() fun addSub100Clicked(toAdd: Int) fun deleteClicked() - fun updateNameA(value: String) - fun updateNameB(value: String) fun updateFocusStateA(state: Boolean) fun updateFocusStateB(state: Boolean) fun swapInputScores() + fun hideKeyboard() + fun showKeyboard() + +} + +interface ICounterViewModel: IKeyBoardViewModel { + val roundScoreList: List + val totalScoreA: Int + val totalScoreB: Int + val teamNameA: String + val teamNameB: String + + fun updateNameA(value: String) + fun updateNameB(value: String) } @HiltViewModel @@ -53,28 +64,43 @@ class CounterViewModel @Inject constructor( ViewModel(), ICounterViewModel { override var roundScoreList by mutableStateOf(emptyList()) + private set override var totalScoreA by mutableStateOf(0) + private set override var totalScoreB by mutableStateOf(0) + private set override var teamNameA by mutableStateOf("") + private set override var teamNameB by mutableStateOf("") + private set override var currentScoreA by mutableStateOf("") + private set override var currentScoreB by mutableStateOf("") + private set override var enableSubmit by mutableStateOf(false) + private set override var isAFocused by mutableStateOf(false) + private set override var isBFocused by mutableStateOf(false) + private set override var requestFocusA by mutableStateOf(FocusRequester()) + private set + + override var requestFocusB by mutableStateOf(FocusRequester()) + private set override var keyboardHidden by mutableStateOf(false) + private set override var activeValue: String get() { @@ -108,6 +134,9 @@ class CounterViewModel @Inject constructor( } } + + private var lastFocused = Focused.TEAM_A + init { viewModelScope.launch { gameRepository.getActiveGameFlow().collect { @@ -126,9 +155,10 @@ class CounterViewModel @Inject constructor( } } - override fun giveFocusToAIfNone() { - if (!isAFocused && !isBFocused) { - requestFocusA.requestFocus() + override fun focusLastInput() { + when(lastFocused){ + Focused.TEAM_A -> if(!isAFocused) requestFocusA.requestFocus() + Focused.TEAM_B -> if(!isBFocused) requestFocusB.requestFocus() } } @@ -170,7 +200,7 @@ class CounterViewModel @Inject constructor( } override fun digitClicked(digit: String) { - giveFocusToAIfNone() + focusLastInput() activeValue += digit updateOtherScore() @@ -178,7 +208,7 @@ class CounterViewModel @Inject constructor( } override fun negateClicked() { - giveFocusToAIfNone() + focusLastInput() activeValue = if (activeValue.contains("-")) { activeValue.replace("-", "") @@ -190,7 +220,7 @@ class CounterViewModel @Inject constructor( } override fun addSub100Clicked(toAdd: Int) { - giveFocusToAIfNone() + focusLastInput() activeValue = try { val temp = activeValue.toInt() + toAdd @@ -231,10 +261,16 @@ class CounterViewModel @Inject constructor( override fun updateFocusStateA(state: Boolean) { isAFocused = state + if(state){ + lastFocused = Focused.TEAM_A + } } override fun updateFocusStateB(state: Boolean) { isBFocused = state + if(state){ + lastFocused = Focused.TEAM_B + } } override fun swapInputScores() { @@ -242,4 +278,12 @@ class CounterViewModel @Inject constructor( currentScoreA = currentScoreB currentScoreB = swap } + + override fun hideKeyboard() { + keyboardHidden = true + } + + override fun showKeyboard() { + keyboardHidden = false + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt index a42b224..600c62d 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt @@ -1,6 +1,7 @@ package me.zobrist.tichucounter.ui.counter import android.content.res.Configuration +import androidx.compose.animation.core.* import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Backspace @@ -8,28 +9,51 @@ import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.KeyboardHide import androidx.compose.material.icons.outlined.SwapHoriz import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.FocusState import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.zobrist.tichucounter.ui.AppTheme +@Composable +fun KeyBoardView(viewModel: IKeyBoardViewModel) { + KeyboardView( + viewModel.currentScoreA, + viewModel.currentScoreB, + viewModel.requestFocusA, + viewModel.requestFocusB, + viewModel.enableSubmit, + viewModel.isAFocused, + viewModel.isBFocused, + { viewModel.updateFocusStateA(it) }, + { viewModel.updateFocusStateB(it) }, + { viewModel.digitClicked(it) }, + { viewModel.addSub100Clicked(it) }, + { viewModel.deleteClicked() }, + { viewModel.negateClicked() }, + { viewModel.submitClicked() }, + { viewModel.hideKeyboard() }, + { viewModel.swapInputScores() } + ) +} -@OptIn(ExperimentalComposeUiApi::class) @Composable fun KeyboardView( scoreA: String, scoreB: String, - requestFocus: FocusRequester, + requestFocusA: FocusRequester, + requestFocusB: FocusRequester, enableSubmit: Boolean, + focusStateA: Boolean, + focusStateB: Boolean, updateFocusStateA: (Boolean) -> Unit, updateFocusStateB: (Boolean) -> Unit, digitClicked: (String) -> Unit, @@ -40,22 +64,15 @@ fun KeyboardView( hideKeyboardClicked: () -> Unit, onSwapClicked: () -> Unit ) { - val keyboardController = LocalSoftwareKeyboardController.current - - Column { Row(Modifier.height(IntrinsicSize.Max)) { Column(Modifier.weight(1f)) { CenteredTextField( scoreA, "0", - Modifier - .focusRequester(requestFocus) - .onFocusChanged { - keyboardController?.hide() - updateFocusStateA(it.isFocused) - } - ) + focusStateA, + requestFocusA + ) { updateFocusStateA(it.isFocused) } } Surface( @@ -76,12 +93,11 @@ fun KeyboardView( CenteredTextField( scoreB, "0", - Modifier - .onFocusChanged { - keyboardController?.hide() - updateFocusStateB(it.isFocused) - } - ) + focusStateB, + requestFocusB + ) { + updateFocusStateB(it.isFocused) + } } } @@ -225,23 +241,64 @@ fun KeyboardIconButton(icon: ImageVector, enabled: Boolean = true, onClicked: () fun CenteredTextField( value: String, placeholder: String, - modifier: Modifier, + focused: Boolean, + focusRequester: FocusRequester? = null, + onFocusStateChanged: (FocusState) -> Unit ) { - TextField( - value = value, - onValueChange = { }, - placeholder = { - Text( - placeholder, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + + val modifier = if(focusRequester != null) { + Modifier.focusRequester(focusRequester) + } else + { + Modifier + } + + Box(contentAlignment = Alignment.Center) { + TextField( + value = value, + onValueChange = { }, + placeholder = { + if(!focused){ + Text( + placeholder, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + }, + textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), + singleLine = true, + readOnly = true, + modifier = modifier + .fillMaxWidth() + .onFocusChanged { + onFocusStateChanged(it) + } + ) + if(focused) + { + val cursorColor = MaterialTheme.colorScheme.onSurface + val infiniteTransition = rememberInfiniteTransition() + val alpha by infiniteTransition.animateFloat( + 0f, + cursorColor.alpha, + animationSpec = infiniteRepeatable( + animation = tween(500), + repeatMode = RepeatMode.Reverse + ) ) - }, - textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), - singleLine = true, - readOnly = true, - modifier = modifier.fillMaxWidth() - ) + Row { + + Text(text = value, color = cursorColor.copy(alpha = 0f)) + Divider( + modifier = Modifier + .padding(start = 3.dp, top = 15.dp, bottom = 15.dp) + .width(1.dp) + .fillMaxHeight(), + color = cursorColor.copy(alpha = alpha)) + } + } + } } @@ -252,19 +309,22 @@ fun KeyboardViewPreview() { AppTheme { Surface { KeyboardView( - "", - "350", + "1", + "3511", FocusRequester(), - false, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}) + FocusRequester(), + enableSubmit = false, + focusStateA = true, + focusStateB = false, + updateFocusStateA = {}, + updateFocusStateB = {}, + digitClicked = {}, + addSub100Clicked = {}, + deleteClicked = {}, + negateClicked = {}, + submitClicked = {}, + hideKeyboardClicked = {}, + onSwapClicked = {}) } } } \ No newline at end of file