본문 바로가기

Android/Utility

[Android] Notification UI에 대한 몇 가지 변경 방법

728x90

지금까지 필자는 아주 간단하게 고정된 Icon만 들어간 App Push를 구현해 왔었다.

보통 앱 아이콘이 들어가고, Title에 Description에 가끔 추가적인 Large Icon정도로 정말 가장 기본적인 모양의 Push만 사용해 왔다.

하지만 이번에 업무를 진행하면서, push에서 볼 수 있는 Notification에 대한 UI를 커스텀해야 하는 경우가 생겼다.

 

아주 간단하게 수정이 가능한 부분이기는 하지만, 이 UI에 대해서 Custom 하고 적용하는 방법에 대하여 가볍게 작성해보고자 한다.

이번 포스팅에서는 Notification에 대한 모든 코드가 작성된 것이 아닌, UI를 변경하기 위한 부분만 작성했음을 미리 안내하고 글을 작성하겠다.


우선,

기본적으로 Notification 관련하여 Builder들이 전부 구현되어 있다면 다음과 같은 부분은 쉽게 찾을 수 있을 것이다.

 

val notificationBuilder = NotificationCompat.Builder(context, channeId)
    .setSmallIcon(smallIcon)
    .setContentTitle(title)
    .setContentText(description)
    .setContentIntent(pendingIntent)

 

가장 단순한 notification builder 형태이며, 해당 빌더의 결과는

 

https://developer.android.com/develop/ui/views/notifications/build-notification?hl=ko

 

 

이처럼 보이게 된다.

icon과 title, description이 존재하고, pendingIntent 부분은 해당 notification을 클릭했을 때 앱에서 진행할 이벤트를 작성해 주면 된다.

 

여기서, 가장 기본적인 UI 말고 우리가 실제 앱을 사용하면서 더 많이 접하는 UI는 다음과 같을 것이다.

 

https://developer.android.com/develop/ui/views/notifications/build-notification?hl=ko

 

우측에 하나의 이미지가 더 들어가는 케이스인데,

우선 이미지가 아닌 리소스 파일로 가지고 있는 아이콘을 설정하는 경우는 다음과 같다.

 

val notificationBuilder = NotificationCompat.Builder(context, channeId)
    .setSmallIcon(smailIcon)
    .setLargeIcon(largeIcon)
    .setContentTitle(title)
    .setContentText(description)
    .setContentIntent(pendingIntent)

 

간단하게 largeIcon이 추가된 것을 확인할 수 있다.

 

smallIcon과  largeIcon의 위치는 OS 버전마다 다르게 보이고 있긴 하지만, 현재 많은 사용자가 보고 있는 화면으로는 위의 구글 샘플 사진과 비슷할 것이다.

사진이 들어간 부분에 설정한 아이콘이 들어가게 되는데, 이미지인 경우 별도로 선처리를 통해 Bitmap 파일로 변환하는 작업이 필요하다.

해당 부분은 뒤에 기술하도록 하겠다.

 

val notificationBuilder = NotificationCompat.Builder(context, channeId)
    .setSmallIcon(smailIcon)
    .setLargeIcon(largeIcon)
    .setContentTitle(title)
    .setContentText(description)
    .setContentIntent(pendingIntent)
    .setStyle(NotificationCompat.BigTextStyle().bigText(bigStyleText))

 

해당 케이스는 BigTextStyle이 추가된 케이스로, 다음과 같이 보이는 Notification이다

 

https://developer.android.com/develop/ui/views/notifications/build-notification?hl=ko

 

쉽게 볼 수 있는 카카오톡을 보면, 채팅의 내용이 길 경우 우측 상단에 위치하는 화살표를 클릭하게 되면 expanded가 되면서 더 많은 텍스트를 확인할 수 있는 경우가 있다.

일반적인 notification인 경우 한 줄의 description만 보여줄 수 있기 때문에 해당 스타일을 추가함으로써 더 많은 텍스트를 보여줄 수 있는 것이다.

 

이와 마찬가지로, 전달받은 사진을 더 크게 보기 위해서는 BigTextStyle이 아닌 BigPictureStyle을 추가해 주면 된다.

 

val notificationBuilder = NotificationCompat.Builder(context, channeId)
    .setSmallIcon(smailIcon)
    .setLargeIcon(largeIcon)
    .setContentTitle(title)
    .setContentText(description)
    .setContentIntent(pendingIntent)
    .setStyle(NotificationCompat.BigPictureStyle().bigPicture(picture))

 

조금 더 확장해서,

 

val notificationBuilder = NotificationCompat.Builder(context, channeId)
    .setSmallIcon(smailIcon)
    .setContentTitle(title)
    .setContentText(description)
    .setContentIntent(pendingIntent)
    .setLargeIcon(picture)
    .setStyle(NotificationCompat.BigPictureStyle()
        .bigPicture(picture)
        .bigLargeIcon(null))

 

이와 같이 설정하게 된다면 notificiation이 접혀있을 때는 LargeIcon 위치에 전달받은 사진이, 확장시켰을 때는 largeIcon이 사라지고 더 큰 이미지로 보이게 되는 것이다.

 

이때,

이미지를 notification에서 보여주기 위해서는 위처럼 bigPictureStyle에 이미지를 넣거나, icon에 넣어주어야 하는데 이때 항상 bitmap 타입으로 넣어주어야 한다.

하지만, notification을 사용하다 보면 사용자 프로필과 같이 단순히 생각해도 url로 데이터가 내려오는 부분을 보여주는 케이스가 존재하게 되는데 이 경우 어떻게 보여줘야 할까?

 

이미지 url을 다운로드하여서 보여주는 것은 너무나도 비효율적이기 때문에, 당연히 우리가 쉽게 사용할 수 있는 Glide나 Coil을 사용하여 image url을 bitmap으로 변경하고, 해당 값을 통해 notification으로 보여줄 수 있다.

 

필자는 Glide를 사용하였고, 마찬가지로 Coil을 사용하는 경우도 쉽게 찾을 수 있으니 Coil인 경우 찾아보길 바란다.

 

val bitmapImage = Glide.with(context)
    .asBitmap()
    .load(largeImageUrl)
    .submit()
    .get()

 

이와 같이 몇 줄의 코드로 Glide를 통해 imageUrl을 bitmap으로 사용할 수 있게 된다.

 

하지만 실제로 적용하고 push를 날려보면 다음과 같이 thread 관련하여 에러가 발생하면서 앱이 죽게 된다.

 

java.lang.IllegalArgumentException: You must call this method on a background thread

 

즉, Glide를 사용하는 부분의 Thread를 직접 지정해서 변경해주어야 한다는 것이다.

 

여러 가지로 확인해 본 결과, Glide로 호출하는 것 자체는 Thread에서 별다른 문제는 없지만, 이미지를 가져오는 get 부분에서 Thread 관련 이슈가 발생한다.

따라서 다음과 같이 설정하여 bitmap을 가져와서 넣어주면 된다.

 

val bitmapImage = Glide.with(context)
    .asBitmap()
    .load("largeImageUrl")
    .submit()

CoroutineScope(Dispatchers.Main).launch {
    val picture = bitmapImage.get()
        ...
}

 

이런 식으로 CoroutineScope를 사용하게 되면 정상적으로 이미지를 Notification으로 받아서 볼 수 있다.

 

여기서 의문이 들 수 있다.

반드시 Image Load Library를 사용해야만 보여줄 수 있는가?

 

물론 아니다.

 

위의 코드에서 중요한 부분은 URL로 들어오는 데이터를 bitmap으로 변환한다는 부분이다.

그렇다면, 라이브러리를 사용하지 않고 URL을 bitmap으로 변환할 수 있다면 라이브러리 없이도 사용이 가능하다.

그리고, 그 코드는 라이브러리를 사용하는 것과 마찬가지로 아주 간단하다.

 

CoroutineScope(Dispatchers.Main).launch {
    val url = URL(largeImageUrl)
    val bitmap = withContext(Dispatchers.IO) {
        try {
            val input = url.openStream()
            BitmapFactory.decodeStream(input)
        } catch (e: IOException) {
            null
        }
    }
    ...
}

 

이처럼 BitmapFactory를 사용해 데이터를 변경해 준 후에 사용하면 된다.


라이브러리를 사용하나 하지 않으나 똑같이 간단한데, 왜 라이브러리를 사용하는가?라고 생각할 수 있다.

물론 단순하게 사용한다고 생각하면 어느 것을 사용하던 큰 차이는 없다고 생각한다.

 

하지만, 라이브러리를 사용하는 이유로는 간단하게 다양한 이벤트 핸들링이 가능하기 때문이다.

물론 라이브러리가 구현을 해두었을 경우라면 그렇다는 것이지만, 안드로이드에서 image load library 중 그래도 가장 많이 쓰이는 두 개의 라이브러리이기 때문에 다양한 에러 핸들링이 쉽게 가능하다.

 

필자는 Glide를 사용하고, Image Load에 대한 RequestListener를 직접 구현하여 이미지 로드에 실패했을 때의 로직을 별도로 구현하였다. 

리스너를 구현하고, Glide에 적용하는 방법은 다음과 같다.

 

val requestListener: RequestListener<Bitmap> = object : RequestListener<Bitmap> {
    override fun onLoadFailed(
        e: GlideException?,
        model: Any,
        target: Target<Bitmap?>,
        isFirstResource: Boolean,
    ): Boolean {
        ...
        return true
    }

    override fun onResourceReady(
        resource: Bitmap?,
        model: Any,
        target: Target<Bitmap?>,
        dataSource: DataSource,
        isFirstResource: Boolean,
    ): Boolean {
        ...
        return true
    }
}

    ...

Glide.with(context)
    .asBitmap()
    .load(largeImageUrl)
    .circleCrop()
    .listener(requestListener)
    .submit()

 

Glide에서 제공해 주는 RequestListener<Bitmap> 객체를 구현하고, 필수 함수를 override 하여 해당 이벤트가 발생했을 때 내가 원하는 대로 이벤트 핸들링을 해주면 된다.

 

onLoadfailed는 말 그대로 이미지 로드가 되지 않았을 때 처리를, onResourceReady는 성공적으로 데이터를 가져왔을 때의 처리를 해주면 된다.

간단하게 notification에 image를 빼거나, 준비가 되면 notification을 보내거나 등의 이벤트를 넣어주면 된다.


 

마지막으로,

기본 규격의 UI가 아닌 전체를 커스텀한 notification을 만들 수 있는가?

 

물론 가능하다.

안드로이드 공식 문서에서도 다양한 방법이 나와있긴 하지만 전체를 커스텀하는 것은 xml을 사용하여 원하는 UI를 그리고, 그 UI를 적용하는 방식으로 사용이 가능하다.

 

val notificationLayout = RemoteViews(context.packageName, R.layout.layout_small_notification)
val notificationLayoutExpanded = RemoteViews(context.packageName, R.layout.layout_large_notification)

val customNotification = NotificationCompat.Builder(context, channelId)
    .setSmallIcon(smailIcon)
    .setStyle(NotificationCompat.DecoratedCustomViewStyle())
    .setCustomContentView(notificationLayout)
    .setCustomBigContentView(notificationLayoutExpanded)

 

 2개의 layout을 선언했는데, 위는 기본적인 notification의 UI이고, 아래는 화살표를 클릭했을 때 나오는 expanded 상태의 UI이다.

 

각 레이아웃을 설정해 주고, NotificationCompat.DecoratedCustomViewStyle을 설정하여 커스텀한 RemoteView로 설정한다고 선언해 준다.

setCustomContentView와 BigContentView는 이름부터 알 수 있듯이 기본 UI와 expanded UI를 설정하는 부분이다.

각각의 layout에는 TextView만 작성하고 데이터를 넣어두었다.

 

이처럼 설정하고 간단하게 notification을 확인하면 다음과 같이 나온다.

 

 

 

이처럼 높이 설정한 것만큼 UI가 그려지고 원하는 text가 그려지게 된다.

이것을 사용한다면, 원하는 UI의 원하는 데이터를 뿌려줄 수 있는 notification을 만들 수 있게 되는 것이다.

 

요구사항에 맞춰서 UI를 변경하기 위해서는 이처럼 custom layout을 적용하여 작업을 하는 것이 편할 것이다.

해당 layout을 선언하지 않고 사용하게 된다면, 기본적인 포맷이 제한되기 때문에 정해진 UI로만 나오기 때문이다.


이것으로 간단하게 Notification UI를 변경할 수 있는 몇 가지 방법에 대하여 작성해 보았다.

Notification에 대한 정보는 안드로이드 공식 문서에 너무나도 자세히 나와있고 다양한 종류에 대한 예제도 나와있기 때문에 쉽게 적용할 수 있었다.

 

지금까지 기본적인 UI만 사용하여 Notification 작업을 진행하다 요구사항에 의하여 Custom 하는 방법을 찾아보게 된 것인데,

생각보다 기본적으로 제공하는 UI가 깔끔하고 필요한 데이터가 다 들어있기 때문에 별도의 요구사항이 없으면 그대로 사용하는 것도 나쁘지 않은 선택인 것 같다.

하지만, 기본을 사용하게 된다면 OS 버전에 따라 보이는 Notification의 모양이 달라질 수 있기 때문에 넓은 범위의 OS를 커버하며 동일한 UI를 보여주기 원한다면 커스텀하여 사용하는 것이 좋은 방법이 될 것 같다.

728x90