Devfest Korea week 2-1 Layouts in Jetpack Compose

1

안녕하세요! 오늘도 열심히 발전중이신 개발자님들 환영합니다!

😢 이제 부터가 진짜 시작이다!

저번 글에서는 애니메이션을 넣는방법, 단순한 레이아웃 그리는법을 배웠습니다.
오늘 알아볼것은 심화된 레이아웃 그리기입니다!
BottomSeatLayout이라던지 NavigationLayout을 이곳에서 배울수 있습니다.
그리고 ConstraintLayout도 알아볼 예정입니다.

👌 수정자

수정자를 사용하면 컴포저블을 꾸밀수 있습니다.
동작, 모양 변경, 입력처리, 클릭, 스크롤, 드래그 등등 고급 상호작용

구글 코드랩에서는 해당된것을 만들어달라고 합니다.

@Composable
fun PhotographerCard() {
    Row {
        Surface(
            modifier = Modifier.size(50.dp),
            shape = CircleShape,
            color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
        ) {
            // Image goes here
        }
        Column {
            Text("Alfred Sisley", fontWeight = FontWeight.Bold)
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("3 minutes ago", style = MaterialTheme.typography.body2)
            }
        }
    }
}

아래의 Layout를 위의것으로 만들라면 어떻게 해야할까요?
일단 서로가 나뉘어야 할것 같으니, 뒤의 글자에 패딩과 수직 정렬을 주도록 합시다.

 .padding(start = 8.dp)
 .align(Alignment.CenterVertically)

modifier 줄이는 법
modifier = Modifier를 계속 호출하는게 귀찮다면,
함수의 파라미터에 등록하는법을 사용하자.

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
    Row(modifier) { ... }
}

이제 클릭이벤트를 줄것입니다.

Row(modifier
      .padding(16.dp)
      .clickable(onClick = { /* Ignoring onClick */ })

클릭 이벤트를 적용했으나, padding값 만큼 넓어지지 않은 모양입니다.
그렇다면 padding값만큼, 클릭이벤트를 적용하려면 어떻게 해야 할까요?

Row(modifier
      //clickable 아래에 padding값을 주면 클릭 범위에 패딩값이 적용된다.
      .clickable(onClick = { /* Ignoring onClick */ })
      .padding(16.dp)    

그렇다면 이제 둘다 적용되는 코드를 짜봅시다.
수정자 목록

@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
  Row(modifier
      .padding(8.dp)
      .clip(RoundedCornerShape(4.dp))
      .background(MaterialTheme.colors.surface)
      .clickable(onClick = { /* Ignoring onClick */ })
      .padding(16.dp)
  ) {
      ...
  }
}

위와 같이 코드를 짜면 16dp만큼 클릭이벤트가 커지며, 8dp 만큼의 여백이 생깁니다.
총 24dp만큼 패딩이 주어지는것을 알수 있습니다.

🤦‍♂️ Slot API

이번 주제는 Slot API 인데, 컴포넌트의 목적과도 같습니다.
만약 버튼이 있고, 버튼에 아이콘을 주고싶습니다.

//XML식 코딩을 한다면 이렇게 나오지 않을까? 하지만 잘못된 코드.
Button(
  text = "Button",
  icon: Icon? = myIcon,
  textStyle = TextStyle(...),
  spacingBetweenIconAndText = 4.dp,
  ...
)

그런데 만약 아이콘에 보라색깔을 입히고 싶고, Icon이 두개 들어간다면, 거기에 대한 매개변수가 필요합니다. 이것이 XML에 대한 한계입니다.

그래서 구글에서는 slot을 만들기로 결심했습니다.
빈공간을 남겨두고, 끼워 넣는식으로 말이죠.

TopAppBar(
              title = {
                  Text(text = "LayoutsCodelab")
              },
              navigationIcon = {
                  IconButton(onClick = { /* doSomething() */ }) {
                      Icon(Icons.Filled.Create, contentDescription = null)
                  }
              },
	    //액션 자체에는 RowScope가 내장되어있어, 가로로 표현된다.
              actions = {
                  IconButton(onClick = { /* doSomething() */ }) {
                      Icon(Icons.Filled.Favorite, contentDescription = null)
                  }
                  IconButton(onClick = { /* doSomething() */ }) {
                      Icon(Icons.Filled.ArrowForward, contentDescription = null)
                  }
              }

          )

이렇게 슬롯에는 Icon, Text가 들어가도 상관없습니다.

🐱‍🏍 Scaffold

Compose에서는 Material 기본 디자인 구성(Topbar, bottomBar, Floating button)을 구현하기 위해서
최상위 레벨의 레이아웃 구현체인 scaffold를 제공합니다.


상당히 많은 Slot를 갖고있는 Scaffold입니다.
topBar,bottomBar,floationActionButtonOptional한 경우이나, 본문에 필요한 Content를 넣기 위해서는 Padding값을 적용해야합니다.


//Content에 패딩값을 적용해야 값이 제대로 나옵니다. 
Scaffold(){ innerPadding ->
BodyContent(Modifier.padding(innerPadding))}

Modifier의 Chaining
Modifier는 .을 이어붙여서 계속 연결이 가능합니다. 이것을 Chaining이라고 하는데,
체이닝을 시도 못할경우

then을 사용해서 이어붙이는게 가능합니다.

Lazy List

저번 시간에 배운것의 연장선입니다.
우리가 List를 띄우고 싶을때 RecycleVIew를 썼었습니다.
안드로이드의 메모리에는 한계가 있기때문에 20개만 보여주고 나머지는 View를 재활용하기 때문에 아무리 많은 데이터를 넣어도 OutOfMemory가 나오지 않습니다.

이코드를 보자. Column에서 Text를 1000개 만듭니다.
형태는 리스트 형태꼴로 나오지만 OutOfMemory가 출현할것이다.
ListView의 형태를 그대로 답습하는 코드이기 때문입니다.

@Composable 
//1000개를 그려준다.
fun SimpleList() { 
//스크롤을 위해 필요하다.
    val scrollState = rememberLazyListState()
	 Column(Modifier.verticalScroll(scrollState))  { 
	repeat(1000) { Text("Item $it") } 
	} 
}

Recycle형식으로 만들려면 Lazy를 붙여서 만듭니다.
그러면 1000개든 2000개든 표현할수 있습니다.

@Composable
fun LazyList() {
  LazyColumn() {
      items(100 0) {
          Text("Item #$it")
      }
  }
}

이제 버튼을 클릭하면 해당 스크롤 위치로 이동 해볼것 입니다.
스크롤을 클릭할때 scrollState.animateScrollToItem으로 원하는값을 주면
이동되는데 이동될때 Rendering을 UI에서 하면 Block될 가능성이 있기때문에
coroutineScope안에서 호출해야합니다.
Compose에서 coroutine을 호출하는것은 rememberCoroutineScope
을 사용하여 만들며 해당 composable과 동일한 Lifecycle을 지닌다.

val listSize = 100
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
// We save the coroutine scope where our animated scroll will be executed
val coroutineScope = rememberCoroutineScope()
Row {
  Button(onClick = {
// suspend때문에 coroutineScope.launch에서 실행되야 합니다.
      coroutineScope.launch {
          // 0 is the first item index
          scrollState.animateScrollToItem(0)
      }
  }) {
      Text("Scroll to the top")
  }
  Button(onClick = {
      coroutineScope.launch {
          // listSize - 1 is the last index of the list
          scrollState.animateScrollToItem(listSize - 1)
      }
  }) {
      Text("Scroll to the end")
  }
}

이미지 넣기

이미지를 넣어보자 현재 2021-11-18일 기준으로 Coil이 가장 뛰어난 성능을 보이기때문에
구글에서도 권장합니다.
painter에서 rememberImagePainter의 데이터에 값을 넣는것으로 호출할 수있다.

    Row(verticalAlignment = Alignment.CenterVertically) {

//이미지 함수이다.
      Image(
          painter = rememberImagePainter(
              data = "https://developer.android.com/images/brand/Android_Robot.png"
          ),
          contentDescription = "Android Logo",
          modifier = Modifier.size(50.dp)
      )
      Spacer(Modifier.width(10.dp))
      Text("Item #$index", style = MaterialTheme.typography.subtitle1)
  }

😜 CustomLayout

Compose는 여러개의 composable funcion을 합쳐 하나의 UI가 만들어진다.

layout를 새로 만들기 위해서는 layout function을 호출하여 새로 만들어야 합니다.

   fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

여기서 가장 중요한 점은 Compose UI does not permit multi-pass measurement.
자식에 대한 size는 한번만 측정할 수 있다는 것입니다.

아래의 예제를 확인해봅시다.

CustomLayout 예제

//placeable은 측정된 width와 height를 가진다.
    val placeable = measurable.measure(constraints)  

      // FirstBaseline이 지원하지 않을경우 Error발생
      check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
      // 여기까지 넘어왔다면 FirstBaseline까지의 width와 height를 얻는다.(2)
      val firstBaseline = placeable[FirstBaseline]

이렇게 얻은값이 first Baseline부터 끝선까지의 거리입니다.
이제 값을 얻었다면, 기본적인 패딩값에서, firstBaseline를 빼줍시다.

//값을뺀값 padding에서 (2)를 뺀값으로 (3)이 나온다. 
val placeableY = firstBaselineToTop.roundToPx() -firstBaseline 

//그래서 기존의 값을 합친다.
val height = placeable.height + placeableY

//배치시키면 완료 
layout(placeable.width, height) {
placeable.placeRelative(0, placeableY)
}

이제 하나의 수정자를 Custom했다면, 이제 레이아웃을 Custom해봅시다.

@Composable
fun MyOwnColumn(
//수정자를 호출해야하고, content가 람다형식으로 제공된다.
  modifier: Modifier = Modifier,
  content: @Composable () -> Unit
) {
  Layout(
      modifier = modifier,
      content = content
  ) { measurables, constraints ->
      
	//각각의 레이아웃의 width, height 값을 가져온다.
      val placeables = measurables.map { measurable ->
          measurable.measure(constraints)
      }

      // Track the y co-ord we have placed children up to
      var yPosition = 0

     //반복문을돌려, 해당크기만큼 칸을띄우는것(컬럼형태가 될것이다.)
      layout(constraints.maxWidth, constraints.maxHeight) 
          placeables.forEach { placeable ->
              placeable.placeRelative(x = 0, y = yPosition)
              yPosition += placeable.height
          }
      }
  }
}

사용 방법은 이렇게 사용합니다.

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
//수정자는 주지않아도 된다.
  MyOwnColumn(modifier.padding(8.dp)) {
      Text("MyOwnColumn")
      Text("places items")
      Text("vertically.")
      Text("We've done it by hand!")
  }
}

복잡한 레이아웃

저희는 레이아웃을 커스텀하는 방법을 배웠습니다.
하지만 위에 내용은 Columm이라는 단순한 함수호출을 통해 표현할수 있습니다.
하지만 이런거라면 어떨까요?

width와 height가 일정치 않습니다.

@Composable fun StaggeredGrid(modifier: Modifier = Modifier, 
rows: Int = 3, content: @Composable () -> Unit) {
	Layout(modifier = modifier, content = content) 
	{ measurables, constraints ->

	}
}

일단 구하기 위해서는 전체적인 레이아웃부터 계산해야 합니다.

  • width: child를 순서대로 각 row에 배치 -> 각 row별로 배치된 child의 width값을 모두 합산 -> 합산된 width가 가장 긴 row가 staggered layout의 width가 됨
  • height: child를 순서대로 각 row에 배치 -> 각 row별로 배치된 child의 height 중 가장 큰 값을 도출 -> 각 row가 같은 height를 모두 합산한 값이 staggered layout의 height가 됨

    이걸통해 X

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글