Android/Jetpack Compose

[Compose] Parents View의 크기를 구해 자식뷰에 적용하기.

Heeg's 2023. 12. 4. 21:50
728x90

Compose를 사용하여 UI를 그리다 보면, 생각보다 유동적인 뷰를 갖고 있는 UI를 그리는 경우가 많다.

기본적으로 Column, Row를 사용하는 경우에는 유동적인 View라고 하더라도 쉽게 구현이 가능하지만, Box를 사용하게 되면 생각보다 까다로운 점들이 많이 생긴다.

 

필자는 업무를 진행하다보니 Box Layout 내부에 있으면서 유동적인 데이터에 따라서 별도의 뷰를 또 그려야 하는 상황을 자주 마주쳤다.

이때, 어떻게 꼼수를 써가면서 구현을 하다가 막히는 케이스가 발견되어 다른 방법을 찾아보다 부모의 뷰의 크기를 구하여 적용하는 방법을 찾아내어 적용하였다.


해당 방법을 사용하기 위해서는

 

fun Modifier.onSizeChanged

 

해당 Modifier 함수 하나만 사용하면 된다.

 

해당 함수의 설명을 보면 다음과 같이 나와있다.

 

Invoked with the size of the modified Compose UI element when the element is first measured or when the size of the element changes.

 

해당 함수는 Compose UI 요소가 변경될 때 호출이 된다는 것이다.

 

Using the onSizeChanged size value in a MutableState to update layout causes the new size value to be read and the layout to be recomposed in the succeeding frame, resulting in a one frame lag.

 

또한, 해당 크기를 사용하게 되면 새 값을 읽고, 재구성하기 때문에 1 프레임의 지연이 발생된다고 한다.

 

즉, UI가 유동적으로 변경이 될 때 해당 함수를 사용하여 변경된 크기를 가져오고,

그 값을 사용하여 레이아웃의 크기를 조정하게 되면 1 프레임의 지연을 걸고 UI를 다시 그릴 수 있다는 것이다.

 

유동적이긴 하지만 한번 바뀌고 나면 새로 갱신하기 전까지 화면이 바뀌지 않는 케이스뿐 아니라, 계속해서 부모의 뷰가 바뀌는 케이스에도 해당 함수를 사용하면 간단하게 유동적으로 UI를 그릴 수 있게 된다.

 

그렇다면,

어떻게 사용해야 할까?

 

@Stable
fun Modifier.onSizeChanged(
    onSizeChanged: (IntSize) -> Unit
) = this.then(
    OnSizeChangedModifier(
        onSizeChanged = onSizeChanged,
        inspectorInfo = debugInspectorInfo {
            name = "onSizeChanged"
            properties["onSizeChanged"] = onSizeChanged
        }
    )
)

 

해당 함수는 다음과 같이 구현이 되어있다.

 

onSizeChanged를 통해 반환되는 값은 IntSize 타입이다.

 

@Stable
fun IntSize(width: Int, height: Int): IntSize = IntSize(packInts(width, height))

@Immutable
@kotlin.jvm.JvmInline
value class IntSize internal constructor(@PublishedApi internal val packedValue: Long) {

    @Stable
    val width: Int
        get() = unpackInt1(packedValue)

    @Stable
    val height: Int
        get() = unpackInt2(packedValue)

	...
    
    companion object {
        val Zero = IntSize(0L)
    }
}

 

위의 코드는 현재 예제에 필요한 코드만 가져왔다.

 

IntSize 타입은 위와 같이 width와 height 값을 가지고 있는 형태이다.

즉, 해당 데이터 타입을 동적으로 받아와서 저장하고 사용하게 구현하면 된다.

 

var size by remember { mutableStateOf(IntSize.Zero) }

Column(modifier = Modifier
    .wrapContentSize()
    .onSizeChanged { size = it }) {
    ...
}

 

이처럼 동적으로 데이터를 size에 저장하는데, 저장하는 타입은 InitSize 타입을 저장한다.

Default 값으로는 InitSize 객체에서 companion object로 선언되어 있던 Zero를 사용하였는데, 물론 해당 값을 사용하지 않고 Default 값을 별도로 사용해도 무관하다.

 

여기서 중요한 부분은, Column의 Modifier 함수에 onSizeChanged 부분이다.

해당 부분에서는 Column의 크기가 변경될 때 마다 View의 크기를 반환하여 Size에 저장하게 된다.

 

Box(
    modifier = modifier
		...
        .then(
            with(LocalDensity.current) {
                Modifier.size(
                    width = size.width.toDp(),
                    height = 1.dp,
                )
            }
        )
)

 

필자가 해당 size를 자식 뷰에 사용한 예제이다.

 

해당 케이스에서는 height는 1으로 고정되어 있지만, width 가 부모 뷰에 따라서 달라지는 케이스였다.

그렇기 때문에, 위와 같이 width에서만 size를 가져와서 적용시켜 주도록 하였다.

 

여기서 평소에는 자주 사용하지 않는 modifier API를 볼 수 있다.

 

.then(
    with(LocalDensity.current) {
        Modifier.size(
            width = size.width.toDp(),
            height = 1.dp,
        )
    }
)

 

해당 부분 함수도 중요하기 때문에 설명하고 넘어가겠다.

 

우선 .then 부분은 Modifier를 순차적으로 연결하기 위해서 사용하는 api이다.

then 절 앞에 있는 Modifier를 적용한 후, 이후에 순차적으로 추가되는 Modifier에 접근하여 적용하도록 한다.

 

뒤의 내용을 보면, Modifier.size ( ~ ) 부분을 통해 Size를 바꿔주고 있는데 해당 부분만 사용한다고 가정하면 then을 사용하지 않고 바로 with(~)을 통해 선언해도 무관하다.

하지만, 필자의 케이스에서는 Modifier 값 자체를 새롭게 선언하여 size만 적용하는 것이 아닌, 전달 받은 Modifier를 적용하고 거기에 추가로 유동적으로 size를 변환하는 케이스였기 때문에 위와 같이 선언하였다.

 

즉,

 

Box(
    modifier = with(LocalDensity.current) {
        Modifier.size(
            width = size.width.toDp(),
            height = 1.dp,
        )
    }
)

 

이렇게만 선언하여 사용해도 괜찮다.

 

다음으로,

with 이하의 부분인데 해당 부분은 LocalDensity를 사용하기 위하여 선언하였다.

LocalDensity에 대한 설명을 보면 다음과 같이 나와있다.

 

Provides the Density to be used to transform between density-independent pixelunits (DP) and pixel units or scale-independent pixel units (SP) and pixel units. This is typically used when a DP is provided and it must be converted in the body of Layout or DrawModifier.

 

간단히 말해서, size 값을 dp로 변환하는데 사용하기 위해서 선언하는 부분이다.

 

Box(
    modifier = Modifier.size(
        width = size.width.toDp(),
        height = 1.dp,
    )
)

 

해당 부분을 지우고 다음과 같이 사용할 수는 있지만,

 

 

이처럼 dp로 변환하는 부분을 직접 찾아서 구현해주어야 한다.


이것으로, 부모 뷰의 크기를 구해 유동적으로 변경되는 자식뷰를 그리는 방법에 대하여 설명해 보았다.

 

어떻게보면 정말 당연히 사용하고 있어야 했던 부분인데 필자는 생각보다 이런 것들의 유무에 대해서 많이 알고 있지 않았고,

그렇기 때문에 주먹구구식으로 구현하던 부분이 많았던 것 같다.

 

계속해서 다양한 문서를 읽어보고, 공부하고 학습해야하는 부분인데 그렇지 못했던 부분이 아쉽게만 생각이 들었고

그렇기 때문에 자그마한 것들이라고 새롭게 알게 된 내용이 있다면 짧게나마 글을 작성해 나가려고 한다.

 

글을 쓰면서 대충 알고 사용했던 부분들에 대해서도 더 자세히 알게 되는 것도 있고 하니 말이다.

728x90