12월 2주차에는 Modifiers에 관한 글입니다.
https://proandroiddev.com/what-can-advanced-lesser-known-modifiers-do-for-your-ui-9c76855bced6
소개
예전에 Modifier의 역사글을 작성한적이 있는데, 이번에는 일반적으로 사용되지 않는 고급 및 덜 알려진 기능들에 대한 글입니다.
내용
1. Modifier.graphicsLayer: 이 수식어는 회전, 크기 조절, 불투명도, 그림자 등을 제어하여 UI 요소에 복잡한 시각적 효과를 만들 수 있습니다. 예를 들어, 카드의 3D 플립 애니메이션을 구현할 때 사용할 수 있습니다. 예제 코드는 아래와 같습니다.
@Composable
fun FlipCardDemo() {
var flipped by remember { mutableStateOf(false) }
val rotationZ by animateFloatAsState(targetValue = if (flipped) 180f else 0f, label = "")
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Card(
modifier = Modifier
.graphicsLayer {
this.rotationX = rotationZ
cameraDistance = 12f * density
shadowElevation = if (flipped) 0f else 30f
alpha = if (flipped) 0.3f else 0.8f
}
.clickable { flipped = !flipped }
.width(350.dp)
.height(200.dp),
colors = CardDefaults.cardColors(
containerColor = Color.DarkGray,
)
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Hey bro", color = Color.White, fontSize = 32.sp)
}
}
}
}
2. Modifier.drawWithCache: 복잡한 그리기 작업에 대한 성능을 향상시킵니다. 복잡한 사용자 정의 그리기에 이상적입니다.
@Composable
fun BarChartExample(dataPoints: List<Float>) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier
.size(300.dp)
.drawWithCache {
onDrawWithContent {
drawContent()
// Draw axes
drawLine(
start = Offset(50f, size.height),
end = Offset(size.width, size.height),
color = Color.Green,
strokeWidth = 10f
)
drawLine(
start = Offset(50f, 50f),
end = Offset(50f, size.height),
color = Color.Red,
strokeWidth = 10f
)
// Draw bars for each data point
val barWidth = size.width / (dataPoints.size * 2)
dataPoints.forEachIndexed { index, value ->
val left = barWidth * (index * 2 + 1)
val top = size.height - (value / dataPoints.max() * size.height)
val right = left + barWidth
val bottom = size.height
drawRect(
Color.Blue,
topLeft = Offset(left, top),
size = Size(right - left, bottom - top)
)
}
}
}
)
}
}
3. Modifier.onSizeChanged: 컴포저블의 크기가 변경될 때 반응합니다. 예를 들어, 컨테이너의 크기에 따라 다른 레이아웃을 보여주는 데 사용할 수 있습니다.
@Composable
fun ResponsiveImageTextLayout() {
var containerWidth by remember { mutableStateOf(0.dp) }
Box(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { newSize ->
containerWidth = newSize.width.dp
},
) {
if (containerWidth < 600.dp) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
PlaceHolderComponent()
TextComponent()
}
} else {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
PlaceHolderComponent()
TextComponent()
}
}
}
}
@Composable
fun PlaceHolderComponent() {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.LightGray)
)
}
@Composable
fun TextComponent() {
Text(
text = "Responsive Text",
modifier = Modifier.padding(16.dp)
)
}
4. Modifier.onPlaced: 컴포저블이 부모 내에서 어디에 배치되었는지 정보를 제공합니다. 이는 특정 UI 요소의 위치에 따라 다른 작업을 수행할 때 유용합니다.
@Composable
fun InteractiveGridDemo() {
val cellSize = 90.dp
val numRows = 3
val numColumns = 3
val gridState = remember { mutableStateOf(Array(numRows * numColumns) { Offset.Zero }) }
val selectedCell = remember { mutableStateOf(-1) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
for (row in 0 until numRows) {
Row {
for (column in 0 until numColumns) {
val cellIndex = row * numColumns + column
Box(
modifier = Modifier
.size(cellSize)
.onPlaced { layoutCoordinates ->
gridState.value[cellIndex] = layoutCoordinates.positionInRoot()
}
.clickable { selectedCell.value = cellIndex }
.border(8.dp, Color.Black)
)
}
}
}
}
if (selectedCell.value >= 0) {
val position = gridState.value[selectedCell.value]
Box(
modifier = Modifier
.offset {
IntOffset(
position.x.roundToInt() - 35.dp
.toPx()
.toInt(),
position.y.roundToInt() - 80.dp
.toPx()
.toInt()
)
}
.size(width = 150.dp, height = 60.dp)
.background(Color.DarkGray.copy(alpha = 0.9f)),
contentAlignment = Alignment.Center
) {
Text(
text = "Cell Clicked: ${selectedCell.value}",
color = Color.Red,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
}
5. Modifier.zIndex(): 컴포저블의 Z-순서를 조정하여 중첩된 요소들 중 어떤 것이 위에 표시될지 결정합니다. 이는 겹쳐지는 UI 요소를 관리할 때 유용합니다.
6. Modifier.onKeyEvent: 하드웨어 키보드의 키 이벤트를 처리합니다. 예를 들어, 사용자가 'Enter' 키를 눌렀을 때 특정 작업을 수행하도록 설정할 수 있습니다.
7. Modifier.clipToBounds(): 컴포저블의 내용이 경계를 넘어가지 않도록 제어합니다. 이는 오버플로우를 제어하고, UI 요소가 부모의 경계를 넘어 확장되는 것을 방지하는 데 유용합니다.
@Composable
fun CanvasClippingExample() {
NonClippingLayout {
Canvas(
modifier = Modifier.size(50.dp)
.border(width = 2.dp, color = Color.Black)
.clipToBounds()
) {
drawRect(
color = Color.Blue,
size = Size(150.dp.toPx(), 150.dp.toPx())
)
}
}
}
@Composable
fun NonClippingLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
8. Modifier.paddingFromBaseline(): 텍스트의 기준선에 상대적으로 패딩을 적용합니다. 이는 텍스트를 수직적으로 정렬할 때 유용하게 사용됩니다.
@Composable
fun BaselinePaddingExample() {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
"Text with baseline padding",
modifier = Modifier
.fillMaxWidth()
.paddingFromBaseline(top = 32.dp),
fontSize = 16.sp,
textAlign = TextAlign.Center
)
Divider(color = Color.Gray, thickness = 1.dp)
Text(
"Another Text aligned to baseline",
modifier = Modifier
.fillMaxWidth()
.paddingFromBaseline(top = 32.dp),
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}
}
9. Interaction Source: 컴포저블의 상호작용 상태를 추적합니다. 터치, 드래그 또는 클릭과 같은 사용자 상호 작용에 대한 시각적 응답을 사용자 지정할 때 유용합니다.
@Composable
fun InteractiveButtonExample() {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(
onClick = { },
interactionSource = interactionSource,
colors = ButtonDefaults.buttonColors(
containerColor = if (isPressed) Color.Gray else Color.Blue
)
) {
Text("Press Me")
}
}
}
10. Modifier.absoluteOffset(): 컴포저블을 원래 위치에 대해 절대적인 x, y 좌표로 오프셋합니다. 이는 특정 위치에 요소를 배치할 때 유용합니다.
11.Modifier.weight(): fillMaxWidth() 또는 fillMaxHeight()와 결합하여 컴포저블이 사용 가능한 공간을 비례적으로 차지하게 합니다.
12. Modifier.focusRequester(): 프로그램적으로 컴포저블에 포커스를 요청합니다. 이는 복잡한 UI에서 특정 요소에 포커스를 설정할 때 유용합니다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FocusRequesterExample() {
val focusRequester = FocusRequester()
val focusRequester2 = FocusRequester()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
TextField(
value = "Field 1",
onValueChange = {},
modifier = Modifier
.focusRequester(focusRequester)
)
TextField(
value = "Field 2",
onValueChange = {},
modifier = Modifier
.focusRequester(focusRequester2)
)
Button(onClick = { focusRequester.requestFocus() }) {
Text("Focus First Field")
}
Button(onClick = { focusRequester2.requestFocus() }) {
Text("Focus Second Field")
}
}
}
13. Modifier.nestedScroll(): 중첩된 스크롤 동작을 가능하게 합니다. 중첩된 스크롤 가능한 컴포넌트가 있을 때, 예를 들어 스크롤 가능한 리스트가 수직 스크롤 가능한 페이지 내에 있는 경우에 유용합니다.
@Composable
fun NestedScrollWithCollapsibleHeader() {
// header height
val headerHeight = remember { mutableStateOf(150.dp) }
// adjust the header size based on scroll
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newHeight = headerHeight.value + delta.dp
headerHeight.value = newHeight.coerceIn(50.dp, 150.dp)
return Offset.Zero // Consuming no scroll
}
}
}
Box(modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)) {
Column {
Box(
modifier = Modifier
.height(headerHeight.value)
.fillMaxWidth()
.background(Color.LightGray)
) {
Text("Collapsible Header", Modifier.align(Alignment.Center))
}
LazyColumn {
items(100) { index ->
Text("Item $index", modifier = Modifier.padding(16.dp))
}
}
}
}
}
정리
예시를 작성하지 않은 것들은 유명하기도 하고 많이 쓰일것 같아서 제외했습니다. 개인적으로 얻는것은 1번하고 7번이 좋아보입니다. 이런 잘 알려지지 않은 기능들에 대한 글이 많이 나왔으면 좋겠습니다.
'Kotlin Weekly' 카테고리의 다른 글
Kotlin Weekly # -386 "Koin 2023: 성장과 미래 계획 " (0) | 2023.12.25 |
---|---|
Kotlin Weekly # -385 "Kotlin Compiler Plugins" (0) | 2023.12.18 |
Kotlin Weekly # -383 "Kotlin 사용 주의점" (0) | 2023.12.13 |
Kotlin Weekly # -382 " 호환성 종류와 이해" (0) | 2023.12.12 |
Kotlin Weekly # -381 "Kotlin Multiplatform(KMP)의 2024년 로드맵" (0) | 2023.12.12 |