공부/Android

[Compose] 화면 따라다니는 Footer 만들기(Composable 높이 구하기)

데자와 맛있다 2023. 8. 3. 02:48

웹으로 화면을 만들다보면 항상 만드는게 잇음

그거슨 바로 화면 스크롤해도 따라다니는 footer!!!

지금 티스토리 글 쓰는데도 footer는 있다

화면을 따라다니면서 붙어있고 중간에있을때는 뒤에 내용을 가리지만

가장 스크롤을 밑으로 내리면 내용물을 가리지 않아야한다

옛날옛날옛날에.. 웹 수업들을때였나 리액트 공부할때였나 암튼 이 footer를 만드는데 맨날~ 얘가 스크롤 맨 밑에 있는 뒤에 요소들을 가리곤했다

그럴때 해결법은 footer의 크기를 구해서 화면 맨 밑에다가 margin주는거였음

근데?.? 나는 지금 컴포즈를 하고있단말임?? 근데? 또 똑같은 문제가 또~~ 생긴거임

 

자 이렇게 footer비슷~ 하게 댓글 다는 창을 뒀고 중간에 있을땐 댓글위에 둥둥 떠있어야하지만? 맨밑으로 내리면 댓글을 가리지 않아야한다

완전 똑같은 상황인거임

 

근데 컴포저블의 높이를 어떻게 구하느냐??

바~로 구글 검색

https://stackoverflow.com/questions/66752369/get-height-of-element-jetpack-compose

 

Get height of element Jetpack Compose

I want to get height of my button(or other element) in Jetpack Compose. Do you know how to get?

stackoverflow.com

https://www.charlezz.com/?p=46064 

 

Composable, 너의 위치를 알려죠! (OnGloballyPositioned) | 찰스의 안드로이드

컴포저블의 포지션은 어떻게 구할 수 있나 Android View로 애플리케이션 UI를 구현할 때는 root 에서 하위에 있는 view의 포지션을 구할 수 있었다. 하지만 컴포즈로 UI를 구현시에는 View는 ComposeView 하

www.charlezz.com

 

위 두개 글을 참고했다

 

OnGloballyPositionedModifier는 Modifier의 일종으로, 콘텐츠의 전역 포지션이 변경되었을 때 레이아웃의 최종 LayoutCoordinates와 함께 onGloballyPositioned 콜백을 호출한다. 좌표를 포함하고 있는 이 콜백은 Composition(구성)이 끝났을 때 호출 됨을 명심하자.

암튼 요약하면 modifier 에 onGloballyPositioned  라는 친구가 있는데 걔를 쓰면 해당 컴포저블의 높이나 너비나 뭐.. 위치 이런걸 알수있나보다

그러면 그 값을 구해서 매개변수로 받은 함수를 써서 상위 컴포저블로 전달하고 그 구한 높이 값을 이용해서 Spacer를 만들어주면된다

근데 이 값은 바로 오는게 아니고 composition이 끝날때 호출되어 받을 수 있으므로 이 높이를 받는 상위 컴포저블에서는

높이값이 변경되는 순간 Spacer를 넣어주기위해 화면을 리컴포지션 해야하므로 상위 컴포저블에 높이 상태변경을 알수있도록 높이를 state로 저장한다

(이러면 화면이 고작 이 Spacer하나 넣자고 리컴포지션 한번 더 하게되는데 괜찮은건가? 좀그렇긴함 걍 고정값 넣을까? 왜냐면 나중에 서버랑 댓글 가져오기 통신을 해야되는데 화면이 Spacer때문에 새로고침되면 이거때문에 또 댓글 새로 가져올거임 그러면 쓸데없이 서버랑 통신하는거임 이런거 때문에 리액트에서는 변경이 필요한 부분만 리렌더링하게되는데 컴포즈는 어떤지 모르겠다)

 

@Composable
fun BoardDetailScreen(navController: NavController, viewModel: BoardViewModel) {
    val selectedPost = viewModel.selectedPost.observeAsState()
    val localDensity = LocalDensity.current
    val scrollState = rememberScrollState()
    var height by remember { mutableStateOf(0.dp) }

    if (selectedPost.value != null) {
        Scaffold(
            topBar = { BoardDetailTopAppBar(selectedPost.value!!, viewModel) },
            content = { it ->
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .fillMaxHeight()
                        .padding(it)
                ) {
                    Column(modifier = Modifier.verticalScroll(scrollState)) {

                        Column(modifier = Modifier.padding(16.dp, 0.dp)) {
                            BoardDetailHead(postData = selectedPost.value!!)
                            BoardDetailBody(postData = selectedPost.value!!)
                            Spacer(modifier = Modifier.size(20.dp))
                            Divider()
                            Spacer(modifier = Modifier.size(20.dp))
                            BoardDetailComments(viewModel = viewModel)
                        }
                        Spacer(modifier = Modifier.size(height))
                    }
                    Box(modifier = Modifier.align(Alignment.BottomCenter)) {
                        CommentWriteComponent(
                            viewModel = viewModel,
                            onGloballyPositioned = { layoutCoordinates ->
                                height = with(localDensity){
                                    layoutCoordinates.size.height.toDp()
                                }
                            }
                        )
                    }
                }
            }
        )
    }
}

암튼 상위 컴포저블에 저렇게 함수를 매개변수로 전달했음 값은 height로 상태 저장

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CommentWriteComponent(
    viewModel: BoardViewModel,
    onGloballyPositioned:(LayoutCoordinates) -> Unit,
) {
    var text by remember { mutableStateOf("") }

    Column (
        modifier = Modifier
            .background(color = Color.White)
            .onGloballyPositioned (onGloballyPositioned)
    ){
        Row (verticalAlignment = Alignment.CenterVertically){
            Spacer(modifier = Modifier.size(20.dp))
            OutlinedTextField(
                value = text,
                onValueChange = { text = it },
                colors = TextFieldDefaults.outlinedTextFieldColors(focusedBorderColor = PrimaryPink),
                placeholder = { Text(text = stringResource(id = R.string.board_detail_write_comment_hint)) },
                shape = RoundedCornerShape(14.dp)
            )
            Spacer(modifier = Modifier.size(10.dp))
            Icon(
                imageVector = MyIconPack.Writecomment,
                contentDescription = "댓글 쓰기",
                tint = Color.Unspecified,
                modifier = Modifier.size(34.dp)
            )
            Spacer(modifier = Modifier.size(20.dp))
        }
        Spacer(modifier = Modifier.size(10.dp))
    }

}

높이를 구해야 되는 하위 컴포저블의 내용임

매개변수로 받은 함수를 Modifier .onGloballyPositioned (onGloballyPositioned) 이렇게 함수를 전달했다

이렇게 해 주니

이렇게 footer가 둥둥 떠있으면서 맨 밑으로 갔을때 내용을 가리지 않게된다

 

실험을 해봄

호출 이거는 viewModel에 있는 댓글 가져오는애 함수에 로그 찍게 한거고

111111111이거는 onGlobalyPositioned 에 보내는 함수 중간에 로그 찍게 한거임

다행히 무한반복은 안됐는데

처음에 게시글 누르면 댓글 가져오기 하고

그다음 화면 구성 완료해서 111111출력되고

그러면 또 화면이 리컴포저블 해서 또 호출

그다음 또 1111출력...

 

그냥 고정 값을 Space로 주는게 좋을까?

 

근데 또 해보니깐 spacer 안해도 걍 호출 계속되는데?

setTest()가 test값 업데이트라고 친 함수임

이렇게 쓰는게 아닌가봄

생각해보니 위 처럼 PostItem에 클릭되면 실행할 함수를 매개변수로 줬는데

이 함수에서 값 업데이트하는 viewModel안의 함수를 불렀었음

다시 실험해보니 위 처럼 댓글 값 받아오기 함수는 한번만 실행되었고 Spacer때문에 111111은 여러번 나왔다

재구성되면 재구성되었다고 출력하게 만들었음(뒤늦게 떠올랐음 리액트 할때 재구성 이런식으로 확인했었음.. 고마워요 니콜라스 ^.^ 보면볼수록 걍 리액트임이거)

보니깐 재구성은 여러번 되고있었고

통신할때 쓰는 함수는 한번만 호출되었다

생각해보면 당연함 클릭 했을때만 댓글 가져오도록 했는데 당연한거임

 

그래서 결론은

1. 컴포저블 요소 크기, 위치 등등은 Modifier .onGloballyPositioned 얘로 가져온다, 상위 컴포저블에서 그 값이 필요한 경우 호이스팅? 으로 값을 가져올 함수를 전달해준다

2. 서버와 통신으로 뭘 가져와야하는 함수는 리컴포지션 되는 부분에 절대 넣지 말고 딱 한번만 일어날 수 있도록 한다

3. 리액트와 매우 유사하므로 논리? 뭐라해야됨 사용법? 팁? 측면에서 리액트의 것을 훔쳐올수있겠다

 

------------------------------------------

잼난걸 찾았다!!!!!!!!!!!!!!!!!!

리액트에서 딱 한번만 (리렌더링 되던가 말던가 한번만실행) 인거가 useEffect인데 고거에 대응되는 컴포즈의 함수? 인지 뭔지를 찾앗다

https://stackoverflow.com/questions/70480709/what-is-the-useeffect-correspondent-in-android-compose-component

 

What is the useEffect correspondent in android compose component

I have a master-detail app for android using kotlin and jetpack compose components. The flow in the app should be: Open app The app starts in the master view with a list having all items that were

stackoverflow.com

LaunchedEffect(Unit) {
    // this will run once per composition 
}

이거라는디?

 

https://kotlinworld.com/251

 

[Compose Side Effect] LaunchedEffect에서 한 번만 실행되어야 하는 동작 처리하기

LaunchedEffect에서 한 번만 실행되어야 하는 동작 처리하기 LaunchedEffect는 key값이 바뀌면 블록내의 동작을 취소한 후 다시 실행한다. 따라서 한 번만 수행해야하는 작업들은 LaunchedEffect에 true나 Unit

kotlinworld.com

바로이거야~ 내가원하는게 바로이거야~ 나중에 자세히 봐야겟다 ㅎ

-------------------------

문제 생김

이게 box로 댓글창을 댓글 위에 올려놓는 식 이라서 댓글창에 버튼이나 text filed 말고 다른 테두리 부분을 눌렀을때

그 밑에 대댓글 버튼이나 메뉴 버튼이 겹쳐져 있는 경우 밑에 깔려있는 그 버튼이 클릭된다

https://stackoverflow.com/questions/71211699/jetpack-compose-box-is-not-working-as-expected

 

Jetpack compose box is not working as expected

I cannot understand how are box working, why when box is covering some layer of our screen, for example we are three boxes @Composable fun TestScope() { 1 Box(modifier = Modifier .

stackoverflow.com

보니깐 밑에 깔려있는거는 아래로 이벤트가 전달된다고 하고 view도 마찬가지라고 한다

그래서 밑으로 클릭이 전달 안되게 하려면 surface로 감싸라고한다

이렇게 댓글다는 창 겉에다가 surface로 감쌌더니 이벤트가 아래로 내려가는게 막혔다

 

 

이렇게 댓글다는 창 전체에 clickable을 넣고 아무것도 안하게 해도 클릭이벤트가 아래로 전달되는것을 막을 수는 있는데 리플이 생기고 맞는 방법인지도 모르겟다