작업을 진행하던 중, 일반적인 앱바와는 다른 양상의 디자인을 마주하게 되었다. 앱바에 아이콘을 넣는 지극히 평범한 작업으로 보였지만... 일반적으로 해오던 방식으로 하는 것은 어림도 없음을 몰랐다...
특정 게시글의 썸네일을 앱바의 배경으로 설정해야 하는 상황에서,
단색 아이콘을 사용하면 사진에 따라 아이콘이 보이지 않는 경우가 발생한다.
그 이유로 아이콘에 shadow 효과를 달라는 요구사항을 받았고,
평소처럼 svg 형태로 저장하여 xml로 변환해서 써야겠다는 안일한 생각으로 작업을 진행하였다.
@Composable
fun IconBtn(
iconId: Int,
onClickIcon: () -> Unit
) {
IconButton(onClick = {
onClickIcon()
}) {
Icon(
painter = painterResource(id = iconId),
contentDescription = "icon_button"
)
}
}
@Preview
@Composable
fun IconBtnPreview() {
val iconId = R.drawable.ic_back_arrow
IconBtn(
iconId = iconId,
onClickIcon = {}
)
}
① 흰색 아이콘을 가져왔음에도 검은색으로 표출
② 그림자 효과가 전혀 나타나지 않음
이 문제의 원인은,
R.drawable.ic_back_arrow
를Icon
이라는 Composable을 활용하여 표출하는 데 있다.
androidx.compose.material3
의 Icon.kt
파일을 살펴보면,@Composable
fun Icon(
imageVector: ImageVector,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current // default tint
) {
Icon(
painter = rememberVectorPainter(imageVector),
contentDescription = contentDescription,
modifier = modifier,
tint = tint
)
}
tint라는 매개변수가 LocalContentColor.current
라는 기본값으로 설정되어 있음을 확인할 수 있다.
LocalContentColor.current
는 대체 어떤 것인가?Icon.kt 파일에서 LocalContentColor를 타고 들어가면 다음과 같은 설명을 확인해볼 수 있다.
val LocalContentColor = compositionLocalOf { Color.Black }
CompositionLocal containing the preferred content color for a given position in the hierarchy. This typically represents the on color for a color in ColorScheme. For example, if the background color is ColorScheme.surface, this color is typically set to ColorScheme.onSurface.
This color should be used for any typography / iconography, to ensure that the color of these adjusts when the background color changes. For example, on a dark background, text should be light, and on a light background, text should be dark.
Defaults to Color.Black if no color has been explicitly set.
요약하자면, LocalContentColor는 현재 Surface 배경의 색에 따른 Content(Text/Icon)의 색깔을 지정하기 위한 변수로, 이 값은 특별히 명시된 바가 없는 한 Black으로 지정된다.
✔ 결국, Icon Composable은 LocalContentColor 값에 대한 별다른 설정이 없는 한 tint를 Black으로 상정하기 때문에, 어떠한 색깔의 Icon Drawable을 가져오더라도 기본적으로 검정색 아이콘이 나오게 된 것이다.
이 문제에 대한 원인을 찾는 데에는 상당히 오랜 시간이 걸렸고, 그 과정에서 많은 삽질도 있었다. 하지만 언제나 그랬듯 원인은 항상 허무한 데에서 나오는 법이었다.
Figma에서 그림자를 포함한 아이콘을 svg 형태로 Export하고,
안드로이드 스튜디오의 Vector Asset을 활용하여 xml 파일로 변환하는 과정에서 유심히 보지 않았던 이상한 경고가 눈에 띄었다.
The image may be incomplete due to encountered issues
issues
를 눌러보니, 다음과 같은 에러들을 확인할 수 있었다.
ERROR @ line 7: <filter> is not supported
ERROR @ line 8: <feFlood> is not supported
ERROR @ line 9: <feColorMatrix> is not supported
ERROR @ line 10: <feOffset> is not supported
ERROR @ line 11: <feGaussianBlur> is not supported
ERROR @ line 12: <feComposite> is not supported
ERROR @ line 13: <feColorMatrix> is not supported
ERROR @ line 14: <feBlend> is not supported
ERROR @ line 15: <feBlend> is not supported
@Composable
fun IconBtn(
iconId: Int,
onClickIcon: () -> Unit
) {
IconButton(onClick = {
onClickIcon()
}) {
Icon(
painter = painterResource(id = iconId),
contentDescription = "icon_button",
tint = Color.White // tint를 원하는 색상으로 변경
)
}
}
🤔 작성자의 경우, 그림자 효과가 없는 아이콘 svg 파일을 이용하였다.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="18dp"
android:viewportWidth="10"
android:viewportHeight="18">
<path
android:pathData="M8.751,18C8.674,18 8.598,17.971 8.539,17.912L0.263,9.636C-0.088,9.285 -0.088,8.714 0.263,8.363L8.539,0.088C8.656,-0.029 8.846,-0.029 8.963,0.088C9.081,0.205 9.081,0.395 8.963,0.512L0.687,8.788C0.57,8.905 0.57,9.095 0.687,9.212L8.963,17.488C9.081,17.605 9.081,17.795 8.963,17.912C8.905,17.971 8.828,18 8.751,18Z"
android:fillColor="#ffffff"/>
</vector>
<layer-list>
태그를 활용하여, 원하는 그림자 효과를 구현한다.ic_back_arrow.xml
의 내용을 기반으로, <layer-list>
내부에 여러 <item>
을 두어 아이콘 부분과 그림자 부분이 겹쳐서 나타나도록 한다.
이 때 주의할 점은, 그림자 위에 아이콘이 나타나야 하기 때문에 그림자 <item>
부분이 앞부분에 오도록 하고 그 뒤에 아이콘에 대한 <item>
을 구현해야 한다는 것이다.
그림자 구현에 핵심이 되는 태그는 <group>
으로,
여기서는 그림자의 모양이 아이콘의 모양과 같기 때문에 원본 아이콘의 기본적인 틀을 가져가되, <group>
으로 <path>
을 묶어 offset을 설정할 수 있도록 하였다.
원래 위치에서 아래로 내리고자 했기 때문에, android:translateY
속성을 활용하였다. 만약 좌우로도 이동시키고 싶다면 android:translateX
속성을 활용한다.
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 그림자 -->
<item>
<vector
android:width="11dp"
android:height="18dp"
android:viewportWidth="11"
android:viewportHeight="18">
<group android:translateY="2">
<path
android:pathData="M2.34,8.99L9.843,16.462C9.965,16.583 10.024,16.731 10.021,16.904C10.018,17.077 9.944,17.235 9.801,17.378C9.658,17.521 9.504,17.593 9.338,17.593C9.173,17.593 9.018,17.521 8.875,17.378L1.298,9.801C1.181,9.684 1.094,9.556 1.038,9.417C0.983,9.278 0.955,9.136 0.955,8.99C0.955,8.824 0.983,8.676 1.038,8.548C1.094,8.42 1.181,8.297 1.298,8.18L8.885,0.561C9.006,0.439 9.161,0.373 9.348,0.362C9.535,0.352 9.7,0.418 9.843,0.561C9.965,0.704 10.026,0.864 10.026,1.04C10.026,1.216 9.965,1.365 9.843,1.487L2.34,8.99Z"
android:fillColor="#80000000"/>
</group>
</vector>
</item>
<!-- 아이콘 -->
<item>
<vector
android:width="11dp"
android:height="18dp"
android:viewportWidth="11"
android:viewportHeight="18">
<path
android:pathData="M2.34,8.99L9.843,16.462C9.965,16.583 10.024,16.731 10.021,16.904C10.018,17.077 9.944,17.235 9.801,17.378C9.658,17.521 9.504,17.593 9.338,17.593C9.173,17.593 9.018,17.521 8.875,17.378L1.298,9.801C1.181,9.684 1.094,9.556 1.038,9.417C0.983,9.278 0.955,9.136 0.955,8.99C0.955,8.824 0.983,8.676 1.038,8.548C1.094,8.42 1.181,8.297 1.298,8.18L8.885,0.561C9.006,0.439 9.161,0.373 9.348,0.362C9.535,0.352 9.7,0.418 9.843,0.561C9.965,0.704 10.026,0.864 10.026,1.04C10.026,1.216 9.965,1.365 9.843,1.487L2.34,8.99Z"
android:fillColor="#ffffff"/>
</vector>
</item>
</layer-list>
컴파일 에러가 발생하지 않아, IconBtn을 구현한 코드는 변경하지 않았다. 하지만 이것이 또 다른 재앙을 불러올 줄은 몰랐다...
Caused by: java.lang.IllegalArgumentException: Only VectorDrawables and rasterized asset types are supported ex. PNG, JPG
painterResource(id)
라는 함수를 사용하였는데, 이 함수의 id로 들어갈 수 있는 파일은 VectorDrawable이다. <vector>
에서 <layer-list>
로 변경되었고, 더 이상 VectorDrawable이 아니게 되므로 오류가 발생한 것이다.@Composable
fun XmlDrawableToBitmap(
@DrawableRes resourceId: Int
): ImageBitmap? =
ContextCompat.getDrawable(
LocalContext.current,
resourceId
)?.toBitmap()?.asImageBitmap()
@Composable
fun IconBtn(
iconBitmap: ImageBitmap?,
onClickIcon: () -> Unit,
modifier: Modifier = Modifier
) {
IconButton(
modifier = modifier,
onClick = { onClickIcon() }
) {
iconBitmap?.let {
Icon(
bitmap = it,
contentDescription = "icon_button",
tint = Color.White
)
}
}
}
@Composable
fun XmlDrawableToBitmap(
@DrawableRes resourceId: Int
): ImageBitmap? =
ContextCompat.getDrawable(
LocalContext.current,
resourceId
)?.toBitmap()?.asImageBitmap()
@Preview
@Composable
fun IconBtnPreview() {
val iconId = R.drawable.ic_back_arrow
val iconBitmap = XmlDrawableToBitmap(resourceId = iconId)
IconBtn(
iconBitmap = iconBitmap,
onClickIcon = {},
modifier = Modifier
)
}