Use compose for keyboard.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2023-01-07 14:31:40 +01:00
parent 26a44dcc18
commit ae6210073d
6 changed files with 330 additions and 285 deletions

View File

@@ -5,7 +5,7 @@ import javax.inject.Inject
class Tichu @Inject constructor() {
fun calculateOtherScore(score: Int?): Int? {
fun calculateOtherScore(score: Int): Int? {
if (score == null) {
return null
}
@@ -21,10 +21,10 @@ class Tichu @Inject constructor() {
return 100 - (score % 100)
}
fun isValidRound(round: Round): Boolean {
if (round.scoreA == null || round.scoreB == null) {
fun isValidRound(scoreA: Int?, scoreB: Int?): Boolean {
if (scoreA == null || scoreB == null) {
return false
}
return (round.scoreA!!.isMultipleOf5()) && round.scoreB!!.isMultipleOf5() && (round.scoreA!! + round.scoreB!!).isMultipleOf100()
return (scoreA!!.isMultipleOf5()) && scoreB!!.isMultipleOf5() && (scoreA!! + scoreB!!).isMultipleOf100()
}
}

View File

@@ -42,43 +42,6 @@ class CounterFragment : FragmentBase<FragmentCounterBinding>(), MenuProvider {
this, viewLifecycleOwner, Lifecycle.State.RESUMED
)
keyboardViewModel.scoreA.observe(viewLifecycleOwner) { value ->
val tichu = Tichu()
val oldValue = currentRound.scoreA
currentRound.scoreA = value
if (ignoreNextUpdate) {
ignoreNextUpdate = false
} else {
if (currentRound.scoreA?.let { oldValue?.getAbsoluteDifference(it) } != 100) {
ignoreNextUpdate = true
currentRound.scoreB = tichu.calculateOtherScore(value)
keyboardViewModel.setScoreB(currentRound.scoreB)
}
keyboardViewModel.setSubmitButtonEnable(tichu.isValidRound(currentRound))
}
}
keyboardViewModel.scoreB.observe(viewLifecycleOwner) { value ->
val tichu = Tichu()
val oldValue = currentRound.scoreB
currentRound.scoreB = value
if (ignoreNextUpdate) {
ignoreNextUpdate = false
} else {
if (currentRound.scoreB?.let { oldValue?.getAbsoluteDifference(it) } != 100) {
ignoreNextUpdate = true
currentRound.scoreA = tichu.calculateOtherScore(value)
keyboardViewModel.setScoreA(currentRound.scoreA)
}
keyboardViewModel.setSubmitButtonEnable(tichu.isValidRound(currentRound))
}
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {

View File

@@ -1,17 +1,33 @@
package me.zobrist.tichucounter.ui.counter
import android.content.Context
import android.icu.number.FormattedNumber
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.zobrist.tichucounter.databinding.FragmentKeyboardBinding
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.ui.FragmentBase
import me.zobrist.tichucounter.ui.history.IHistoryFragmentViewModel
@AndroidEntryPoint
class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
@@ -19,226 +35,280 @@ class Keyboard : FragmentBase<FragmentKeyboardBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentKeyboardBinding
get() = FragmentKeyboardBinding::inflate
private var unhandledNegation: Boolean = false
private val viewModel: KeyboardViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
private val requester = FocusRequester()
private var isAFocused: Boolean = false
private var isBFocused: Boolean = false
binding.inputTeamA.setRawInputType(InputType.TYPE_NULL)
binding.inputTeamB.setRawInputType(InputType.TYPE_NULL)
binding.inputTeamA.requestFocus()
var enableSubmit = mutableStateOf(false)
disableSubmitButton()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewModel.enableSubmitButton.observe(viewLifecycleOwner) { enabled ->
if (enabled) enableSubmitButton() else disableSubmitButton()
}
viewModel.scoreA.observe(viewLifecycleOwner) { value ->
updateScore(binding.inputTeamA, value)
}
viewModel.scoreB.observe(viewLifecycleOwner) { value ->
updateScore(binding.inputTeamB, value)
}
setListeners()
}
private fun setListeners() {
binding.inputTeamA.setOnFocusChangeListener { _, b ->
if (b) {
hideKeyboard()
snapshotFlow { viewModel.scoreA.value }
.onEach {
enableSubmitOnValidRound(viewModel)
}
}
binding.inputTeamB.setOnFocusChangeListener { _, b ->
if (b) {
hideKeyboard()
snapshotFlow { viewModel.scoreB.value }
.onEach {
enableSubmitOnValidRound(viewModel)
}
}
binding.buttonAdd100.setOnClickListener {
var value = getActiveValue()
value = if (value != null) {
value + 100
} else {
100
}
setActiveValue(value)
}
binding.buttonSub100.setOnClickListener {
var value = getActiveValue()
value = if (value != null) {
value!! - 100
} else {
-100
}
setActiveValue(value)
}
binding.button0.setOnClickListener {
appendToFocusedScore(0)
}
binding.button1.setOnClickListener {
appendToFocusedScore(1)
}
binding.button2.setOnClickListener {
appendToFocusedScore(2)
}
binding.button3.setOnClickListener {
appendToFocusedScore(3)
}
binding.button4.setOnClickListener {
appendToFocusedScore(4)
}
binding.button5.setOnClickListener {
appendToFocusedScore(5)
}
binding.button6.setOnClickListener {
appendToFocusedScore(6)
}
binding.button7.setOnClickListener {
appendToFocusedScore(7)
}
binding.button8.setOnClickListener {
appendToFocusedScore(8)
}
binding.button9.setOnClickListener {
appendToFocusedScore(9)
}
binding.buttonInv.setOnClickListener {
var value = getActiveValue()
if (value == null) {
unhandledNegation = if (getActiveText() == "-") {
setActiveText("")
false
} else {
setActiveText("-")
true
}
} else {
value = value?.times(-1)
setActiveValue(value)
}
}
binding.buttonBack.setOnClickListener {
var value = getActiveValue()
if (value != null) {
value = try {
value.toString().dropLast(1).toInt()
} catch (e: Exception) {
null
return ComposeView(requireContext()).apply {
// Dispose of the Composition when the view's LifecycleOwner
// is destroyed
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme {
KeyboardView(viewModel)
}
}
setActiveValue(value)
}
binding.submit.setOnClickListener {
viewModel.submitButtonClicked()
}
}
private fun hideKeyboard() {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
private fun enableSubmitOnValidRound(viewModel: IKeyboardViewModel) {
enableSubmit.value = try {
val tichu = Tichu()
if(tichu.isValidRound(viewModel.scoreA.value.toInt(), viewModel.scoreB.value.toInt()))
{
true
}
false
} catch(_:Exception)
{
false
}
}
private fun giveFocusToAIfNone() {
if (!binding.inputTeamA.isFocused && !binding.inputTeamB.isFocused) {
binding.inputTeamA.requestFocus()
if(!isAFocused && !isBFocused)
{
requester.requestFocus()
}
}
private fun updateScore(field: EditText, score: Int?) {
if (score == null) {
field.setText("")
return
}
val text = try {
score.toString()
} catch (e: Exception) {
""
}
field.setText(text)
private fun appendToFocusedScore(toAppend: String, viewModel: IKeyboardViewModel) {
val value = getActiveValue(viewModel)
value.value += toAppend
updateOtherScore(viewModel)
}
private fun appendToFocusedScore(toAppend: Int) {
var value = getActiveValue()
private fun updateOtherScore(viewModel: IKeyboardViewModel) {
val value = getActiveValue(viewModel)
if (value != null) {
value = value.times(10)
value = value.plus(toAppend)
try {
val tichu = Tichu()
val myScore = value.value.toInt()
val hisScore = tichu.calculateOtherScore(myScore)
if(tichu.isValidRound(myScore, hisScore))
{
updateInactiveValue(hisScore?.toString() ?: "", viewModel)
} else
{
updateInactiveValue("", viewModel)
}
} catch(_: Exception) {
updateInactiveValue("", viewModel)
}
}
private fun negateActiveInput(viewModel: IKeyboardViewModel) {
val value = getActiveValue(viewModel)
if(value.value.contains("-"))
{
value.value = value.value.replace("-", "")
} else {
value = toAppend
if (unhandledNegation) {
value = value.times(-1)
unhandledNegation = false
value.value = "-" + value.value
}
updateOtherScore(viewModel)
}
private fun addToActiveInput(toAdd: Int, viewModel: IKeyboardViewModel) {
val value = getActiveValue(viewModel)
try {
val temp = value.value.toInt() + toAdd
value.value = temp.toString()
} catch (e: Exception) {
value.value = toAdd.toString()
}
}
private fun removeFromActiveInput(viewModel: IKeyboardViewModel) {
val value = getActiveValue(viewModel)
if(value.value != "") {
value.value = value.value.dropLast(1)
}
updateOtherScore(viewModel)
}
private fun getActiveValue(viewModel: IKeyboardViewModel): MutableState<String> {
giveFocusToAIfNone()
if (isBFocused) {
return viewModel.scoreB
}
return viewModel.scoreA
}
private fun updateInactiveValue(value: String, viewModel: IKeyboardViewModel){
giveFocusToAIfNone()
if (isBFocused) {
viewModel.scoreA.value = value
} else
{
viewModel.scoreB.value = value
}
try {
val tichu = Tichu()
enableSubmit.value = tichu.isValidRound(viewModel.scoreA.value.toInt(), viewModel.scoreB.value.toInt())
} catch(_: java.lang.NumberFormatException) {
enableSubmit.value = false
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Preview
@Composable
fun KeyboardView(viewModel: IKeyboardViewModel = DefaultViewModel()) {
val keyboardController = LocalSoftwareKeyboardController.current
Column {
Row {
CompositionLocalProvider(
LocalTextToolbar provides EmptyTextToolbar
) {
TextField(
value = viewModel.scoreA.value,
onValueChange = { },
placeholder = { Text("0") },
singleLine = true,
modifier = Modifier
.onFocusChanged {
keyboardController?.hide()
isAFocused = it.isFocused
}
.focusRequester(requester)
.weight(1f),
colors = TextFieldDefaults.textFieldColors(
cursorColor = Color.Transparent,
errorCursorColor = Color.Transparent
)
)
TextField(
value = viewModel.scoreB.value,
onValueChange = { },
placeholder = { Text("0") },
singleLine = true,
modifier = Modifier
.onFocusChanged {
keyboardController?.hide()
isBFocused = it.isFocused
}
.weight(1f),
colors = TextFieldDefaults.textFieldColors(
cursorColor = Color.Transparent,
errorCursorColor = Color.Transparent
)
)
}
}
Row {
Button(
onClick = { appendToFocusedScore("1", viewModel) },
modifier = Modifier.weight(1F)
) { Text("1") }
Button(
onClick = { appendToFocusedScore("2", viewModel ) },
modifier = Modifier.weight(1F)
) { Text("2") }
Button(
onClick = { appendToFocusedScore("3", viewModel) },
modifier = Modifier.weight(1F)
) { Text("3") }
Button(onClick = { addToActiveInput(100, viewModel) }, modifier = Modifier.weight(1F)) { Text("+100") }
}
Row {
Button(
onClick = { appendToFocusedScore("4", viewModel) },
modifier = Modifier.weight(1F)
) { Text("4") }
Button(
onClick = { appendToFocusedScore("5", viewModel) },
modifier = Modifier.weight(1F)
) { Text("5") }
Button(
onClick = { appendToFocusedScore("6", viewModel) },
modifier = Modifier.weight(1F)
) { Text("6") }
Button(onClick = { addToActiveInput(-100, viewModel) }, modifier = Modifier.weight(1F)) { Text("-100") }
}
Row {
Button(
onClick = { appendToFocusedScore("7", viewModel) },
modifier = Modifier.weight(1F)
) { Text("7") }
Button(
onClick = { appendToFocusedScore("8", viewModel) },
modifier = Modifier.weight(1F)
) { Text("8") }
Button(
onClick = { appendToFocusedScore("9", viewModel) },
modifier = Modifier.weight(1F)
) { Text("9") }
Button(onClick = { removeFromActiveInput(viewModel) }, modifier = Modifier.weight(1F)) { Text("DEL") }
}
Row {
Button(onClick = { negateActiveInput(viewModel) }, modifier = Modifier.weight(1F)) { Text("+/-") }
Button(
onClick = { appendToFocusedScore("0", viewModel) },
modifier = Modifier.weight(1F)
) { Text("0") }
Spacer(modifier = Modifier.weight(1F))
Button(onClick = { submit(viewModel) }, modifier = Modifier.weight(1F), enabled = enableSubmit.value) { Text("ENTER") }
}
}
setActiveValue(value)
}
private fun getActiveValue(): Int? {
giveFocusToAIfNone()
if (binding.inputTeamA.isFocused) {
return viewModel.scoreA.value
}
return viewModel.scoreB.value
private fun submit(viewModel: IKeyboardViewModel) {
viewModel.submitScore(viewModel.scoreA.value.toInt(), viewModel.scoreB.value.toInt())
viewModel.scoreA.value = ""
viewModel.scoreB.value = ""
enableSubmit.value = false
}
private fun setActiveValue(value: Int?) {
giveFocusToAIfNone()
if (binding.inputTeamA.isFocused) {
viewModel.setScoreA(value)
} else {
viewModel.setScoreB(value)
object EmptyTextToolbar : TextToolbar {
override val status: TextToolbarStatus = TextToolbarStatus.Hidden
override fun hide() {}
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?,
) {
}
}
private fun getActiveText(): String {
giveFocusToAIfNone()
if (binding.inputTeamA.isFocused) {
return binding.inputTeamA.text.toString()
}
return binding.inputTeamB.text.toString()
}
internal class DefaultViewModel : IKeyboardViewModel {
override var scoreA: MutableState<String> = mutableStateOf("")
override var scoreB: MutableState<String> = mutableStateOf("")
private fun setActiveText(value: String) {
giveFocusToAIfNone()
if (binding.inputTeamA.isFocused) {
binding.inputTeamA.setText(value)
} else {
binding.inputTeamB.setText(value)
override fun submitScore(scoreA: Int, scoreB: Int) {
}
}
private fun enableSubmitButton() {
binding.submit.imageAlpha = 255 // 0 being transparent and 255 being opaque
binding.submit.isEnabled = true
}
private fun disableSubmitButton() {
binding.submit.imageAlpha = 60 // 0 being transparent and 255 being opaque
binding.submit.isEnabled = false
}
}

View File

@@ -1,54 +1,37 @@
package me.zobrist.tichucounter.ui.counter
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject
interface IKeyboardViewModel {
var scoreA: MutableState<String>
var scoreB: MutableState<String>
fun submitScore(scoreA: Int, scoreB: Int)
}
@HiltViewModel
class KeyboardViewModel @Inject constructor(private val gameRepository: GameRepository) :
ViewModel() {
private val _scoreA: MutableLiveData<Int?> = MutableLiveData()
private val _scoreB: MutableLiveData<Int?> = MutableLiveData()
private val _enableSubmitButton: MutableLiveData<Boolean> = MutableLiveData()
ViewModel(), IKeyboardViewModel {
override var scoreA = mutableStateOf( "")
override var scoreB = mutableStateOf("")
val scoreA: LiveData<Int?>
get() {
return _scoreA
}
val scoreB: LiveData<Int?>
get() {
return _scoreB
}
val enableSubmitButton: LiveData<Boolean>
get() {
return _enableSubmitButton
}
fun setScoreA(score: Int?) {
_scoreA.value = score
}
fun setScoreB(score: Int?) {
_scoreB.value = score
}
fun setSubmitButtonEnable(enabled: Boolean) {
_enableSubmitButton.value = enabled
}
fun submitButtonClicked() {
override fun submitScore(scoreA: Int, scoreB: Int) {
viewModelScope.launch {
gameRepository.addRoundToActiveGame(scoreA.value!!, scoreB.value!!)
_scoreA.value = null
_scoreB.value = null
setSubmitButtonEnable(false)
gameRepository.addRoundToActiveGame(scoreA, scoreB)
}
}
}

View File

@@ -5,11 +5,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardElevation
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -82,23 +84,28 @@ class HistoryFragment : Fragment() {
Row() {
Text(
text = game.nameA,
style = MaterialTheme.typography.headlineSmall)
style = MaterialTheme.typography.headlineSmall
)
Text(
text = game.scoreA.toString(),
style = MaterialTheme.typography.headlineSmall)
style = MaterialTheme.typography.headlineSmall
)
}
Row() {
Text(
text = game.nameB,
style = MaterialTheme.typography.headlineSmall)
style = MaterialTheme.typography.headlineSmall
)
Text(
text = game.scoreB.toString(),
style = MaterialTheme.typography.headlineSmall)
style = MaterialTheme.typography.headlineSmall
)
}
Row() {
Text(
text = format.format(game.modified),
style = MaterialTheme.typography.labelSmall)
style = MaterialTheme.typography.labelSmall
)
}
}
}
@@ -109,8 +116,30 @@ class HistoryFragment : Fragment() {
override val gameAndHistory: State<List<GameAndScore>>
get() {
val tempData = mutableListOf<GameAndScore>()
tempData.add(GameAndScore(false, "TeamA1sdfdsf", "TeamB1", Date(), Date(), 1, 10, 50))
tempData.add(GameAndScore(true, "TeamA2", "TeamB2sdfsdf", Date(), Date(), 2, 20, 60))
tempData.add(
GameAndScore(
false,
"TeamA1sdfdsf",
"TeamB1",
Date(),
Date(),
1,
10,
50
)
)
tempData.add(
GameAndScore(
true,
"TeamA2",
"TeamB2sdfsdf",
Date(),
Date(),
2,
20,
60
)
)
tempData.add(GameAndScore(false, "TeamA3", "TeamB3", Date(), Date(), 3, 30, 70))
tempData.add(GameAndScore(false, "TeamA4", "TeamB4", Date(), Date(), 4, 40, 80))
tempData.add(GameAndScore(false, "TeamA5", "TeamB5", Date(), Date(), 5, 50, 90))

View File

@@ -37,26 +37,26 @@ class TichuUnitTest {
assertGeneratedRound(tichu, 400, 0)
//Good rounds trough Tichu
assertValidRound(tichu, Round(1, 0, 0))
assertValidRound(tichu, Round(1, -100, 0))
assertValidRound(tichu, 0, 0)
assertValidRound(tichu, -100, 0)
//Bad rounds
assertInvalidRound(tichu, Round(1, 5, 12))
assertInvalidRound(tichu, Round(1, 12, 5))
assertInvalidRound(tichu, Round(1, 5, 55))
assertInvalidRound(tichu, 5, 12)
assertInvalidRound(tichu, 12, 5)
assertInvalidRound(tichu, 5, 55)
}
private fun assertGeneratedRound(tichu: Tichu, scoreA: Int, expectedScoreB: Int) {
val round = Round(1, scoreA, tichu.calculateOtherScore(scoreA))
assertEquals(expectedScoreB, round.scoreB)
assertTrue(tichu.isValidRound(round))
val scoreB = tichu.calculateOtherScore(scoreA)
assertEquals(expectedScoreB, scoreB)
assertTrue(tichu.isValidRound(scoreA, scoreB!!))
}
private fun assertInvalidRound(tichu: Tichu, round: Round) {
assertFalse(tichu.isValidRound(round))
private fun assertInvalidRound(tichu: Tichu, scoreA: Int, scoreB: Int) {
assertFalse(tichu.isValidRound(scoreA, scoreB))
}
private fun assertValidRound(tichu: Tichu, round: Round) {
assertTrue(tichu.isValidRound(round))
private fun assertValidRound(tichu: Tichu, scoreA: Int, scoreB: Int) {
assertTrue(tichu.isValidRound(scoreA, scoreB))
}
}