Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Scrum 44] 뽀모도르 알림 UI/UX 개선 #145

Merged
merged 9 commits into from
Nov 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal object ServiceModule {
): NotificationCompat.Builder = NotificationCompat.Builder(context, POMODORO_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_app_notification)
.setVibrate(null)
.setOngoing(true)
.setContentIntent(ServiceHelper.clickPendingIntent(context, POMODORO_NOTIFICATION_ID))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.pomonyang.mohanyang.presentation.di

import com.pomonyang.mohanyang.data.repository.pomodoro.PomodoroTimerRepository
import com.pomonyang.mohanyang.presentation.service.PomodoroTimer
import com.pomonyang.mohanyang.presentation.service.focus.FocusTimer
import com.pomonyang.mohanyang.presentation.service.rest.RestTimer
Expand All @@ -15,13 +14,9 @@ internal object PomodoroModule {

@Provides
@FocusTimerType
fun provideFocusTimer(
timerRepository: PomodoroTimerRepository
): PomodoroTimer = FocusTimer(timerRepository = timerRepository)
fun provideFocusTimer(): PomodoroTimer = FocusTimer()

@Provides
@RestTimerType
fun provideRestTimer(
timerRepository: PomodoroTimerRepository
): PomodoroTimer = RestTimer(timerRepository = timerRepository)
fun provideRestTimer(): PomodoroTimer = RestTimer()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러의미로 감탄을 자아내는...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 많은 일이 있었다는 증명의 클래스야

Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package com.pomonyang.mohanyang.presentation.noti

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import androidx.annotation.ColorRes
import androidx.annotation.FontRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.mohanyang.presentation.R
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

internal class PomodoroNotificationBitmapGenerator @Inject constructor(
@ApplicationContext private val context: Context
) {
Comment on lines +17 to +19
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteViews가 때려 죽여도 TextView에 Style이 안먹고 시스템 폰트로 가버려서 억지로 Text를 Canvas에 그리고 그 Bitmap을 ImageView에 세팅하는 형태로 구현

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

..오.... 세상에나...... 내 문해력이 부족해서 잘못 이해하고 있나 했어 진짜 많은 일이 있었구나....


private val timeBitmaps: Map<String, Bitmap> by lazy {
generateNumberBitmaps(
fontResId = R.font.pretendard_bold,
textSize = 40f,
colorResId = R.color.notification_pomodoro_time
)
}

private val overtimeNumberBitmaps: Map<String, Bitmap> by lazy {
generateNumberBitmaps(
fontResId = R.font.pretendard_semibold,
textSize = 18f,
colorResId = R.color.notification_pomodoro_over_time
)
}

private val colonBitmap: Bitmap by lazy {
createTextBitmap(
text = ":",
fontResId = R.font.pretendard_bold,
textSize = 40f,
colorResId = R.color.notification_pomodoro_time
)
}

private val overtimeColonBitmap: Bitmap by lazy {
createTextBitmap(
text = ":",
fontResId = R.font.pretendard_semibold,
textSize = 18f,
colorResId = R.color.notification_pomodoro_over_time
)
}

fun createStatusBitmap(statusText: String): Bitmap = createTextBitmap(
text = statusText,
fontResId = R.font.pretendard_semibold,
textSize = 16f,
colorResId = R.color.notification_pomodoro_category_text
)

fun combineTimeBitmaps(time: String, isOvertime: Boolean): Bitmap {
val digits = time.toCharArray()
return combineBitmaps(
digits = digits,
isOvertime = isOvertime
)
}

fun createTextBitmap(
@StringRes text: Int,
@ColorRes color: Int,
@FontRes font: Int,
textSize: Float
): Bitmap = createTextBitmap(
text = context.getString(text),
fontResId = font,
textSize = textSize,
colorResId = color
)

private fun generateNumberBitmaps(@FontRes fontResId: Int, textSize: Float, @ColorRes colorResId: Int): Map<String, Bitmap> = (0..9).associate { number ->
number.toString() to createTextBitmap(
text = number.toString(),
fontResId = fontResId,
textSize = textSize,
colorResId = colorResId
)
}

private fun createTextBitmap(text: String, @FontRes fontResId: Int, textSize: Float, @ColorRes colorResId: Int): Bitmap {
val typeface = ResourcesCompat.getFont(context, fontResId)!!
val textColor = ContextCompat.getColor(context, colorResId)
return textToBitmap(text, typeface, textSize, textColor)
}

private fun textToBitmap(
text: String,
typeface: Typeface,
textSize: Float,
textColor: Int
): Bitmap {
val paint = Paint().apply {
this.typeface = typeface
this.textSize = textSize * context.resources.displayMetrics.density
this.color = textColor
isAntiAlias = true
textAlign = Paint.Align.LEFT
}
val baseline = -paint.ascent()
val width = (paint.measureText(text) + 0.5f).toInt()
val height = (baseline + paint.descent() + 0.5f).toInt()

return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
Canvas(this).drawText(text, 0f, baseline, paint)
}
}

private fun combineBitmaps(
digits: CharArray,
isOvertime: Boolean
): Bitmap {
val bitmaps = mutableListOf<Bitmap>()
var totalWidth = 0
var maxHeight = 0

digits.forEach { digit ->
val bitmap = when (digit) {
':' -> if (isOvertime) overtimeColonBitmap else colonBitmap
else -> {
val digitStr = digit.toString()
if (isOvertime) overtimeNumberBitmaps[digitStr] else timeBitmaps[digitStr]
}
}
bitmap?.let {
bitmaps.add(it)
totalWidth += it.width
maxHeight = maxOf(maxHeight, it.height)
}
}

if (isOvertime) {
val extraTextBitmap = createTextBitmap(
text = context.getString(R.string.timer_exceed_time),
fontResId = R.font.pretendard_semibold,
textSize = 18f,
colorResId = R.color.notification_pomodoro_over_time
)
bitmaps.add(extraTextBitmap)
totalWidth += extraTextBitmap.width
maxHeight = maxOf(maxHeight, extraTextBitmap.height)
}

return Bitmap.createBitmap(
totalWidth,
maxHeight,
Bitmap.Config.ARGB_8888
).apply {
val canvas = Canvas(this)
var currentX = 0f

bitmaps.forEach { bitmap ->
canvas.drawBitmap(bitmap, currentX, 0f, null)
currentX += bitmap.width
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.pomonyang.mohanyang.presentation.noti

import android.content.Context
import android.graphics.Bitmap
import android.view.View
import android.widget.RemoteViews
import com.mohanyang.presentation.R
import com.pomonyang.mohanyang.presentation.model.setting.PomodoroCategoryType
import com.pomonyang.mohanyang.presentation.util.formatTime
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

internal class PomodoroNotificationContentFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val bitmapGenerator: PomodoroNotificationBitmapGenerator
) {
Comment on lines +13 to +16
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service에서 Notification의 Content를 만드는 부분만 로직 분리

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oop 맛집이네요


fun createPomodoroNotificationContent(isRest: Boolean): RemoteViews {
val remoteViews = RemoteViews(context.packageName, R.layout.notification_pomodoro_standard)

val titleBitmap = generateTitleBitmap(isRest)
val contentBitmap = generateContentBitmap(isRest)

setTitle(remoteViews, titleBitmap)
setContent(remoteViews, contentBitmap)

return remoteViews
}

fun createPomodoroNotificationBigContent(
category: PomodoroCategoryType?,
time: String,
overtime: String?
): RemoteViews {
val remoteViews = RemoteViews(context.packageName, R.layout.notification_pomodoro_expand)

val formattedTime = formatTimeString(time)
val formattedOvertime = formatOvertimeString(overtime)

val (statusBitmap, iconRes) = getStatusBitmapAndIcon(category)

setStatus(remoteViews, statusBitmap)
setTime(remoteViews, formattedTime)
setOvertime(remoteViews, formattedOvertime)
setIcon(remoteViews, iconRes)

return remoteViews
}

private fun generateTitleBitmap(isRest: Boolean): Bitmap {
val titleRes = if (isRest) R.string.notification_rest_title else R.string.notification_focus_title
return bitmapGenerator.createTextBitmap(
text = titleRes,
color = R.color.notification_pomodoro_title,
font = R.font.pretendard_semibold,
textSize = 14f
)
}

private fun generateContentBitmap(isRest: Boolean): Bitmap {
val contentRes = if (isRest) R.string.notification_rest_content else R.string.notification_focus_content
return bitmapGenerator.createTextBitmap(
text = contentRes,
color = R.color.notification_pomodoro_content,
font = R.font.pretendard_regular,
textSize = 14f
)
}

private fun setTitle(remoteViews: RemoteViews, titleBitmap: Bitmap) {
remoteViews.setImageViewBitmap(R.id.iv_text_title, titleBitmap)
}

private fun setContent(remoteViews: RemoteViews, contentBitmap: Bitmap) {
remoteViews.setImageViewBitmap(R.id.iv_text_content, contentBitmap)
}

private fun formatTimeString(timeStr: String): String {
val totalSeconds = timeStr.toIntOrNull()
return totalSeconds?.formatTime() ?: context.getString(R.string.notification_timer_default_time)
}

private fun formatOvertimeString(overtime: String?): String? = if (!overtime.isNullOrEmpty() && overtime != "0") {
formatTimeString(overtime)
} else {
null
}

private fun setStatus(remoteViews: RemoteViews, statusBitmap: Bitmap) {
remoteViews.setImageViewBitmap(R.id.text_status, statusBitmap)
}

private fun setTime(remoteViews: RemoteViews, formattedTime: String) {
val timeBitmap = bitmapGenerator.combineTimeBitmaps(
time = formattedTime,
isOvertime = false
)
remoteViews.setImageViewBitmap(R.id.text_time, timeBitmap)
}

private fun setOvertime(remoteViews: RemoteViews, formattedOvertime: String?) {
if (formattedOvertime != null) {
val overtimeBitmap = bitmapGenerator.combineTimeBitmaps(
time = formattedOvertime,
isOvertime = true
)
remoteViews.setImageViewBitmap(R.id.text_overtime, overtimeBitmap)
remoteViews.setViewVisibility(R.id.text_overtime, View.VISIBLE)
} else {
remoteViews.setViewVisibility(R.id.text_overtime, View.GONE)
}
}

private fun setIcon(remoteViews: RemoteViews, iconRes: Int) {
remoteViews.setImageViewResource(R.id.icon_category, iconRes)
}

private fun getStatusBitmapAndIcon(category: PomodoroCategoryType?): Pair<Bitmap, Int> {
val statusTextRes = category?.kor ?: R.string.notification_timer_rest
val statusText = context.getString(statusTextRes)
val statusBitmap = bitmapGenerator.createStatusBitmap(statusText)
val iconRes = category?.iconRes ?: R.drawable.ic_rest
return statusBitmap to iconRes
}
}
Loading