안녕하세요! 오늘도 열심히 발전중이신 개발자님들 환영합니다!
저번 글에서는 애니메이션을 넣는방법, 단순한 레이아웃 그리는법을 배웠습니다.
오늘 알아볼것은 심화된 레이아웃 그리기입니다!
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 인데, 컴포넌트의 목적과도 같습니다.
만약 버튼이 있고, 버튼에 아이콘을 주고싶습니다.
//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가 들어가도 상관없습니다.
Compose에서는 Material 기본 디자인 구성(Topbar, bottomBar, Floating button
)을 구현하기 위해서
최상위 레벨의 레이아웃 구현체인 scaffold
를 제공합니다.
상당히 많은 Slot
를 갖고있는 Scaffold
입니다.
topBar
,bottomBar
,floationActionButton
는 Optional
한 경우이나, 본문에 필요한 Content를 넣기 위해서는 Padding값을 적용해야합니다.
//Content에 패딩값을 적용해야 값이 제대로 나옵니다.
Scaffold(){ innerPadding ->
BodyContent(Modifier.padding(innerPadding))}
Modifier의 Chaining
Modifier
는 .을 이어붙여서 계속 연결이 가능합니다. 이것을 Chaining이라고 하는데,
체이닝을 시도 못할경우
then
을 사용해서 이어붙이는게 가능합니다.
저번 시간에 배운것의 연장선입니다.
우리가 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)
}
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 ->
}
}
일단 구하기 위해서는 전체적인 레이아웃부터 계산해야 합니다.
이걸통해 X