-
Notifications
You must be signed in to change notification settings - Fork 9
/
textarea.kt
333 lines (318 loc) · 10.7 KB
/
textarea.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/*
* Designed and developed by 2022 SungbinLand, Team Duckie
*
* Licensed under the MIT.
* Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE
*/
package team.duckie.quackquack.ui.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import team.duckie.quackquack.ui.animation.QuackAnimationSpec
import team.duckie.quackquack.ui.border.QuackBorder
import team.duckie.quackquack.ui.border.animatedQuackBorderAsState
import team.duckie.quackquack.ui.color.QuackColor
import team.duckie.quackquack.ui.component.internal.QuackText
import team.duckie.quackquack.ui.modifier.applyQuackBorder
import team.duckie.quackquack.ui.textstyle.QuackTextStyle
import team.duckie.quackquack.ui.theme.LocalQuackTextFieldColors
/**
* [QuackBorderTextArea] 의 테두리 속성을 계산합니다.
*
* @param isFocused 포커스 여부
* @return 포커스 여부에 따라 계산된 [테두리 속성][QuackBorder]
*/
@Suppress("KDocUnresolvedReference")
private val QuackTextAreaBorder: (
isFocused: Boolean,
) -> QuackBorder = { isFocused ->
when (isFocused) {
true -> QuackBorder(
color = QuackColor.DuckieOrange,
width = 1.dp,
)
false -> QuackBorder(
color = QuackColor.Gray3,
width = 1.dp,
)
}
}
/**
* [QuackBorderTextArea] 의 [QuackTextStyle] 을 계산합니다.
*
* @param isPlaceholder 보여자고 있는 텍스트가 플레이스홀더인지 여부
* @return 플레이스홀더 여부에 따라 계산된 [QuackTextStyle]
*/
@Suppress("KDocUnresolvedReference")
private val QuackBorderTextAreaTextStyle: (
isPlaceholder: Boolean,
) -> QuackTextStyle = { isPlaceholder ->
when (isPlaceholder) {
true -> QuackTextStyle.Subtitle2.change(
color = QuackColor.Gray2,
weight = FontWeight.Normal,
)
else -> QuackTextStyle.Subtitle2.change(
color = QuackColor.Black,
weight = FontWeight.Normal,
)
}
}
/**
* [QuackTextArea] 의 [QuackTextStyle] 을 계산합니다.
*
* @param isPlaceholder 보여자고 있는 텍스트가 플레이스홀더인지 여부
* @return 플레이스홀더 여부에 따라 계산된 [QuackTextStyle]
*/
@Suppress("KDocUnresolvedReference")
private val QuackTextAreaTextStyle: (
isPlaceholder: Boolean,
) -> QuackTextStyle = { isPlaceholder ->
when (isPlaceholder) {
true -> QuackTextStyle.Subtitle.change(
color = QuackColor.Gray2,
weight = FontWeight.Normal,
)
else -> QuackTextStyle.Subtitle.change(
color = QuackColor.Black,
weight = FontWeight.Normal,
)
}
}
/**
* [QuackBorderTextArea] 의 안쪽에 들어갈 패딩 값
*/
private val QuackBorderTextAreaPadding = PaddingValues(
all = 16.dp,
)
/**
* [QuackTextArea] 의 안쪽에 들어갈 패딩 값
*/
private val QuackTextAreaPadding = PaddingValues(
top = 10.dp,
bottom = 8.dp,
)
/**
* [QuackBorderTextArea] 의 최소 높이 값
*
* QuackTextArea 는 최소 높이 값을 갖습니다. 이 높이를
* 초과할 경우 패딩에 맞게 늘어나야 합니다.
*/
private val QuackBorderTextAreaDefaultHeight = 140.dp
/**
* [QuackTextArea] 의 최소 높이 값
*
* QuackTextArea 는 최소 높이 값을 갖습니다. 이 높이를
* 초과할 경우 패딩에 맞게 늘어나야 합니다.
*/
private val QuackTextAreaDefaultHeight = 250.dp
private val QuackBorderTextAreaShape = RoundedCornerShape(
size = 12.dp,
)
/**
* 테두리를 갖는 TextArea 를 구현합니다.
* TextArea 는 항상 현재 화면의 가로 길이를 꽉 채워서 width 가
* 지정됩니다.
*
* 모든 값에는 자동으로 [QuackAnimationSpec] 애니메이션이 적용됩니다.
*
* @param text 표시할 텍스트
* @param onTextChanged IME 로 텍스트가 입력됐을 때 호출되는 람다.
* 람다의 인자로는 입력된 텍스트가 들어옵니다.
* @param placeholderText [text] 가 비어있을 때 표시할 대체 텍스트
* @param doneAction [IME 버튼][ImeAction.Done] 클릭 이벤트를 받았을 때 호출될 람다
*/
@Composable
public fun QuackBorderTextArea(
text: String,
onTextChanged: (text: String) -> Unit,
placeholderText: String = "",
padding: PaddingValues = QuackBorderTextAreaPadding,
doneAction: KeyboardActionScope.() -> Unit = {},
): Unit = QuackTextAreaInternal(
text = text,
onTextChanged = onTextChanged,
placeholderText = placeholderText,
doneAction = doneAction,
padding = padding,
isBordered = true,
)
/**
* 기본 TextArea 를 구현합니다.
* TextArea 는 항상 현재 화면의 가로 길이를 꽉 채워서 width 가
* 지정됩니다.
*
* 모든 값에는 자동으로 [QuackAnimationSpec] 애니메이션이 적용됩니다.
*
* @param text 표시할 텍스트
* @param onTextChanged IME 로 텍스트가 입력됐을 때 호출되는 람다.
* 람다의 인자로는 입력된 텍스트가 들어옵니다.
* @param placeholderText [text] 가 비어있을 때 표시할 대체 텍스트
* @param doneAction [IME 버튼][ImeAction.Done] 클릭 이벤트를 받았을 때 호출될 람다
*/
@Composable
public fun QuackTextArea(
text: String,
onTextChanged: (text: String) -> Unit,
placeholderText: String = "",
padding: PaddingValues = QuackTextAreaPadding,
doneAction: KeyboardActionScope.() -> Unit = {},
): Unit = QuackTextAreaInternal(
text = text,
onTextChanged = onTextChanged,
placeholderText = placeholderText,
doneAction = doneAction,
padding = padding,
isBordered = false,
)
/**
* TextArea 들을 구현합니다. 덕키 내부용으로 사용됩니다.
* TextArea 는 항상 현재 화면의 가로 길이를 꽉 채워서 width 가
* 지정됩니다.
*
* 모든 값에는 자동으로 [QuackAnimationSpec] 애니메이션이 적용됩니다.
*
* @param text 표시할 텍스트
* @param onTextChanged IME 로 텍스트가 입력됐을 때 호출되는 람다.
* 람다의 인자로는 입력된 텍스트가 들어옵니다.
* @param placeholderText [text] 가 비어있을 때 표시할 대체 텍스트
* @param doneAction [IME 버튼][ImeAction.Done] 클릭 이벤트를 받았을 때 호출될 람다
* @param isBordered 구현해야 할 TextArea 가 [QuackBorderTextArea] 인지 여부.
* 이 값에 따라 디자인이 달라집니다.
*/
@Composable
private fun QuackTextAreaInternal(
text: String,
onTextChanged: (text: String) -> Unit,
placeholderText: String,
padding: PaddingValues = QuackTextAreaPadding,
doneAction: KeyboardActionScope.() -> Unit,
isBordered: Boolean,
) {
val quackTextFieldColors = LocalQuackTextFieldColors.current
val isPlaceholder = text.isEmpty()
var isFocused by remember {
mutableStateOf(
value = false,
)
}
val shape = remember {
when (isBordered) {
true -> QuackBorderTextAreaShape
else -> RectangleShape
}
}
val textStyle = remember {
when (isBordered) {
true -> QuackBorderTextAreaTextStyle(
/*isPlaceholder = */
false,
)
else -> {
QuackTextAreaTextStyle(
/*isPlaceholder = */
false,
)
}
}.asComposeStyle()
}
val placeholderTextStyle = remember {
when (isBordered) {
true -> QuackBorderTextAreaTextStyle(
/*isPlaceholder = */
true,
)
else -> {
QuackTextAreaTextStyle(
/*isPlaceholder = */
true,
)
}
}
}
val animatedQuackBorderOrNull = when (isBordered) {
true -> animatedQuackBorderAsState(
targetValue = QuackTextAreaBorder(
/*isFocused = */
isFocused,
)
)
else -> null
}
Box(
modifier = Modifier
.wrapContentSize()
.clip(
shape = shape,
)
.applyQuackBorder(
enabled = isBordered,
border = animatedQuackBorderOrNull,
shape = shape,
)
.padding(
paddingValues = padding,
),
) {
if (isPlaceholder) {
QuackText(
modifier = Modifier
.fillMaxWidth()
.zIndex(
zIndex = 0f,
),
text = placeholderText,
style = placeholderTextStyle,
)
}
BasicTextField(
modifier = Modifier
.zIndex(
zIndex = 1f,
)
.fillMaxWidth()
.requiredHeightIn(
min = when (isBordered) {
true -> QuackBorderTextAreaDefaultHeight
else -> QuackTextAreaDefaultHeight
},
)
.onFocusEvent { event ->
isFocused = event.isFocused
},
value = text,
onValueChange = onTextChanged,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onAny = doneAction,
),
textStyle = textStyle,
cursorBrush = quackTextFieldColors.textFieldCursorColor.toBrush(),
)
}
}