Android/UI(XML)

Android 그림자 살펴보기

MJ핫산 2022. 10. 13. 03:23

🌚 그림자 그거 그냥 넣으면 되는거 아닌가? ㅋㅋ

프로젝트를 하다보면 디자이너들이 화면에 그림자를 적용해달라고 하는 경우가 꽤 많다.

 

그런데 그림자를 코드로 커스텀할 수 있는 iOS와 달리 안드로이드에서는 elevation 속성을 사용해서 깊이감을 주는 것 외에는 그림자를 따로 커스텀할 수 있는 방법이 없어서 보통 9-patch 이미지를 만들어서 사용한다. ( ɵ̥̥ ˑ̫ ɵ̥̥)

iOS와 Android 그림자 적용 방법 비교


참고) iOS & Android: UI 디자인 - DesignCompass



9-patch 이미지가 하나의 리소스로 여러 사이즈의 View에 대응할 수 있다는 장점 때문에 주로 사용되는 것 같은데, 단점도 있다. 그림자로 표시될 부분의 간격 조절에 신경을 많이 써야하고, 또 View 마다 이미지를 뒤에 깔아주는 것이기 때문에 오버드로가 발생한다.

 

그래서 이번에 진행하는 새 프로젝트에서는 안드로이드의 기본 옵션들을 활용해서 그림자를 적용해보려고 했더니 해결해야할 문제가 몇가지 있었다.

1️⃣ 1번째 문제. 안드로이드는 그림자를 커스텀 할 수 없다.

앞에서 말했던 것처럼 안드로이드는 그림자를 커스텀 할 수 없다.
아래 사진은 이전 프로젝트에서 만들었던 화면인데, 흰색 카드들은 9-patch를 사용해서 그림자를 만들었고, 왼쪽 아래 챗봇 아이콘은 elevation으로 그림자를 만들었다.

디자이너들이 원한 그림자는 흰색 카드들처럼 연한 색인데, elevation은 너무 경계가 생기는 느낌...

2️⃣ 2번째 문제. 아래로 갈 수록 그림자가 진해진다.

두번째는 스크롤이 아래로 갈수록 그림자가 진해진다는 것이다. 자세히 안보면 모를 수도 있지만 A와 E를 비교했을 때 확실히 그림자의 차이가 나는 것을 알 수 있다.

...
이런 문제들을 해결하기 위해서 안드로이드의 그림자에 대해 조금 더 자세히 알아보기로 했다. =͟͟͞͞(๑•̀ㅁ•́ฅ✧

👀 시작하기 전에.. 그림자는 왜 필요할까?

더 나은 앱을 만드려면 Material Design이 제시하는 가이드 라인을 따르는 것이 좋은데, 그 Material Design의 가장 큰 특징 중 하나가 3차원 공간으로 빛과 그림자를 포함하고 있다는 것이다.

3차원 공간이란?

Material Design의 특징이 3차원 공간이라고 했는데, 3차원 공간이란 뭘까? 정리하자면 모든 오브젝트들이 x, y, z 값을 가지고 있는 것이다.
특히 z축은 디스플레이 평면에서 수직으로 정렬되며 양의 z축은 뷰어를 향해 확장된다. 그리고 Material 환경에서 모든 객체의 두께는 1dp이다.

깊이(Depth)란?

깊이는 사용자 인터페이스에서 중요한 수준을 정의하는 수단으로 사용된다.
책상에 올려둔 종이들에도 깊이(depth)가 있다. 얇은 종이 위에 또 다른 종이를 올려놓으면 우리의 눈은 그 종이들에 깊이가 있음을 인지하는 것과 같다.

모든 요소는 각각 우선 순위를 가지고 있는데, 이 스크린을 예시로 살펴보자.
우선 구성은 아래와 같이 되어있다.

 

이 화면에서 Card들은 리사이클러뷰 안에서 스크롤 될 것이므로 맨 아래 레이어다. 그 다음이 App bar, 그리고 마지막(맨 위에 있는) 레이어가 Floating Action Button 이라고 볼 수 있다.

지금처럼 View들에 대해 사용자가 순위나 깊이를 느낄 수 있게 만드려면 어떻게 해야할까? 바로 z-value를 조절하는 것이다.

그리고 버튼, 카드, 다이얼로그 등 안드로이드에 있는 많은 위젯들의 깊이를 결정할 때 역시 Material Design 가이드를 참고하면 좋을 것 같다. ㅎㅎ

 

z-value 란?

방금 말한 View의 z-value는 두가지로 구성되어 있다.

  • elevation(정적)
    정적인 z값으로 변하지 않는다. Press 등의 z축 애니메이션을 발생시키려면 translationZ를 사용해야한다.
  • translationZ(동적)
    버튼을 눌렀을 때 그림자가 커지는건 elevation이 변하는게 아니라 translationZ가 변하는 것이다. 안드로이드 stateListAnimator를 사용해서 View의 translationZ값을 변경할 수 있다.

z-value = elevation + translationZ 이다. 만약 여러개의 View가 겹쳐진다면 이런 식으로 계산된다.

 

Pressed / Resting

translationZ에 대해 조금 더 자세히 알아보기 위해 버튼이 눌리는 모습을 관찰해보자.
높이가 커지면서 그림자도 더 커지는 것을 관찰할 수 있다.

 

이게 바로 translationZ 값이 변하기 때문인데, 예를 들어서 FAB는 기본적으로 elevation이 6dp, translationZ가 0dp이다. 그런데 버튼을 누르면(Pressed) elevation은 그대로 6dp이지만 translationZ가 0dp -> 6dp로 변하기 때문에 그림자가 더 커지는 것이다. 반대로 버튼을 놓으면(Resting) translationZ가 다시 6dp -> 0dp로 변하면서 그림자도 줄어든다.

 

이상 depth, z-value 등 z축과 관련된 것들을 알아봤다. 다음은 3차원을 표현하는 요소 중 하나인 light(빛)에 대해 알아보자!

안드로이드에서의 Light

이제 Material Design은 거의 마지막이다..ㅎㅎ
그림자가 있으려면 빛이 있어야 하는데, 안드로이드의 빛은 어디서 비춰지고 있는걸까?

 

안드로이드에는 두개의 빛이 있다. 맨 위에 있는 조명이 key-light고 밑에 있는 것이 ambient-light다. 안드로이드에서 보이는 그림자는 이 두 빛의 조합으로 보여지는 것이다.

 

!! 여기서 앞에서 말했던 문제들 중에 두번째 문제가 해결된다!

🧩 아래로 갈 수록 그림자가 진해진다 - 해결방법!

아래로 갈 수록 그림자가 진해졌던 이유는 바로 옆에서 비추는 key-light의 각도 때문이었다.
이걸 수정하는 방법은 다음과 같다.

<!-- No.1 -->
<style name="Theme.Shadows" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="android:ambientShadowAlpha">1</item>
        <item name="android:spotShadowAlpha">0</item>
</style>

android:spotShadowAlpha를 0으로 만들어서 빛을 없애는(?) 것이다. 그러면 그림자가 일정하게 보인다. (아래 사진 참고)

반대로 android:ambientShadowAlpha를 0으로 하면 두번째 사진처럼 보인다.

<!-- No.2 -->
<style name="Theme.Shadows" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="android:ambientShadowAlpha">0</item>
        <item name="android:spotShadowAlpha">1</item>
</style>

그리고 이 둘의 값을 적당히 조절하면 그냥 elevation을 적용하는 것보다 훨씬 자연스러운 그림자를 얻을 수 있다. (•̀ᴗ•́)و ̑̑

<!-- No.3 -->
<style name="Theme.Shadows" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <item name="android:ambientShadowAlpha">0.02</item>
        <item name="android:spotShadowAlpha">0.03</item>
</style>

 

🧩 안드로이드는 그림자를 커스텀 할 수 없다 - 해결 방법!

위에서 한 것처럼 android:ambientShadowAlpha, android:spotShadowAlphaelevation 값을 적절히 조절하면 원하는 그림자를 얻을 수 있다. 아래 사진은 실제 프로젝트에서 해당 값들을 조절해서 만든 그림자다.

 

🎨 API 28 이상이라면 색상도 커스텀 가능!

그리고 API 28 이상이라면 그림자의 색상도 커스텀 할 수 있다.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    binding.rootView.outlineAmbientShadowColor = Color.parseColor([colorCode])
    binding.rootView.outlineSpotShadowColor = Color.parseColor([colorCode])
}

 

🧱  화면 넘어서도 그림자가 생기도록 하고 싶다면

app:cardUseCompatPadding="true"
app:cardPreventCornerOverlap="false"

 

🔗 참고