Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.goalpanzi.mission_mate.core.ui.component

import android.view.KeyEvent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
Expand All @@ -23,7 +24,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -59,15 +63,33 @@ fun InvitationCodeTextField(
contentPadding: PaddingValues = PaddingValues(),
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
readOnly : Boolean = false
readOnly: Boolean = false,
onDeleteWhenBlank: () -> Unit = {},
) {
val textFieldValue by remember(text) {
mutableStateOf(
TextFieldValue(
text = text,
selection = TextRange(
index = if(text.isNotBlank()) 1 else 0
)
)
)
}
var isFocused by remember { mutableStateOf(false) }
BasicTextField(
modifier = modifier
.onFocusChanged {
isFocused = it.isFocused
}
.onKeyEvent { event ->
val isDeleteButton = event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL
if (isDeleteButton && textFieldValue.text.isBlank()) {
onDeleteWhenBlank()
}
false
},
value = text,
value = textFieldValue,
singleLine = isSingleLine,
textStyle = textStyle.copy(
color = textColor,
Expand All @@ -77,38 +99,122 @@ fun InvitationCodeTextField(
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
onValueChange = onValueChange,
onValueChange = {
if (it.text.isBlank() || it.selection != TextRange.Zero) onValueChange(it.text)
},
readOnly = readOnly,
decorationBox = { innerTextField ->
Box(
modifier = Modifier
.clip(shape)
.border(
border = if (isError) errorBorderStroke
else if (isFocused || text.isNotEmpty()) focusedBorderStroke
else borderStroke,
shape = shape
)
.background(
if(text.isNotEmpty()) containerColor
else if (!isFocused ) unfocusedHintColor
else containerColor
)
.padding(contentPadding),
contentAlignment = textAlign
) {
if (text.isBlank() && !isFocused) {
Text(
modifier = Modifier.fillMaxWidth(),
text = hint ?: "0",
style = hintStyle,
color = hintColor,
textAlign = TextAlign.Center
)
}
innerTextField()
}
InvitationCodeBox(
text = text,
isFocused = isFocused,
hint = hint,
isError = isError,
hintStyle = hintStyle,
hintColor = hintColor,
containerColor = containerColor,
unfocusedHintColor = unfocusedHintColor,
borderStroke = borderStroke,
focusedBorderStroke = focusedBorderStroke,
errorBorderStroke = errorBorderStroke,
shape = shape,
textAlign = textAlign,
contentPadding = contentPadding,
innerTextField = innerTextField,
)
}
)
}

@Composable
fun InvitationCodeText(
text: String,
modifier: Modifier = Modifier,
hint: String? = null,
isError: Boolean = false,
textStyle: TextStyle = MissionMateTypography.heading_md_bold,
hintStyle: TextStyle = MissionMateTypography.heading_md_bold,
textColor: Color = ColorGray1_FF404249,
hintColor: Color = ColorDisabled_FFB3B3B3,
containerColor: Color = ColorWhite_FFFFFFFF,
unfocusedHintColor: Color = ColorGray5_FFF5F6F9,
borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_FFF5F6F9),
focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5),
errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858),
shape: Shape = RoundedCornerShape(12.dp),
textAlign: Alignment = Alignment.Center,
contentPadding: PaddingValues = PaddingValues()
) {
InvitationCodeBox(
modifier = modifier,
text = text,
isFocused = false,
hint = hint,
isError = isError,
hintStyle = hintStyle,
hintColor = hintColor,
containerColor = containerColor,
unfocusedHintColor = unfocusedHintColor,
borderStroke = borderStroke,
focusedBorderStroke = focusedBorderStroke,
errorBorderStroke = errorBorderStroke,
shape = shape,
textAlign = textAlign,
contentPadding = contentPadding,
innerTextField = {
Text(
text = text,
style = textStyle,
color = textColor
)
}
)
}

@Composable
fun InvitationCodeBox(
text: CharSequence,
isFocused: Boolean,
modifier: Modifier = Modifier,
hint: String? = null,
isError: Boolean = false,
hintStyle: TextStyle = MissionMateTypography.heading_md_bold,
hintColor: Color = ColorDisabled_FFB3B3B3,
containerColor: Color = ColorWhite_FFFFFFFF,
unfocusedHintColor: Color = ColorGray5_FFF5F6F9,
borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_FFF5F6F9),
focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5),
errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858),
shape: Shape = RoundedCornerShape(12.dp),
textAlign: Alignment = Alignment.Center,
contentPadding: PaddingValues = PaddingValues(),
innerTextField: @Composable () -> Unit = {}
) {
Box(
modifier = modifier
.clip(shape)
.border(
border = if (isError) errorBorderStroke
else if (isFocused || text.isNotEmpty()) focusedBorderStroke
else borderStroke,
shape = shape
)
.background(
if (text.isNotEmpty()) containerColor
else if (!isFocused) unfocusedHintColor
else containerColor
)
.padding(contentPadding),
contentAlignment = textAlign
) {
if (text.isBlank() && !isFocused) {
Text(
modifier = Modifier.fillMaxWidth(),
text = hint ?: "0",
style = hintStyle,
color = hintColor,
textAlign = TextAlign.Center
)
}
innerTextField()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
import com.goalpanzi.mission_mate.core.ui.component.InvitationCodeText
import com.goalpanzi.mission_mate.feature.board.R
import com.goalpanzi.mission_mate.core.ui.component.InvitationCodeTextField
import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights

@Composable
Expand Down Expand Up @@ -84,29 +84,21 @@ fun InvitationCodeDialog(
modifier = Modifier.padding(bottom = 32.dp).wrapContentHeight(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
InvitationCodeTextField(
InvitationCodeText(
modifier = Modifier.weight(1f).aspectRatio(1f),
text = "${code[0]}",
onValueChange = {},
readOnly = true
text = "${code[0]}"
)
InvitationCodeTextField(
InvitationCodeText(
modifier = Modifier.weight(1f).aspectRatio(1f),
text = "${code[1]}",
onValueChange = {},
readOnly = true
text = "${code[1]}"
)
InvitationCodeTextField(
InvitationCodeText(
modifier = Modifier.weight(1f).aspectRatio(1f),
text = "${code[2]}",
onValueChange = {},
readOnly = true
)
InvitationCodeTextField(
InvitationCodeText(
modifier = Modifier.weight(1f).aspectRatio(1f),
text = "${code[3]}",
onValueChange = {},
readOnly = true
text = "${code[3]}"
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.goalpanzi.mission_mate.feature.onboarding.model

class InvitationCode private constructor(
private val symbols: List<InvitationCodeSymbol>
) {
fun getCode(): String = symbols.joinToString(EMPTY_VALUE)

fun valueAt(index: Int): String {
return when (val target = symbols[index]) {
is InvitationCodeSymbol.Code -> target.symbol.toString()
else -> EMPTY_VALUE
}
}

fun input(
text: String,
startIndex: Int
): InvitationCode {
if (startIndex !in symbols.indices) return this
return if (text.isEmpty()) {
deleteAt(startIndex)
} else {
inputSymbols(startIndex, *text.toCharArray())
}
}

private fun inputSymbols(
startIndex: Int,
vararg symbols: Char
): InvitationCode {
val newCodes = this.symbols.toMutableList()
symbols.forEachIndexed { index, c ->
if (startIndex + index >= CODE_LENGTH || !InvitationCodeSymbol.isValidSymbol(c)){
return this
}
newCodes[startIndex + index] = InvitationCodeSymbol.Code(c)
}
return InvitationCode(newCodes)
}

fun deleteAt(index: Int): InvitationCode {
if (index !in symbols.indices) return this
val newCodes = symbols.toMutableList()
newCodes[index] = InvitationCodeSymbol.Blank
return InvitationCode(newCodes)
}

fun isValid(): Boolean {
return symbols.size == CODE_LENGTH && symbols.all {
isValidSymbol(it)
}
}

private fun isValidSymbol(value: InvitationCodeSymbol): Boolean {
return when (value) {
is InvitationCodeSymbol.Code -> InvitationCodeSymbol.isValidSymbol(value.symbol)
else -> false
}
}

companion object {
private const val CODE_LENGTH = 4
private const val EMPTY_VALUE = ""

fun create(initialCode: InvitationCodeSymbol = InvitationCodeSymbol.Blank): InvitationCode {
val codes = List(CODE_LENGTH) { initialCode }
return InvitationCode(codes)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.goalpanzi.mission_mate.feature.onboarding.model

sealed class InvitationCodeSymbol{
data object Blank: InvitationCodeSymbol()
data class Code(val symbol: Char) : InvitationCodeSymbol()

companion object {
fun isValidSymbol(symbol: Char) : Boolean {
return symbol.isDigit() || symbol.isUpperCase()
}
}
}
Loading