Kotlin Weekly

Kotlin Weekly # -384 "Jetpack Compose의 Modifiers"

베블렌 2023. 12. 13. 11:58

12월 2주차에는 Modifiers에 관한 글입니다.

 

https://proandroiddev.com/what-can-advanced-lesser-known-modifiers-do-for-your-ui-9c76855bced6

 

What can Advanced / Lesser Known Modifiers do for your UI?

A Journey Through Advanced and Lesser Utilized Modifiers in Jetpack Compose

proandroiddev.com

 

 

 

소개

https://veblen.tistory.com/23

예전에 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번이 좋아보입니다. 이런 잘 알려지지 않은 기능들에 대한 글이 많이 나왔으면 좋겠습니다.

 

 

 

http://kotlinweekly.net/

 

** Kotlin Weekly **

 

kotlinweekly.net