Empty Compose Activity를 만들면
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Android_JetPack_03_Compose_SAA_starterTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Greeting("Android 좋은 듯 하다")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Android_JetPack_03_Compose_SAA_starterTheme {
Greeting("Android!!!!")
}
}
위와 같은 액티비티가 기본적으로 생성된다.
Android_JetPack_03_Compose_SAA_starterTheme
은 Theme.kt에 저장된 theme 대로 설정을 해주고
setContent부터가 뷰를 구성하는 부분이다.
text요소를 구성할 수 있는 속성들은 대표적으로 아래와 같은 것들이 있다.
Text(
modifier = Modifier.width(200.dp),
color = Color.Red,
text = "color",
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Cursive,
letterSpacing = 10.sp,
maxLines = 2,
textDecoration = TextDecoration.Underline,
textAlign = TextAlign.Justify
)
참고로 fontSize나 letterSpacing과 같은 요소들을 sp가 아닌 dp로 설정하면 오류가 난다.
@Composable
fun Greeting(onButtonClicked: () -> Unit) {
Button(
onClick = onButtonClicked,
enabled = true,
border = BorderStroke(width = 10.dp, color = Color.Magenta),
shape = RoundedCornerShape(1.dp),
contentPadding = PaddingValues(100.dp)
) {
Icon(
imageVector = Icons.Filled.Send,
contentDescription = "send" // icon의 의미를 적어줌
)
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
Text(text = "send")
}
}
Button은 위와 같은 대표적인 속성들이 존재하고 생성자로 onClicked에 들어갈 함수, enabled 여부 등을 받을 수 있다.
contentPadding
은 요소의 padding 값으로 특정 상하좌우를 명시하지 않으면 전체를 의미한다.
onButtonClicked에 주려는 함수는
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePracticeTheme {
// A surface container using the 'background' color from the theme
Surface(
color = MaterialTheme.colorScheme.background
) {
Greeting(onButtonClicked = {
Toast.makeText(this, "send clicked!", Toast.LENGTH_SHORT).show()
})
}
}
}
}
}
간단히 onCreate단에서 위와 같이 줄 수 있다.
button 자체의 color는 modifier에서 background 값으로 주는 것이 아닌,
color 생성자에 값을 넣어서 준다.
colors = ButtonDefaults.buttonColors(
contentColor = Color.Yellow,
containerColor = Color.Green
)
(modifier의 background를 red로 주면 이런식으로 요소를 감싸는 부분만 빨강이 된다.)
Modifier 는 Surface, Text 등의 대부분의 UI 컴포넌트가 선택적으로 허용하는 매개변수로
상위 컴포넌트 레이아웃에서 UI 요소가 배치되고 표시되는 동작방식을 지정한다.
Surface는 Modifier의 역할과 비슷하게 배경색, 테두리 등을 설정할 수 있는 UI 요소이다.
특히나 elevation
을 주기 위해 surface를 사용하곤 한다.
Surface로 원하는 뷰를 감싼 다음 아래와 같은 속성들을 주면 된다.
@Composable
fun Greeting() {
Surface(
shadowElevation = 10.dp,
modifier = Modifier.padding(5.dp),
border = BorderStroke(1.dp , Color.Magenta)
) {
Text("text text" , modifier = Modifier.padding(1.dp) )
}
}
물론 이러한 border와 같은 속성들을 Text 자체에서 Modifier를 줘서 구성할 수도 있긴 하다.
Text("text text" , modifier = Modifier.padding(5.dp)
.border(BorderStroke(1.dp, Color.Magenta)))
하지만 각종 이유에서 surface를 사용하여 UI를 구성하는게 부작용이 덜하고 가독성이 좋다고 한다.
Column - Y축 정렬
Row - X축 정렬
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Greeting("Android 좋은 듯 하다!!12222")
Greeting("Android 좋은 듯 하다!!2")
Greeting("Android 좋은 듯 하다!!33")
Greeting("Android 좋은 듯 하다!!444")
}
정렬 기준은 alignment, 배치는 (xml의 chain과 유사) Arrangement로 설정을 해준다.
예를 들어
horizontalArrangement = Arrangement.SpaceBetween
는 컴포넌트 간의 간격을 최대로 주면서 정렬하는 방식이다.
framelayout처럼 겹치는 것을 허용하는 컴포넌트이다.
Box(
modifier = Modifier
.background(color = Color.Green)
.width(300.dp)
.height(300.dp)
) {
Text("Hello", fontSize = 24.sp)
Text("nice to meet you", modifier = Modifier.align(Alignment.BottomEnd))
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopEnd) {
Text("Hello", fontSize = 24.sp, fontStyle = FontStyle.Italic)
}
}
BoxWithConstraints는 Box의 기능을 모두 포함하되 width와 height의 min, max 값을 줄 수 있다.
@Composable
fun outer(){
Column(){
inner(modifier = Modifier.widthIn(min = 100.dp).heightIn(min = 30.dp, max = 100.dp))
}
}
@Composable
fun inner(modifier: Modifier = Modifier){
BoxWithConstraints(modifier) {
Text("maxW ${maxWidth} maxH ${maxHeight} minW ${minWidth} minH $minHeight")
}
}
inner 컴포넌트에서 modifier를 인자로 받아 이를 사용하는 outer에서 Modifier로 widthIn
, heightIn
값을 주었다.
인자로 받을 것 없이 바로 BoxWidthConstraints에서 Modifier.WidthIn
으로 값을 줘도 된다.
기본적으로 min,maxHeight는 따로 지정하지 않은 경우에 화면에 따라 다른 dp 값으로 나온다. 따라서 서로 다른 종류의 휴대폰 화면에 대응하기 위해 boxWithConstraints가 유용하게 사용될 수 있다.
Elevation을 주기 위해 주로 사용하는 컴포넌트이다.
Card(
elevation = CardDefaults.cardElevation(100.dp),
) {
AsyncImage(
model = "https://dfstudio-d420.kxcdn.com/wordpress/wp-content/uploads/2019/06/digital_camera_photo-1080x675.jpg",
contentDescription = "url 이미지",
contentScale = ContentScale.Crop,
modifier = Modifier.clip(RoundedCornerShape(4)),
)
}
Checkbox 요소는 체크되어있는지 안되어있는지에 따라 동적으로 체크 표시를 하도록 구성해줘야 한다. State 설명 포스트를 확인하자.
var checked by remember{ mutableStateOf(false) }
Checkbox(
checked = checked,
onCheckChange = {
checked = !checked
}
)
Material3(혹은 2)를 사용하는 compose에서 TextField는
기본적으로
TextField(value = "ddd", onValueChange = {})
OutlinedTextField(value = "outlined", onValueChange = {})
의 두가지를 볼 수 있는데, 각각 filledTextField와 OutlinedTextField 이다.
또한 Material 디자인이 아닌 직접 커스텀을 위해 순정 디자인을 쓰려면
BasicTextField
을 사용하면 된다.
하지만 이만으로는 유저에 입력에 따른 UI 수정이 기존의 editText처럼 되지 않는다.
var text by remember{ mutableStateOf("start text") }
Column {
TextField(value = text , onValueChange = {text = it})
Canvas는
drawLine (Color.Red, Offset(x: 30f, y: 10f), Offset(x: 50f, y: 40f))
drawCircle(Color.Yellow, radius: 10f, Offset ( x: 15f, y: 40f))
drawRect(Color .Magenta, Offset(x: 30f, y: 30f), Size( width: 10f, height: 10f))
등의 함수가 있어 안에 특정 좌표에 특정 도형을 그릴 수 있다.
이미지를 화면에 띄우는 방법은 3가지가 있다.
Image(painter = painterResource(id = R.drawable.image), contentDescription = "샘플이미지 입니다", modifier = Modifier.height(500.dp))
Image(imageVector = Icons.Filled.Settings, contentDescription = "세팅")
//Image(bitmap = , contentDescription = )
painter는 일반적인 drawable 리소스를 가져올 때 사용한다.
imageVector는 아이콘 등의 vector asset을 가져올 때 사용한다.
bitmap은 비트맵 객체로 가져올 때 사용하는데, 카메라 로직 관련하여 쓸 일이 더러 있다.
Glide를 주요 사용하는 XML 방식에 비해 compose는 Coil 라이브러리를 이미지 렌더링을 위해 주요 사용하는 것으로 보인다.
이를 위해 의존성을 추가해줘야 한다.
implementation 'io.coil-kt:coil:2.2.2'
implementation 'io.coil-kt:coil-compose:2.2.2'
이미지를 로드하기 위해서는
rememberImagePainter
라고 하는 painter 객체 컴포넌트를 사용한다. Coil 라이브러리를 사용하는 이미지 렌더링 요소이다. 이 안에 사용하려는 이미지 url을 넣어준다.
그리고 이 painter를 Image 안에 넣어준다.
val painter = rememberImagePainter(
data = "https://dfstudio-d420.kxcdn.com/wordpress/wp-content/uploads/2019/06/digital_camera_photo-1080x675.jpg"
)
Image(painter, contentDescription = "url 이미지")
이 역시 Coil 라이브러리에서 지원하는 이미지 요소로, 더욱 간단히 네트워크 url 이미지를 받아올 수 있다.
AsyncImage(
model = "https://dfstudio-d420.kxcdn.com/wordpress/wp-content/uploads/2019/06/digital_camera_photo-1080x675.jpg",
contentDescription = "url 이미지",
contentScale = ContentScale.Crop,
placeHoler = ColorPainter(Color(0x33000000))
)
다른 레퍼런스들을 보았을 때 대부분 모두 AsyncImage를 주로 사용하는 것으로 보인다.
compose에서 contstraintlayout 을 쓰는 일이 일반적인 것은 아니지만, 사용할 수 있다.
implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.1'
이를 위해 dependency를 추가해준다
@Composable
fun ConstraintLayoutEx() {
ConstraintLayout(Modifier.fillMaxSize()) {
val (redBox, greenBox, magentaBox, yellowBox) = createRefs()
Box(modifier = Modifier
.size(40.dp)
.background(Color.Red)
.constrainAs(redBox){
bottom.linkTo(parent.bottom, margin = 8.dp)
end.linkTo(parent.end, margin=4.dp)
}) {
}
Box(modifier = Modifier
.size(40.dp)
.background(Color.Magenta)
.constrainAs(magentaBox){
start.linkTo(parent.start)
end.linkTo(parent.end)
}) {
}
Box(modifier = Modifier
.size(40.dp)
.background(Color.Green)
.constrainAs(greenBox){
centerTo(parent)
}) {
}
Box(modifier = Modifier
.size(40.dp)
.background(Color.Yellow)
.constrainAs(yellowBox){
start.linkTo(magentaBox.end)
top.linkTo(magentaBox.bottom)
}) {
}
}
}
간단한 constraintlayout 예시이다.
createRefs는 요소들을 변수화 해야 할 때 사용하는 함수이고, constrainAs
에서 어떤 레퍼런스인지를 지정해준다.
constraint 지정 방식은 xml과 크게 다르지 않다.
@Composable
fun ConstraintLayoutEx() {
val constraintSet = ConstraintSet{
val redBox = createRefFor("redBox")
val greenBox = createRefFor("greenBox")
val magentaBox = createRefFor("magentaBox")
val yellowBox = createRefFor("yellowBox")
constrain(redBox){
bottom.linkTo(parent.bottom, 10.dp)
end.linkTo(parent.end, 10.dp)
}
}
ConstraintLayout(
constraintSet,
Modifier.fillMaxSize()) {
Box(modifier = Modifier
.size(40.dp)
.background(Color.Red)
.layoutId("redBox"))
}
}
ConstraintSet
을 만들어 각각의 요소에 layoutId를 만들어주고, constrain을 해준다.
내가 만든 constraintSet을 이제 ConstraintLayout의 인자로 지정해주고, 안의 Box의 Id를 내가 적은 id로 연결해준다.
Chain은 xml의 constraint chain처럼 수직, 수평적으로 요소를 연결해서 간격을 일정하게 하는 등의 제약을 주기 위해 사용하고,
Barrier는 Barrier로 묶인 요소들의 최상, 최하, 가장 start, 가장 end 지점을 가져와서 다른 요소들이 이에 대한 제약을 걸 수 있도록 한다.
@Composable
fun ConstraintLayoutEx() {
ConstraintLayout(Modifier.fillMaxSize()) {
val (redBox, greenBox, magentaBox, yellowBox,text) = createRefs()
Box(modifier = Modifier
.size(40.dp)
.background(Color.Red)
.constrainAs(redBox){
}) {
}
Box(modifier = Modifier
.size(40.dp)
.background(Color.Magenta)
.constrainAs(magentaBox){
}) {
}
Box(modifier = Modifier
.size(40.dp)
.background(Color.Green)
.constrainAs(greenBox){
}) {
}
Box(modifier = Modifier
.size(40.dp)
.background(Color.Yellow)
.constrainAs(yellowBox){
}) {
}
createHorizontalChain(redBox, yellowBox, magentaBox, greenBox, chainStyle = ChainStyle.Spread)
val barrier = createBottomBarrier(redBox, yellowBox, magentaBox, greenBox)
Text("barrier!!!!", modifier = Modifier.constrainAs(text){
top.linkTo(barrier)
}, fontSize = 30.sp)
}
}
Barrier를 createBottomBarrier(redBox, yellowBox, magentaBox, greenBox)로 만들어서 가장 요소의 아래쪽을 의미하게 하였고, 그것을 constraintTop으로 하여 text의 constraint 요소를 추가하였다.
@Composable
fun Greeting5(name: String) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first"){
composable("first"){
FirstScreen(name = "firstscreen", navController)
}
composable("second"){
SecondScreen(name = "second screen", navController)
}
// parameter로 넘어오는 값은 /{} 형태로 표현한다. “라우트 이름/전달할 데이터”
composable("third/{text}"){ navBackStackEntry ->
ThirdScreen(
name = "third screen",
navController,
text = navBackStackEntry.arguments?.getString("text") ?: ""
)
}
}
}
@Composable
fun FirstScreen(name: String, navController: NavController) {
Column{
Text("첫번째 화면:$name")
Button(onClick = {
navController.navigate("second")
}) {
Text("다음화면으로")
}
}
}
@Composable
fun SecondScreen(name: String, navController: NavController) {
val (text, setText) = remember {
mutableStateOf("")
}
Column{
Text("두번째 화면:$name")
Button(onClick = {
//뒤로가기 둘다 동일.
navController.navigateUp()
// navController.popBackStack()
}) {
Text("뒤로가기")
}
Spacer(modifier = Modifier.padding(8.dp))
OutlinedTextField(value = text, onValueChange = setText)
Spacer(modifier = Modifier.padding(8.dp))
Button(onClick = {
//text가 비어있지 않으면 이동한다. 값을 가져가야 하므로 /하위로 전달.“라우트 이름/전달할 데이터”
if(text.isNotEmpty()){
navController.navigate("third/$text")
}
}) {
Text("세번째 화면으로 가기")
}
}
}
@Composable
fun ThirdScreen(name: String, navController: NavController, text:String) {
Column{
Text("세번째 화면:$name")
Spacer(modifier = Modifier.padding(8.dp))
Text("받은 값 :$text")
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview7() {
Compose_Theme {
Greeting5("Android")
}
}
웹 path 처럼 인자를 전달해 줄 수도 있다.
화면 구성에 필요한 TopBar, BottomBar, SnackBar, FAB, Drawer 등 기본구성이 적용된 틀
@Composable
fun MyApp3(content: @Composable () -> Unit) {
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
val scope = rememberCoroutineScope()
Compose_Theme {
// A surface container using the 'background' color from the theme
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text("TopAppBar") },
backgroundColor = MaterialTheme.colors.primary
)
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("clicked..")
}
}) {
Text("X")
}
},
drawerContent = { Text(text = "drawerContent")},
content = {
content()
},
bottomBar = { BottomAppBar(backgroundColor = MaterialTheme.colors.primary) { Text("BottomAppBar") } }
)
}
}