티스토리 뷰
이번에 작업한 UI 출처입니다.
@Composable
fun GameScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(getGameBlack())
) {
LazyColumn(
contentPadding = PaddingValues(bottom = 70.dp),
modifier = Modifier.fillMaxWidth()
) {
/** 베너 영역 **/
item { GameHeader() }
/** 메인 컨텐츠 **/
item { GameBody() }
}
/** 하단바 **/
GameFooter(modifier = Modifier.align(Alignment.BottomCenter))
}
}
1) 베너 영역
@OptIn(ExperimentalPagerApi::class)
@Composable
fun GameHeader() {
val state = rememberPagerState()
val bannerList = listOf(
R.drawable.img_game_banner1,
R.drawable.img_game_banner2,
R.drawable.img_game_banner3
)
val titleList = listOf("SUPER MARIO", "KIRBY", "DIGIMON SURVIVE")
Box(
modifier = Modifier
.fillMaxWidth()
.height(370.dp)
) {
/** 베너 이미지 HorizontalPager **/
HorizontalPager(
count = 3,
state = state
) { index ->
BannerItem(bannerList[index], titleList[index])
}
/** Indicator **/
HorizontalPagerIndicator(
pagerState = state,
activeColor = Color.White,
inactiveColor = getGameGray(),
modifier = Modifier
.padding(bottom = 60.dp, end = 17.dp)
.align(Alignment.BottomEnd)
)
LaunchedEffect(state.currentPage) {
delay(3000)
val newPosition = (state.currentPage + 1) % state.pageCount
state.animateScrollToPage(newPosition)
}
}
}
@Composable
fun BannerItem(imageRes: Int, title: String) {
Box(modifier = Modifier.fillMaxWidth()) {
Image(
painter = painterResource(id = imageRes),
contentDescription = "banner",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(370.dp)
)
/** 베너 하단 어둡게 처리 **/
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(
Brush.verticalGradient(
listOf(Color.Transparent, getGameBlack())
)
)
.align(Alignment.BottomCenter)
)
/** 타이틀 **/
Text(
text = title,
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
modifier = Modifier
.padding(start = 10.dp, bottom = 20.dp)
.align(Alignment.BottomStart)
)
/** 게임 정보 **/
Row(
horizontalArrangement = Arrangement.spacedBy(18.dp),
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
.padding(start = 10.dp, bottom = 2.dp)
) {
Text(text = "10+", fontSize = 14.sp, color = getGameGray())
Text(text = "2022", fontSize = 14.sp, color = getGameGray())
Text(text = "Children,Puzzle", fontSize = 14.sp, color = getGameGray())
}
}
}
베너 영역은 Pager
사용이 메인인데 이건 바로 이전 글에 작성하였으므로 생략하겠습니다.
2) 메인 컨텐츠
메인 컨텐츠는 다시 텝 영역과 컨텐츠 영역으로 나뉩니다.
우선 컨텐츠부터 보면 두 페이지로 나뉘게 됩니다.
원래 디자인에서는 좀 더 내용이 있지만 중요한 부분도 아니고 해서…생략을 했습니다.
2-1) 컨텐츠
Crossfade(
targetState = state.value,
animationSpec = tween(durationMillis = 500, easing = LinearEasing)
) {
if (it == "RECOMMEND") {
GameRecommend()
} else {
GameRanking()
}
}
두 화면은 Crossfade를 이용해서 약간 잔상이 보이며 화면이 전환되는 효과를 만들어 봤습니다.
추가로 티는 잘 안 나겠지만 두 번째 화면에 그러데이션 효과를 주었습니다.
원래 디자인에서도 바깥으로 가면서 약간 밝아지는 효과가 있어서 비슷하게 만들어봤습니다.
Box(
modifier = Modifier
.padding(top = 20.dp + iconSize / 2)
.height(cardHeight)
.fillMaxWidth()
.clip(RoundedCornerShape(7.dp))
.background(
brush = Brush.radialGradient(
colors = listOf(cardColor, Color.White),
radius = 1100f
)
)
)
그러데이션의 종류는 여러 가지가 있는데 이건 다른 포스팅에서 다루도록 하겠습니다.
저는 이번에는 방사형 그러데이션이 필요로 하였습니다.
Modifier.background
의 속성 중 brush
에 Brush.radialGradient
를 설정합니다.
colors
에 원하는 색상들의 값을 넣어주고 radius
의 값을 설정하여 원의 크기를 지정합니다.
이번 UI
에서는 눈에 띄게 그러데이션이 보이는 게 아니어서 radius
값을 크게 줘서 그러데이션이 있는 듯 없는 듯 주었습니다.
2-2) 탭
탭에서는 두 가지 애니메이션을 적용시켰습니다.
첫 번째는 아이템을 선택할 때 Border
영역이 선택한 아이템 영역으로 움직이는 애니메이션입니다.
두 번째는 텍스트 색상이 채워지는 애니메이션입니다.
Box(
modifier = Modifier
.padding(horizontal = 14.dp)
.fillMaxWidth()
.height(36.dp)
.border(1.dp, getGameGray(), RoundedCornerShape(6.dp))
)
/** Recommend, Ranking Tab **/
Row(
modifier = Modifier
.padding(horizontal = 14.dp)
.height(36.dp)
.fillMaxWidth()
) {
RecommendButton(
isSelected = state.value == "RECOMMEND",
modifier = Modifier.weight(1f)
) {
state.value = "RECOMMEND"
}
RankingButton(
isSelected = state.value == "RANKING",
modifier = Modifier.weight(1f)
) {
state.value = "RANKING"
}
}
val alignmentState by animateAlignmentAsState(if (state.value == "RANKING") Alignment.TopEnd else Alignment.TopStart)
Box(
modifier = Modifier
.padding(horizontal = 14.dp)
.height(36.dp)
.fillMaxWidth(0.5f)
.align(alignmentState)
.border(
border = BorderStroke(1.dp, Color.White),
shape = RoundedCornerShape(6.dp)
)
) ...
}
우선 최상위 Box
에서 전체 테두리를 지정해 주었습니다.
Row
를 이용하여 탭을 구현하고 하단에 Box
를 이용해서 선택된 Item
의 Boader
를 구현하였습니다.
animateAlignmentAsState
를 이용해서 아이템이 선택되었을 때 해당 아이템이 있는 Alignment
를 지정하여 이동하게 하였습니다.
@OptIn(ExperimentalTextApi::class)
@Composable
fun RecommendButton(
isSelected: Boolean,
modifier: Modifier = Modifier,
onClickListener: () -> Unit
) {
val maxValue = 430f
val startAnimation = animateFloatAsState(
targetValue = if (isSelected) 0f else maxValue,
animationSpec = tween(durationMillis = 500)
)
Box(
modifier = modifier
.height(36.dp)
.clickable {
onClickListener()
}
.background(Color.Transparent)
) {
Text(
text = "RECOMMEND",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = getGameGray(),
modifier = Modifier.align(Alignment.Center)
)
Text(
text = "RECOMMEND",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = getGameGray(),
style = TextStyle(
brush = Brush.horizontalGradient(
colors = listOf(getGameBlue(), getGamePurple()),
startX = startAnimation.value,
endX = maxValue,
tileMode = TileMode.Decal
)
),
modifier = Modifier.align(Alignment.Center)
)
}
}
우선 Text에 그러데이션이 있는데 이걸 구현하는 방법은 여러가지가 있겠지만 조사한 것들 중 간단한건 2가지였습니다.
modifier = Modifier.align(Alignment.Center)
.graphicsLayer(alpha = 0.99f)
.drawWithCache {
val brush = Brush.horizontalGradient(listOf(getGameBlue(), getGamePurple()))
onDrawWithContent {
drawContent()
drawRect(brush, blendMode = BlendMode.SrcAtop)
}
}
drawWithCahe
를 이용하여 텍스트 위에 그러데이션을 그리고 graphicsLayer
를 설정하여 텍스트 위에만 보이게 설정합니다.
사실 이 부분은 아직 학습 전이여서 정확하게 이해를 못 했습니다.
graphicsLayer
관련해서는 학습하고 포스팅하도록 하겠습니다. 그래서 이번에는 이걸 사용하지 않았습니다!
TextStyle(
brush = Brush.horizontalGradient(
colors = listOf(getGameBlue(), getGamePurple()),
startX = startAnimation.value,
endX = maxValue,
tileMode = TileMode.Decal
)
두 번째 방식은 TextStyle
을 이용하는 방법입니다.
하지만 TextStyle
에 brush
를 넣는 방법은 아직 테스트 단계이므로 언제 바뀔지는 알 수 없습니다.
horizontalGradient
효과를 줄 때 시작점과 종료점을 지정할 수 있는데 이를 이용하여 색상 채우는 애니메이션을 적용하였습니다.
아쉽게도 정확하게 Text
의 크기를 알아내는 방식을 찾지 못해서 하드코딩으로 진행하게 되었습니다.
val maxValue = 430f
val startAnimation = animateFloatAsState(
targetValue = if (isSelected) 0f else maxValue,
animationSpec = tween(durationMillis = 500)
)
endX
를 Text
의 가장 오른쪽에 배치해 두고 startX
의 시작점이 선택 여부에 따라 선택되면 0 아니면 가장 오른쪽에 위치합니다.
그러므로 선택이 되었을 때 오른쪽에서 왼쪽으로 채워지게 되고 다른 아이템을 선택했을 때에는 왼쪽에서 오른쪽으로 색상이 지워지게 됩니다.
다른 버튼에서는 방향이 반대로 설정하면 됩니다.
@OptIn(ExperimentalTextApi::class)
@Composable
fun RankingButton(
isSelected: Boolean,
modifier: Modifier = Modifier,
onClickListener: () -> Unit
) {
val maxValue = 300f
val startAnimation = animateFloatAsState(
targetValue = if (isSelected) maxValue else 0f,
animationSpec = tween(durationMillis = 500)
)
Box(
modifier = modifier
.height(36.dp)
.clickable {
onClickListener()
}
.background(Color.Transparent)
) {
Text(
text = "RANKING",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = getGameGray(),
modifier = Modifier.align(Alignment.Center)
)
Text(
text = "RANKING",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = getGameGray(),
style = TextStyle(
brush = Brush.horizontalGradient(
colors = listOf(getGameBlue(), getGamePurple()),
startX = 0f,
endX = startAnimation.value,
tileMode = TileMode.Decal
)
),
modifier = Modifier.align(Alignment.Center)
)
}
}
3) 하단 바
@Composable
fun GameFooter(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.background(getGameLightBlack())
) {
Box(
modifier = Modifier
.size(82.dp, 57.dp)
.background(getGameDarkGray())
.align(Alignment.CenterEnd)
) {
Icon(
painter = painterResource(id = R.drawable.ic_search),
contentDescription = "search",
modifier = Modifier
.padding(start = 25.dp)
.size(34.dp)
.align(Alignment.Center)
)
}
Box(
modifier = Modifier
.padding(start = 15.dp)
.clip(CircleShape)
.background(getGameBlue())
.size(30.dp)
.align(Alignment.CenterStart)
)
Row(
horizontalArrangement = Arrangement.spacedBy(70.dp),
modifier = Modifier.align(Alignment.Center)
) {
ConstraintLayout {
val (text, box) = createRefs()
Text(
text = "HOME",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.constrainAs(text) {
start.linkTo(parent.start)
top.linkTo(parent.top)
}
)
Box(
modifier = Modifier
.constrainAs(box) {
start.linkTo(text.start)
end.linkTo(text.end)
top.linkTo(text.bottom, 4.dp)
width = Dimension.fillToConstraints
height = Dimension.value(3.dp)
}
.background(Color.White, RoundedCornerShape(3.dp))
)
}
Text(
text = "FIND",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = getGameGray()
)
}
Image(
painter = painterResource(id = R.drawable.img_game_footer),
contentDescription = "footer",
contentScale = ContentScale.Crop,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 37.dp)
.height(57.dp)
)
}
}
하단 바는 곡선 부분을 구현해 볼까 싶었는데 Canvas
쓰는 것 외에는 생각이 나질 않아서 그냥 이미지로 대체해서 구현하였습니다.
'안드로이드 > 디자인' 카테고리의 다른 글
Compose 디자인 (0) | 2022.08.24 |
---|
- Total
- Today
- Yesterday
- Pokedex
- Worker
- Compose BottomSheet
- Compose Naver Map
- column
- Duplicate class found error
- 안드로이드
- compose
- Compose ModalBottomSheetLayout
- Row
- Compose MotionLayout
- Fast api
- WebView
- Compose BottomSheetScaffold
- Compose 네이버 지도 api
- Compose BottomSheetDialog
- Compose 네이버 지도
- LazyColumn
- 웹뷰
- Compose ConstraintLayout
- Duplicate class fond 에러
- Compose QRCode Scanner
- 안드로이드 구글 지도
- Retrofit
- Gradient
- WorkManager
- Android Compose
- 포켓몬 도감
- Kotlin
- Android
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |