Skip to content

Commit 4ad673d

Browse files
authored
Merge pull request #89 from Nexters/fix/invitation-code-input
�초대 코드 입력 화면 버그 수정 및 붙여넣기 구현
2 parents 16d2547 + 5acdad3 commit 4ad673d

File tree

7 files changed

+344
-197
lines changed

7 files changed

+344
-197
lines changed

core/ui/src/main/java/com/goalpanzi/mission_mate/core/ui/component/InvitationCodeTextField.kt

Lines changed: 137 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.goalpanzi.mission_mate.core.ui.component
22

3+
import android.view.KeyEvent
34
import androidx.compose.foundation.BorderStroke
45
import androidx.compose.foundation.background
56
import androidx.compose.foundation.border
@@ -23,7 +24,10 @@ import androidx.compose.ui.draw.clip
2324
import androidx.compose.ui.focus.onFocusChanged
2425
import androidx.compose.ui.graphics.Color
2526
import androidx.compose.ui.graphics.Shape
27+
import androidx.compose.ui.input.key.onKeyEvent
28+
import androidx.compose.ui.text.TextRange
2629
import androidx.compose.ui.text.TextStyle
30+
import androidx.compose.ui.text.input.TextFieldValue
2731
import androidx.compose.ui.text.input.VisualTransformation
2832
import androidx.compose.ui.text.style.TextAlign
2933
import androidx.compose.ui.unit.dp
@@ -59,15 +63,33 @@ fun InvitationCodeTextField(
5963
contentPadding: PaddingValues = PaddingValues(),
6064
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
6165
keyboardActions: KeyboardActions = KeyboardActions.Default,
62-
readOnly : Boolean = false
66+
readOnly: Boolean = false,
67+
onDeleteWhenBlank: () -> Unit = {},
6368
) {
69+
val textFieldValue by remember(text) {
70+
mutableStateOf(
71+
TextFieldValue(
72+
text = text,
73+
selection = TextRange(
74+
index = if(text.isNotBlank()) 1 else 0
75+
)
76+
)
77+
)
78+
}
6479
var isFocused by remember { mutableStateOf(false) }
6580
BasicTextField(
6681
modifier = modifier
6782
.onFocusChanged {
6883
isFocused = it.isFocused
84+
}
85+
.onKeyEvent { event ->
86+
val isDeleteButton = event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL
87+
if (isDeleteButton && textFieldValue.text.isBlank()) {
88+
onDeleteWhenBlank()
89+
}
90+
false
6991
},
70-
value = text,
92+
value = textFieldValue,
7193
singleLine = isSingleLine,
7294
textStyle = textStyle.copy(
7395
color = textColor,
@@ -77,38 +99,122 @@ fun InvitationCodeTextField(
7799
visualTransformation = visualTransformation,
78100
keyboardOptions = keyboardOptions,
79101
keyboardActions = keyboardActions,
80-
onValueChange = onValueChange,
102+
onValueChange = {
103+
if (it.text.isBlank() || it.selection != TextRange.Zero) onValueChange(it.text)
104+
},
81105
readOnly = readOnly,
82106
decorationBox = { innerTextField ->
83-
Box(
84-
modifier = Modifier
85-
.clip(shape)
86-
.border(
87-
border = if (isError) errorBorderStroke
88-
else if (isFocused || text.isNotEmpty()) focusedBorderStroke
89-
else borderStroke,
90-
shape = shape
91-
)
92-
.background(
93-
if(text.isNotEmpty()) containerColor
94-
else if (!isFocused ) unfocusedHintColor
95-
else containerColor
96-
)
97-
.padding(contentPadding),
98-
contentAlignment = textAlign
99-
) {
100-
if (text.isBlank() && !isFocused) {
101-
Text(
102-
modifier = Modifier.fillMaxWidth(),
103-
text = hint ?: "0",
104-
style = hintStyle,
105-
color = hintColor,
106-
textAlign = TextAlign.Center
107-
)
108-
}
109-
innerTextField()
110-
}
107+
InvitationCodeBox(
108+
text = text,
109+
isFocused = isFocused,
110+
hint = hint,
111+
isError = isError,
112+
hintStyle = hintStyle,
113+
hintColor = hintColor,
114+
containerColor = containerColor,
115+
unfocusedHintColor = unfocusedHintColor,
116+
borderStroke = borderStroke,
117+
focusedBorderStroke = focusedBorderStroke,
118+
errorBorderStroke = errorBorderStroke,
119+
shape = shape,
120+
textAlign = textAlign,
121+
contentPadding = contentPadding,
122+
innerTextField = innerTextField,
123+
)
124+
}
125+
)
126+
}
111127

128+
@Composable
129+
fun InvitationCodeText(
130+
text: String,
131+
modifier: Modifier = Modifier,
132+
hint: String? = null,
133+
isError: Boolean = false,
134+
textStyle: TextStyle = MissionMateTypography.heading_md_bold,
135+
hintStyle: TextStyle = MissionMateTypography.heading_md_bold,
136+
textColor: Color = ColorGray1_FF404249,
137+
hintColor: Color = ColorDisabled_FFB3B3B3,
138+
containerColor: Color = ColorWhite_FFFFFFFF,
139+
unfocusedHintColor: Color = ColorGray5_FFF5F6F9,
140+
borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_FFF5F6F9),
141+
focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5),
142+
errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858),
143+
shape: Shape = RoundedCornerShape(12.dp),
144+
textAlign: Alignment = Alignment.Center,
145+
contentPadding: PaddingValues = PaddingValues()
146+
) {
147+
InvitationCodeBox(
148+
modifier = modifier,
149+
text = text,
150+
isFocused = false,
151+
hint = hint,
152+
isError = isError,
153+
hintStyle = hintStyle,
154+
hintColor = hintColor,
155+
containerColor = containerColor,
156+
unfocusedHintColor = unfocusedHintColor,
157+
borderStroke = borderStroke,
158+
focusedBorderStroke = focusedBorderStroke,
159+
errorBorderStroke = errorBorderStroke,
160+
shape = shape,
161+
textAlign = textAlign,
162+
contentPadding = contentPadding,
163+
innerTextField = {
164+
Text(
165+
text = text,
166+
style = textStyle,
167+
color = textColor
168+
)
112169
}
113170
)
114171
}
172+
173+
@Composable
174+
fun InvitationCodeBox(
175+
text: CharSequence,
176+
isFocused: Boolean,
177+
modifier: Modifier = Modifier,
178+
hint: String? = null,
179+
isError: Boolean = false,
180+
hintStyle: TextStyle = MissionMateTypography.heading_md_bold,
181+
hintColor: Color = ColorDisabled_FFB3B3B3,
182+
containerColor: Color = ColorWhite_FFFFFFFF,
183+
unfocusedHintColor: Color = ColorGray5_FFF5F6F9,
184+
borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_FFF5F6F9),
185+
focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5),
186+
errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858),
187+
shape: Shape = RoundedCornerShape(12.dp),
188+
textAlign: Alignment = Alignment.Center,
189+
contentPadding: PaddingValues = PaddingValues(),
190+
innerTextField: @Composable () -> Unit = {}
191+
) {
192+
Box(
193+
modifier = modifier
194+
.clip(shape)
195+
.border(
196+
border = if (isError) errorBorderStroke
197+
else if (isFocused || text.isNotEmpty()) focusedBorderStroke
198+
else borderStroke,
199+
shape = shape
200+
)
201+
.background(
202+
if (text.isNotEmpty()) containerColor
203+
else if (!isFocused) unfocusedHintColor
204+
else containerColor
205+
)
206+
.padding(contentPadding),
207+
contentAlignment = textAlign
208+
) {
209+
if (text.isBlank() && !isFocused) {
210+
Text(
211+
modifier = Modifier.fillMaxWidth(),
212+
text = hint ?: "0",
213+
style = hintStyle,
214+
color = hintColor,
215+
textAlign = TextAlign.Center
216+
)
217+
}
218+
innerTextField()
219+
}
220+
}

feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/component/dialog/InvitationCodeDialog.kt

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
2424
import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C
2525
import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
2626
import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
27+
import com.goalpanzi.mission_mate.core.ui.component.InvitationCodeText
2728
import com.goalpanzi.mission_mate.feature.board.R
28-
import com.goalpanzi.mission_mate.core.ui.component.InvitationCodeTextField
2929
import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
3030

3131
@Composable
@@ -84,29 +84,21 @@ fun InvitationCodeDialog(
8484
modifier = Modifier.padding(bottom = 32.dp).wrapContentHeight(),
8585
horizontalArrangement = Arrangement.spacedBy(8.dp)
8686
) {
87-
InvitationCodeTextField(
87+
InvitationCodeText(
8888
modifier = Modifier.weight(1f).aspectRatio(1f),
89-
text = "${code[0]}",
90-
onValueChange = {},
91-
readOnly = true
89+
text = "${code[0]}"
9290
)
93-
InvitationCodeTextField(
91+
InvitationCodeText(
9492
modifier = Modifier.weight(1f).aspectRatio(1f),
95-
text = "${code[1]}",
96-
onValueChange = {},
97-
readOnly = true
93+
text = "${code[1]}"
9894
)
99-
InvitationCodeTextField(
95+
InvitationCodeText(
10096
modifier = Modifier.weight(1f).aspectRatio(1f),
10197
text = "${code[2]}",
102-
onValueChange = {},
103-
readOnly = true
10498
)
105-
InvitationCodeTextField(
99+
InvitationCodeText(
106100
modifier = Modifier.weight(1f).aspectRatio(1f),
107-
text = "${code[3]}",
108-
onValueChange = {},
109-
readOnly = true
101+
text = "${code[3]}"
110102
)
111103
}
112104
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.goalpanzi.mission_mate.feature.onboarding.model
2+
3+
class InvitationCode private constructor(
4+
private val symbols: List<InvitationCodeSymbol>
5+
) {
6+
fun getCode(): String = symbols.joinToString(EMPTY_VALUE)
7+
8+
fun valueAt(index: Int): String {
9+
return when (val target = symbols[index]) {
10+
is InvitationCodeSymbol.Code -> target.symbol.toString()
11+
else -> EMPTY_VALUE
12+
}
13+
}
14+
15+
fun input(
16+
text: String,
17+
startIndex: Int
18+
): InvitationCode {
19+
if (startIndex !in symbols.indices) return this
20+
return if (text.isEmpty()) {
21+
deleteAt(startIndex)
22+
} else {
23+
inputSymbols(startIndex, *text.toCharArray())
24+
}
25+
}
26+
27+
private fun inputSymbols(
28+
startIndex: Int,
29+
vararg symbols: Char
30+
): InvitationCode {
31+
val newCodes = this.symbols.toMutableList()
32+
symbols.forEachIndexed { index, c ->
33+
if (startIndex + index >= CODE_LENGTH || !InvitationCodeSymbol.isValidSymbol(c)){
34+
return this
35+
}
36+
newCodes[startIndex + index] = InvitationCodeSymbol.Code(c)
37+
}
38+
return InvitationCode(newCodes)
39+
}
40+
41+
fun deleteAt(index: Int): InvitationCode {
42+
if (index !in symbols.indices) return this
43+
val newCodes = symbols.toMutableList()
44+
newCodes[index] = InvitationCodeSymbol.Blank
45+
return InvitationCode(newCodes)
46+
}
47+
48+
fun isValid(): Boolean {
49+
return symbols.size == CODE_LENGTH && symbols.all {
50+
isValidSymbol(it)
51+
}
52+
}
53+
54+
private fun isValidSymbol(value: InvitationCodeSymbol): Boolean {
55+
return when (value) {
56+
is InvitationCodeSymbol.Code -> InvitationCodeSymbol.isValidSymbol(value.symbol)
57+
else -> false
58+
}
59+
}
60+
61+
companion object {
62+
private const val CODE_LENGTH = 4
63+
private const val EMPTY_VALUE = ""
64+
65+
fun create(initialCode: InvitationCodeSymbol = InvitationCodeSymbol.Blank): InvitationCode {
66+
val codes = List(CODE_LENGTH) { initialCode }
67+
return InvitationCode(codes)
68+
}
69+
}
70+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.goalpanzi.mission_mate.feature.onboarding.model
2+
3+
sealed class InvitationCodeSymbol{
4+
data object Blank: InvitationCodeSymbol()
5+
data class Code(val symbol: Char) : InvitationCodeSymbol()
6+
7+
companion object {
8+
fun isValidSymbol(symbol: Char) : Boolean {
9+
return symbol.isDigit() || symbol.isUpperCase()
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)