오늘은 아래의 영상처럼 Splash Screen에 애니메이션을 적용해 볼 것이다. 

6초짜리 만드는데 3시간 걸린 거 실화냐? 가슴이 웅장해진다..

 

1. themes.xml에 아래 코드 추가

    <!-- splash screen -->
    <style name="Theme.FortuneWheel.Starting" parent="Theme.SplashScreen" >
        <item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/white</item>
        <item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/avd_splash</item>
        <item name="windowSplashScreenAnimationDuration">1500</item>
        <item name="postSplashScreenTheme">@style/Theme.FortuneWheel</item>
    </style>

 

2. drawable 폴더 안에 avd_splash.xml 파일을 만들어준다. 이 파일은 AnimatedVectorDrawable이다.

<?xml version="1.0" encoding="utf-8"?>
<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_launcher_splash">
    <target
        android:name="rotationGroup"
        android:animation="@animator/rotation"
        />

</animated-vector>

 

3. drawable 폴더 안에 ic_launcher_splash.xml 파일을 만들어준다.

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="240dp"
    android:height="240dp"
    android:viewportWidth="240"
    android:viewportHeight="240">
    <group
        android:name="rotationGroup"
        android:pivotX="120.0"
        android:pivotY="120.0">
        <path
            android:pathData="M120,120m-80,0a80,80 0,1 1,160 0a80,80 0,1 1,-160 0"
            android:fillColor="#FF648D"/>
        <path
            android:pathData="M120,120m-72.07,0a72.07,72.07 0,1 1,144.15 0a72.07,72.07 0,1 1,-144.15 0"
            android:fillColor="#FFA3BE"/>
        <path
            android:pathData="M188.58,120C188.58,82.12 157.88,51.42 120,51.42C107.96,51.42 96.13,54.59 85.71,60.61L120,120L188.58,120Z"
            android:fillColor="#FFC3D3"/>
        <path
            android:pathData="M85.71,60.61C64.49,72.86 51.42,95.5 51.42,120C51.42,144.5 64.49,167.14 85.71,179.39L120,120L85.71,60.61Z"
            android:fillColor="#FFE7E9"/>
        <path
            android:pathData="M85.71,179.39C96.13,185.41 107.96,188.58 120,188.58C157.88,188.58 188.58,157.88 188.58,120L120,120L85.71,179.39Z"
            android:fillColor="#ffffff"/>
        <path
            android:pathData="M120,120m-11.2,0a11.2,11.2 0,1 1,22.4 0a11.2,11.2 0,1 1,-22.4 0"
            android:fillColor="#FF648D"/>
        <path
            android:pathData="M120,120m-7.33,0a7.33,7.33 0,1 1,14.65 0a7.33,7.33 0,1 1,-14.65 0"
            android:fillColor="#ffffff"/>
    </group>
</vector>

 

 

4. animator 폴더 안에 rotation.xml 파일을 만들어준다.

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:startOffset="300"
    android:duration="1000"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="360"
    android:valueType="floatType"
    android:interpolator="@android:anim/anticipate_overshoot_interpolator"/>

 

5. AndroidManifest.xml에서 Application theme (또는 첫 Activity의 Theme)를 @style/Theme.FortuneWheel.Starting 로 변경한다.

걱정하지 마라.

Splash Screen이 끝나고 나면 Theme.FortuneWheel.Starting에서 postSplashScreenTheme로 설정한 테마로 변경된다.

 

 

6. 첫번째 액티비티 (일반적으로는 MainActivity)의 onCreate 메소드에 아래 코드를 넣어준다.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
		//(필수)setContentView 이전에 설정해줘야 함
        installSplashScreen()
		
        //(선택)splashScreen이 사라질 때의 애니메이션을 주고 싶다면 아래 코드 입력
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            splashScreen.setOnExitAnimationListener { splashScreenView ->
                val anim = ObjectAnimator.ofFloat(splashScreenView, View.ALPHA, 1f, 0f)
                anim.duration = 1000
                anim.doOnEnd { splashScreenView.remove() }
                anim.start()
            }
        }

        setContentView(binding.root)

 

 


아래는 나의 삽질 일기다.

 

1. Splash screen의 icon에 애니메이션이 안된다?

=> 적용한 Theme의 windowSplashScreenAnimatedIcon에 넣어준 drawable이 AnimatedVectorDrawable이나  AnimationDrawable인지 확인하기 (단, AnimationDrawable 넣어줬을 때도 애니메이션이 되는지는 확인 못함)

=> 적용한 Theme이 Theme.SplashScreen을 상속받았는지 확인하기

 

2. Splash screen의 icon이 잘린다?

=> 전체 크기 240dpx240dp에 아이콘은 160dpx160dp가 되도록 이미지를 만들어야 한다.

=> 만약에 오른쪽같은 이미지 밖에 없어요 라고 하면.. vector 파일(ic_launcher_splash.xml)의 group에 scaleX와 scaleY를 설정해서 크기를 조절해준다.

 

3. Splash screen의 icon의 애니메이션이 안 끝났는데 화면이 이동해요

=> Theme.FortuneWheel.Starting의 windowSplashScreenAnimationDuration를 애니메이션 길이와 같거나 길도록 맞춰주세요.

 

4. Splash screen의 icon의 애니메이션이 너무 빨리 시작해요

=> rotation.xml에 startOffset 속성을 주세요

 

 


 

 

참고 자료 :

https://developer.android.com/develop/ui/views/launch/splash-screen

https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable

왜 이 글을 적게 되었는가..

 

팝업에서 ChipGroup을 사용할 일이 생겨서 열심히 코딩하고 실행까지 했는데

팝업을 열려고 할 때 갑자기 아래와 같은 오류가 생겼다.

 

The style on this component requires your app theme to be Theme.AppCompat (or a descendant).

 

오 앱 테마가 Theme.AppCompat을 상속받아야 하는가보다..

 

하지만 확인 결과 나는 application theme도 Material 테마였고, App Theme을 AppCompat을 상속받게 하였지만 문제는 나아지지 않았다.

 

그리고 30분째..나는 계속되는 구글링으로 지쳐가고있었고..

 

그러던 중 '혹시'나 해서

 

val dialog = ListDialog(baseContext)

val dialog = ListDialog(this@MainActivity)

로 바꿔봤더니..

 

잘 된다..?

 

도대체 이런 일은 왜 생긴 걸까...? 에서 시작한 오늘의 포스팅..

 

baseContext, applicationContext, 그리고 this@MainActivity는 무엇이 다른가!

 

지금 시작합니다!

 


1. this@MainActivity

가장 먼저, 가장 쉬운 this@MainActivity 먼저 살펴보겠다.

해석해보면 '이거@MainActivity'다.

그렇다!

이건 그냥 this 라는 예약어에다가.. 특별히 'MainActivity 말이야!' 라는 걸 붙여준 것이다.

메인액티비티 내에서 this만 써도 무관한 경우가 있지만, 경우에 따라 this가 꼭 MainActivity를 가리키지 않을 때가 있기 때문이다.

 

그렇다면 이제 this가 무엇인지를 확인해보자.

this는 해당 클래스 내에서 자기 자신의 객체를 반환할 때 쓰는 예약어라고 보면 된다.

 

따라서!

View.OnClickListener() {
	fun onClick(v: View) {
		Toast.makeText(this, "이건 오류남", Toast.LENGTH_SHORT).show()
    }
}

이 경우에는 this가 OnClickListener의 객체를 반환하기 때문에 오류가 난다.

this@MainActivity 처럼 사용하여야 한다.

 

결론 : this@MainActivity는 해당 액티비티의 객체를 반환하는 것이다. Activity는 Context를 상속받기 때문에 context를 대체해서 쓸 수 있는 거고..


2. baseContext

일단 이 녀석도 현재 액티비티 또는 서비스의 context를 반환한다.

'일반적으로' Toast나 Dialog같이 context가 필요한 개체를 만들 때 사용된다.

액티비티나 서비스와 연결되어있기 때문에 해당 액티비티나 서비스가 종료되면 함께 사라진다.

 

사실 이 녀석과 this@MainActivity의 차이를 잘 몰랐다.

그래서 위와 같은 사단이 났던 것..

 

this@MainActivity와 baseContext의 차이는...

현재 액티비티와 관련이 있다! 무적권 현재 액티비티를 넘겨줘야 한다! => this

일반적인 목적이다! 특정 액티비티와 연관이 있는 건 아니다! => baseContext

 

그리고 불러오는 곳도 다르다.

안드로이드 스튜디오에서 baseContext를 어디서 불러오냐 찾아보면

ContextWrapper 클래스에서 불러온다는 걸 알 수 있다.

 

this@MainActivity는 해당 Activity를 반환한다.

Activity는 ContextWrapper를 상속받은 ContextThemeWrapper를 상속받는다.

 

여기서 더 나아가서..

ContextWrapper.getBaseContext()는 open 키워드가 없는 것으로 볼 때 이는 오버라이드가 불가능하다는 것을 알 수 있다.

즉 getBaseContext()는 ContextWrapper단계에서 불러오지, 하위 Activity 등에서 불러오는 게 아니라는 것..

 

그렇기에 테마에 대한 값을 가지고 있지 않기에

AppCompat 테마가 필수인 ChipGroup은

'??님 이거 사용하려면 AppCompat 테마가 필요하다요!!'

하면서 에러를 내뿜었던 것이다.


3. applicationContext

이 녀석은 어플리케이션 단위의 context다. 

특정 액티비티에 국한되지 않은 Singleton Context라고 한다.

수명이 긴 개체에 넣어줘야할 때는 applicationContext를 사용하면 된다. 

예를 들어 글로벌 구성 설정, 애플리케이션 기본 설정 및 시스템 서비스와 같은 애플리케이션 수준 리소스 및 서비스에 대한 액세스를 제공하는 데 사용된다고 한다.

 

baseContext와 this@MainActivity가 특정 개체의 생명주기동안에만 유효하다고 한다면

applicationContext는 애플리케이션이 시작될 때 생성되고 애플리케이션의 전체 수명 주기 동안 활성 상태를 유지한다.

 

그렇기때문에 메모리 누수가 발생할 수 있으며 개발 시 이를 충분히 염두에 두어야 한다.

 


다 적고 났더니 뭔가 큰 걸 놓쳤다는 생각이 든다.

 

그렇다.

 

context가 뭔지에 대해서는 정리하지 않은 것이다.

 

context란? 직역하면 '문맥' 이런 뜻인데..

문맥이라는 건 '서로 이어져 있는 문장의 앞뒤 관계'를 말한다.

 

일반적으로 리소스에 액세스할 때, Activity를 시작할 때, 시스템 서비스에 액세스할 때, 팝업, 토스트 등의 개체를 만들 때 사용하는 클래스라고 보면 된다.

 

뭔가랑 뭔가를 이어줄 때 필요한 녀석이구나 정도로 이해하고 넘어가면 될 것 같다.

 

 

 


참고 :

 

chatGPT

https://mrgamza.tistory.com/197

 

안드로이드 스튜디오 플라맹고 버전을 설치하고...

Gradle 8.0.0으로 변경하고 난 후 자바 버전과 일치하지 않는다는 오류 메세지를 받았음.

 

오류 메세지 : 

'compileDebugJavaWithJavac' task (current target is 1.8) and 'kaptGenerateStubsDebugKotlin' task (current target is 19) jvm target compatibility should be set to the same Java version.

 

Module 단계의 build.gradle로 들어가서 compileOptions와 kotlinOptions를 아래와 같이 바꿔주었다.

(기존에는 VERSION_1_8, 1.8로 되어있었다)

compileOptions {
    sourceCompatibility JavaVersion.VERSION_19
    targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
    jvmTarget = '19'
}

 

추가로 ProjectStructure에 들어가서 Gradle 버전과 Java 버전이 잘 맞게 들어가있나도 확인해주었다.

File-ProjectStructure (단축키 Ctrl+Alt+Shift+S)

 

그래들 버전은 이렇게 확인하면 되고..

 

 

자바 버전은 여기서 확인하면 된다.

 

컴퓨터에 자바 20이 깔려있어도 Gradle JDK에는 17짜리 다운받아서 쓸 수 있음..

 

다운받는 법.. GradleJDK 드롭박스 클릭 후 Download JDK를 클릭한다.

 

 

그런 다음 Version에서

아까 오류 메세지에서 나왔던

'kaptGenerateStubsDebugKotlin' task (current target is 19)

이 숫자(19)를 선택한 다음 아래 Vender에서 아무거나 선택하고 Download를 누른다.

 

 

그런다음 다 OK를 누르고 창을 닫으면 끝..

 

 

 

 

 

 

<참고> 그래들 버전 별 자바 버전(출처 : https://docs.gradle.org/current/userguide/compatibility.html)

 

객체지향 프로그래밍에서 상속은 extends, implements 두 가지 방법으로 이루어진다.

 

일반적인 클래스 상속은 논외로 하고

 

abstract class와 interface에 대해 짧게 정리를 하고자 한다.

 

1. abstract class (추상 클래스)

abstract 키워드를 사용해서 정의할 수 있으며

직접 객체를 만들 수 없고(인스턴스화 불가)

abstract method를 통해 자식 클래스에서 꼭 구현해야할 기능들을 정의할 수 있다.

 

 

2. interface

interface 키워드를 사용해서 정의할 수 있으며

마찬가지로 특정 함수(기능)을 강제하기 위해 사용된다.

 

 

얼핏 보면 abstract class의 abstract method를 쓰면 interface를 쓰지 않아도 된다고 생각할 수 있다.

사실 필자가 그랬다.

하지만 실제로는 interface를 더 많이 쓴다. (물론 사바사이긴 하지만... interface를 이용한 상속을 권장하는 사람이 많았다)

왜 그럴까?

필자의 짧은 견해로는 

① 다중 상속이 가능하다.

② 필요한 기능만 구현 가능하다.

이런 이유에서가 아닐까 싶다.

 

 

예를 들어 abstract class로 '휴대폰' 클래스를 만든다고 하겠다.

abstract method로 call() 이라는, 전화를 걸 수 있는 기능을 정의하였다.

그런데 새로운 모델들에서는 인터넷 접속이 가능하다고 한다. 구형 모델 클래스에서는 불가능하고!

abstract method로 connectInternet() 메소드를 추가하였다.

구형 모델 클래스에는 사용하지않지만 구현을 시켜주었다.

 

이렇듯 abstract class로 구현 시

새로운 기능이 추가될 때 마다 새로운 abstract method를 추가하고

또 자식 클래스에서 사용하지도 않을 method 들을 구현해줘야하는 번거로움이 있다.

 

하지만 interface를 사용한다면

새로운 기능이 추가될 때 그에 맞는 interface를 만들고

실제로 그 기능을 쓰는 자식 클래스에서만 구현하면 된다.

 

abstract class와 interface는 서로를 대체하는 존재가 아니라

아예 쓰는 목적이 다르다고 생각이 된다.

 

abstract => 큰 틀을 잡을 때, 청사진을 그리듯이..

interface => 다중 상속이 필요할 때, 유연한 기능 추가/삭제를 원할 때

 

틀린 점이 있다면 댓글로 알려주시길 :)

'개발' 카테고리의 다른 글

삼성 출처미상 앱 제보  (0) 2024.08.29
안드로이드와 iOS에서의 FCM 수신 문제  (0) 2024.08.29
앞으로의 공부 계획  (0) 2022.12.06

paging3 라이브러리를 적용하면서 발생한 실수 ㅜ 

 

[문제가 된 코드]

lifecycleScope.launch {        
    viewModel.topicFlow.collectLatest {
        topicAdapter.submitData(it)
    }
            
    viewModel.photoFlow(TopicData.TOPIC_IDX_ALL).collectLatest {
        val isFirst = photoAdapter.itemCount == 0
        photoAdapter.submitData(it)
        if(isFirst){
            checkEmpty()
        }
    }
}

[현상]

viewModel.photoFlow().. 부분이 동작하지 않았음.

 

[이유]

스택오버플로우 

You will need different coroutines, since collect() is a suspending function that suspends until your Flow terminates.

 

아래는 단톡방에서 말씀해주신 이유들..

야옹이님 : 계속 수집하기때문에 Blocking되서 아래것이 실행안되는게 아닐까요

이상해씨님 : collect가 suspend function이라서 코루틴 빌더 안에 있어야해요. collect가 suspend function인데 flow가 끝이 없다면 suspend가 끝나지 않겠죠

Rokace님 : Stateflow는 lifecyclescope에 기반하여 데이터의변화를 관측하는 무한루프 쓰레드 하나를 만듭니다.
Stateflow 하나당 쓰레드 하나죠
그래서 collect는 destroy이전까지 동작합니다.

 

 

[개선한 코드]

lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.topicFlow.collectLatest {
                        topicAdapter.submitData(it)
                    }
                }

                launch {
                    viewModel.photoFlow(TopicData.TOPIC_IDX_ALL).collectLatest {
                        val isFirst = photoAdapter.itemCount == 0
                        photoAdapter.submitData(it)
                        if(isFirst){
                            checkEmpty()
                        }
                    }
                }
            }
        }

별도의 코루틴으로 launch 해 주었다.

+ Recent posts