앱에 스플래시 스크린 추가하기

이번 영상에서는 앱에 스플래시 스크린을 설정하는 법을 알아보도록 하겠습니다.

Splashscreen API

아이콘을 선택하고 앱이 구동되면 메인화면을 보여주기 전까지 로딩 시간이 필요한데 그 시간동안 보여주는 화면을 스플래시 스크린이라고 합니다. 기존에는 스플래시 스크린 기능이 안드로이드에 존재하지 않아서 임의로 기능을 구현할 필요가 있었지만 이제 스플래시 스크린 기능이 정식 API로 추가되었기에 API 사용법을 소개하도록 하겠습니다.

Android 12에서는 앱에 스플래시 스크린을 표시할 수 있는 SplashScreen Androidx API가 도입되었습니다. 기존에는 API가 없어서 아이콘 하나를 표시하는 테마를 만들고 그 테마를 SplashActivity에 적용한 뒤, 앱 시작시에 SplashActivity를 실행하는 방식으로 스플래시 스크린을 직접 구현해야했지만 이제는 구글의 공식 API를 통해 스플래시 스크린을 사용할 수 있게 된 것입니다. 개인적인 생각으로는 도입 시기가 늦은 감이 있어 보이지만 어쨌든 공식 API를 사용할 수 있게 된 것은 환영할 일입니다.

스플래시 스크린을 사용하면 유저가 앱을 바로 사용하지 못하고 우선 로고를 봐야하는 불편함이 있기 때문에 원래는 안티패턴으로 취급되었습니다. 하지만 앱이 조금씩 거대해지면서 초기로딩에 소요되는 시간도 점차 길어지게 되었고 그렇다면 어차피 로딩중에 백지화면을 띄우느니 그 동안 로고라도 표시하는게 지연시간을 더 효율적으로 사용하는 것이란 의견이 강해지게 되었습니다. 중구난방으로 작성되던 스플래시 스크린의 구현방법을 통일하고 표현방법도 일관되게 정리할 수 있게 되었으니 구글로서도 API를 개발할만한 충분한 동기가 있었을 것 같습니다.

API specification

App startup time

앱은 콜드 스타트, 웜 스타트, 핫 스타트라는 세 가지 상태 중 하나에서 시작됩니다. 각 상태의 정의는 다음과 같습니다.

  • 콜드 스타트 : 앱의 프로세스가 실행되고 있지 않음
  • 웜 스타트 : 액티비티가 생성되지 않음
  • 핫 스타트 : 액티비티가 백그라운드에 있음

출처: https://developer.android.com/topic/performance/vitals/launch-time

기존의 방법으로는 스타트 상태를 구분할 수 없어 스플래시 스크린 화면을 일괄적으로 표시하였지만, 스플래시 스크린 API는 콜드 스타트와 웜 스타트 시에만 스플래시 스크린을 표시하고 핫 스타트 시에는 표시하지 않게 하여 앱을 빠르게 로딩할 수 있도록 합니다.

Splashscreen Icon

스플래시 스크린에 사용할 아이콘은 다음 사항에 따라 준비되어야 합니다.

출처: https://developer.android.com/guide/topics/ui/splash-screen

  1. 앱 아이콘은 벡터 드로어블이어야 하고 정지 혹은 애니메이션 어느쪽이든 상관없음. 애니메이션인 경우 1초를 넘지 않도록 권장하며 기본값은 런처 아이콘이 사용됨
  2. 아이콘 백그라운드는 옵션이며 적응형 아이콘을 사용할 경우 아이콘과 배경 사이에 충분한 콘트라스트가 있을 경우 배경이 표시됨
  3. 적응형 아이콘을 사용할 경우 포그라운드의 1/3은 가려짐
  4. 배경은 단일 색상으로 구성됨

출처: https://developer.android.com/guide/topics/ui/splash-screen

  • 브랜드 이미지: 200×80 dp여야 합니다.
  • 아이콘 배경이 있는 앱 아이콘: 240×240 dp이고 지름이 160 dp인 원 안에 맞아야 합니다.
  • 아이콘 배경이 없는 앱 아이콘: 288×288 dp이고 지름이 192 dp인 원 안에 맞아야 합니다.

API 적용하기

단순 아이콘 표시하기

우선 API 사용을 위해 디펜던시를 추가합니다.

1
implementation "androidx.core:core-splashscreen:1.0.0"

스플래시 아이콘으로 사용할 벡터 아이콘을 drawable 폴더에 추가합니다. 아이콘은 ellisonchan/SplashScreen 저장소의 ic_kotlin_hero_new.xml 파일을 사용하도록 하겠습니다.

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="640dp"
    android:height="640dp"
    android:viewportWidth="640"
    android:viewportHeight="640">
    <path
        android:pathData="M322.565,171.76l-145.45,152.975l0,136.725l145.225,-145.5l144.475,-144.2z"
        android:strokeLineJoin="miter"
        android:strokeWidth="1"
        android:fillType="nonZero"
        android:strokeLineCap="butt">
        <aapt:attr name="android:fillColor">
            <gradient
                android:startY="422.17175"
                android:startX="144.278"
                android:endY="189.88942"
                android:endX="376.56033"
                android:type="linear">
                <item android:offset="0.1075" android:color="#FFC757BC"/>
                <item android:offset="0.2138" android:color="#FFD0609A"/>
                <item android:offset="0.42540002" android:color="#FFE1725C"/>
                <item android:offset="0.6048" android:color="#FFEE7E2F"/>
                <item android:offset="0.74300003" android:color="#FFF58613"/>
                <item android:offset="0.8232" android:color="#FFF88909"/>
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:pathData="M177.115,461.46l145.225,-145.5l144.475,145.5z"
        android:strokeLineJoin="miter"
        android:strokeWidth="1"
        android:fillType="nonZero"
        android:strokeLineCap="butt">
        <aapt:attr name="android:fillColor">
            <gradient
                android:startY="504.0033"
                android:startX="279.2872"
                android:endY="381.63876"
                android:endX="401.65176"
                android:type="linear">
                <item android:offset="0.09677" android:color="#FF0095D5"/>
                <item android:offset="0.3007" android:color="#FF238AD9"/>
                <item android:offset="0.6211" android:color="#FF557BDE"/>
                <item android:offset="0.8643" android:color="#FF7472E2"/>
                <item android:offset="1" android:color="#FF806EE3"/>
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:pathData="M177.115,171.76l145.45,0l-145.45,152.975z"
        android:strokeLineJoin="miter"
        android:strokeWidth="1"
        android:fillType="nonZero"
        android:strokeLineCap="butt">
        <aapt:attr name="android:fillColor">
            <gradient
                android:startY="261.53333"
                android:startX="163.82558"
                android:endY="154.57826"
                android:endX="270.78058"
                android:type="linear">
                <item android:offset="0.09677" android:color="#FF0095D5"/>
                <item android:offset="0.3007" android:color="#FF238AD9"/>
                <item android:offset="0.6211" android:color="#FF557BDE"/>
                <item android:offset="0.8643" android:color="#FF7472E2"/>
                <item android:offset="1" android:color="#FF806EE3"/>
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:pathData="M430.415,395.435c-2.35,-2.35 -2.325,-6.175 0.025,-8.525s6.175,-2.325 8.5,0.025c2.35,2.35 2.325,6.175 -0.025,8.5C436.59,397.81 432.765,397.785 430.415,395.435M383.515,348.26c-2.35,-2.35 -2.325,-6.175 0.025,-8.525c2.35,-2.35 6.175,-2.325 8.5,0.025c2.35,2.35 2.325,6.175 -0.025,8.5S385.84,350.61 383.515,348.26M457.665,371.41l23.25,-6.15c1.325,-0.35 2.125,-1.725 1.775,-3.05c-0.35,-1.325 -1.725,-2.125 -3.05,-1.775l-23.55,6.25c-3.55,-9.6 -9.25,-18.7 -17.125,-26.6c-7.875,-7.925 -16.925,-13.675 -26.5,-17.275l6.375,-23.525c0.35,-1.325 -0.425,-2.7 -1.75,-3.075c-1.325,-0.35 -2.7,0.425 -3.075,1.75l-6.3,23.225c-22.525,-6.725 -47.325,-2 -66.3,13.95l101.925,102.5C459.39,418.735 464.265,393.96 457.665,371.41"
        android:strokeLineJoin="miter"
        android:strokeWidth="2"
        android:fillColor="#3DDC84"
        android:fillType="nonZero"
        android:strokeLineCap="butt"/>
</vector>

다음으로 스플래시 스크린에 적용할 테마를 설정합니다. themes.xml 내부에 Theme.SplashScreen을 상속받는 테마를 추가하고 설정값을 다음과 같이 변경하면 됩니다. 예를 들어 백그라운드 색상을 지정하고 싶으면 windowSplashScreenBackground 속성을 사용하면 됩니다.

1
2
3
4
5
6
7
8
<style name="Theme.App.Splash" parent="Theme.SplashScreen">
    // 백그라운드 색상
    <item name="windowSplashScreenBackground">@color/purple_200</item>
    // 스플래시 종료 후 표시될 액티비티의 테마
    <item name="postSplashScreenTheme">@style/Theme.MyApplication</item>
    // 표시할 아이콘
    <item name="windowSplashScreenAnimatedIcon">@drawable/ic_kotlin_hero_new</item>
</style>

그리고 이 테마를 AndroidManifest의 application 테마로 지정합니다.

1
2
3
4
5
6
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application 
        android:theme="@style/Theme.App.Splash">
        ...        
    </application>
</manifest>

그리고 MainActivity에서 setContentView가 수행되기 전에 installSplashScreen을 실행하면 스플래시 스크린이 표시됩니다.

1
2
3
4
5
6
7
8
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

+        installSplashScreen()
        setContentView(R.layout.activity_main)
    }
}

스플래시 화면을 장시간 표시하기

스플래시 스크린 표시가 종료되는 타이밍은 MainActivity 화면이 처음으로 그려지기 시작할 때 입니다. 만약 스플래시 스크린의 표시 시간을 더 길게 하고 싶다면ViewTreeObserver.OnPreDrawListener를 사용하여 isReadytrue일때 앱이 시작되도록 할 수 있습니다. 여기서는 5초 후에 isReady 플래그를 true로 바꾸도록 했습니다.

 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
31
32
33
34
class MainActivity : AppCompatActivity() {
+    private var isReady = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        installSplashScreen()
        setContentView(R.layout.activity_main)

+        thread(start=true) {
+            for (i in 1..5) {
+                Thread.sleep(1000)
+            }
+            isReady = true
+        }

+        val content: View = findViewById(android.R.id.content)
+        content.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    // Check if the initial data is ready.
+                    return if (isReady) {
+                        // The content is ready; start drawing.
+                        content.viewTreeObserver.removeOnPreDrawListener+(this)
+                        true
+                    } else {
+                        // The content is not ready; suspend.
+                        false
+                    }
+                }
+            }
+        )
    }
}

아이콘 탈출 효과 주기

Android 12(API 31) 이상의 기기에서는 스플래시 아이콘에 탈출 효과를 줄 수도 있습니다. 다음 코드는 스플래시 스크린이 종료되면 아이콘을 화면 위쪽으로 탈출시키는 애니메이션을 보여주는 예제입니다. setOnExitAnimationListener를 사용하여 스플래시 스크린의 종료를 체크하고 ObjectAnimator로 애니메이션을 정의하면 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequiresApi(Build.VERSION_CODES.S)
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Add a callback that's called when the splash screen is animating to
    // the app content.
    splashScreen.setOnExitAnimationListener { splashScreenView ->
        // Create your custom animation.
        val slideUp = ObjectAnimator.ofFloat(
            splashScreenView,
            View.TRANSLATION_Y,
            0f,
            -splashScreenView.height.toFloat()
        )
        slideUp.interpolator = AnticipateInterpolator()
        slideUp.duration = 200L

        // Call SplashScreenView.remove at the end of your custom animation.
        slideUp.doOnEnd { splashScreenView.remove() }

        // Run your animation.
        slideUp.start()
    }
}

이렇게 해서 Splashscreen API를 이용해 앱에 스플래시 스크린을 설정하는 법에 대해 알아보았습니다.

Built with Hugo
Theme Stack designed by Jimmy