diff --git a/app/src/main/java/com/polzzak_android/presentation/common/item/LoadMoreLoadingSpinnerItem.kt b/app/src/main/java/com/polzzak_android/presentation/common/item/LoadMoreLoadingSpinnerItem.kt new file mode 100644 index 00000000..c762a448 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/common/item/LoadMoreLoadingSpinnerItem.kt @@ -0,0 +1,49 @@ +package com.polzzak_android.presentation.common.item + +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import androidx.constraintlayout.widget.ConstraintLayout +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemLoadMoreLoadingBinding +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.common.util.toPx + +class LoadMoreLoadingSpinnerItem( + private val marginTopDp: Int = DEFAULT_VERTICAL_MARGIN_DP, + private val marginBottomDp: Int = DEFAULT_VERTICAL_MARGIN_DP +) : + BindableItem() { + override val layoutRes: Int = R.layout.item_load_more_loading + + override fun areItemsTheSame(other: BindableItem<*>): Boolean = + other is LoadMoreLoadingSpinnerItem + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = + other is LoadMoreLoadingSpinnerItem && this.marginTopDp == other.marginTopDp && + this.marginBottomDp == other.marginBottomDp + + override fun bind(binding: ItemLoadMoreLoadingBinding, position: Int) { + val context = binding.root.context + (binding.ivSpinner.layoutParams as? ConstraintLayout.LayoutParams)?.let { lp -> + binding.ivSpinner.layoutParams = lp.apply { + topMargin = marginTopDp.toPx(context) + bottomMargin = marginBottomDp.toPx(context) + } + } + val rotateAnimation = RotateAnimation( + 0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f + ).apply { + duration = 1000 + interpolator = LinearInterpolator() + repeatCount = Animation.INFINITE + } + binding.ivSpinner.startAnimation(rotateAnimation) + + } + + companion object { + private const val DEFAULT_VERTICAL_MARGIN_DP = 24 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItem.kt b/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItem.kt index 348872d9..4c06d0ab 100644 --- a/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItem.kt +++ b/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItem.kt @@ -1,6 +1,5 @@ package com.polzzak_android.presentation.common.util -import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.databinding.ViewDataBinding @@ -11,11 +10,7 @@ import androidx.databinding.ViewDataBinding abstract class BindableItem { @get:LayoutRes abstract val layoutRes: Int - - open fun onCreateViewHolder(parent: ViewGroup, position: Int) {} - abstract fun bind(binding: B, position: Int) - abstract fun areItemsTheSame(other: BindableItem<*>): Boolean abstract fun areContentsTheSame(other: BindableItem<*>): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItemAdapter.kt b/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItemAdapter.kt index 0ad537ea..63ce3e3a 100644 --- a/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItemAdapter.kt +++ b/app/src/main/java/com/polzzak_android/presentation/common/util/BindableItemAdapter.kt @@ -30,18 +30,19 @@ class BindableItemAdapter : rv = null } - fun updateItem(item: List>) { - submitList(item) + fun updateItem(item: List>, commitCallback: (() -> Unit)? = null) { + submitList(item, commitCallback) } - fun addItem(items: List>) { + fun addItem(items: List>, commitCallback: (() -> Unit)? = null) { val newItems = currentList + items - submitList(newItems) + submitList(newItems, commitCallback) } - private fun submitList(items: List>) { - asyncDiffer.submitList(items){ + private fun submitList(items: List>, commitCallback: (() -> Unit)? = null) { + asyncDiffer.submitList(items) { rv?.invalidateItemDecorations() + commitCallback?.invoke() } } diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/NotificationViewModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/NotificationViewModel.kt new file mode 100644 index 00000000..113b7926 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/NotificationViewModel.kt @@ -0,0 +1,245 @@ +package com.polzzak_android.presentation.feature.notification + +import android.text.SpannableString +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.polzzak_android.presentation.common.model.ModelState +import com.polzzak_android.presentation.common.model.copyWithData +import com.polzzak_android.presentation.feature.notification.list.NotificationItemStateController +import com.polzzak_android.presentation.feature.notification.list.model.NotificationModel +import com.polzzak_android.presentation.feature.notification.list.model.NotificationRefreshStatusType +import com.polzzak_android.presentation.feature.notification.list.model.NotificationsModel +import com.polzzak_android.presentation.feature.notification.setting.model.SettingMenuModel +import com.polzzak_android.presentation.feature.notification.setting.model.SettingMenuType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import javax.inject.Inject + +@HiltViewModel +class NotificationViewModel @Inject constructor() : ViewModel(), NotificationItemStateController { + private val _notificationLiveData = MutableLiveData>() + val notificationLiveData: LiveData> = _notificationLiveData + private var requestNotificationJobData: NotificationJobData? = null + + //TODO 추가 삭제 등 알림목록 수정 이벤트 + private var updateNotificationJobMap = HashMap() + + var isRefreshed = false + private set + + private val notificationHorizontalScrollPositionMap = HashMap() + + private val _settingMenusLiveData = MutableLiveData>() + val settingMenusLiveData: LiveData> = _settingMenusLiveData + private var requestSettingMenusJob: Job? = null + + private val notificationMutex = Mutex() + + init { + initNotifications() + } + + private fun initNotifications() { + val priority = INIT_NOTIFICATIONS_PRIORITY + if (requestNotificationJobData.getPriorityOrZero() < priority) requestNotificationJobData?.job?.cancel() + else if (requestNotificationJobData?.job?.isCompleted == false) return + requestNotificationJobData = NotificationJobData( + priority = priority, + job = createJobWithUnlockOnCompleted { + isRefreshed = true + notificationMutex.lock() + _notificationLiveData.value = ModelState.Loading(NotificationsModel()) + notificationMutex.unlock() + requestNotifications() + }, + ) + } + + fun refreshNotifications() { + val priority = REFRESH_NOTIFICATIONS_PRIORITY + if (requestNotificationJobData.getPriorityOrZero() < priority) requestNotificationJobData?.job?.cancel() + else if (requestNotificationJobData?.job?.isCompleted == false) return + requestNotificationJobData = NotificationJobData( + priority = priority, + job = createJobWithUnlockOnCompleted { + isRefreshed = true + notificationMutex.lock() + val prevData = notificationLiveData.value?.data ?: NotificationsModel() + _notificationLiveData.value = + ModelState.Loading(prevData.copy(refreshStatusType = NotificationRefreshStatusType.Loading)) + notificationMutex.unlock() + requestNotifications() + }, + ) + } + + fun requestMoreNotifications() { + if (notificationLiveData.value?.data?.hasNextPage == false) return + val priority = MORE_NOTIFICATIONS_PRIORITY + if (requestNotificationJobData.getPriorityOrZero() <= priority) requestNotificationJobData?.job?.cancel() + else if (requestNotificationJobData?.job?.isCompleted == false) return + requestNotificationJobData = NotificationJobData( + priority = priority, + job = createJobWithUnlockOnCompleted { + isRefreshed = false + notificationMutex.lock() + val prevData = notificationLiveData.value?.data ?: NotificationsModel() + _notificationLiveData.value = + ModelState.Loading(prevData.copy(refreshStatusType = NotificationRefreshStatusType.Normal)) + notificationMutex.unlock() + requestNotifications() + }, + ) + } + + //TODO test용 delay를 위해 suspend 붙여줌(제거 필요) + private suspend fun requestNotifications() { + //TODO api 연동(현재 mock data) + val nextOffset = notificationLiveData.value?.data?.nextOffset.takeIf { !isRefreshed } ?: 0 + delay(2000) + val nextData = getMockNotificationData(nextOffset, NOTIFICATION_PAGE_SIZE) + + //onSuccess + notificationMutex.lock() + val prevData = + notificationLiveData.value?.data.takeIf { !isRefreshed } ?: NotificationsModel() + if (isRefreshed) notificationHorizontalScrollPositionMap.clear() + _notificationLiveData.value = + ModelState.Success( + nextData.copy( + items = (prevData.items ?: emptyList()) + (nextData.items ?: emptyList()), + refreshStatusType = NotificationRefreshStatusType.Normal + ) + ) + notificationMutex.unlock() + } + + fun deleteNotification(id: Int) { + if (updateNotificationJobMap[id]?.isCompleted == false) return + updateNotificationJobMap[id] = createJobWithUnlockOnCompleted { + //TODO api 적용 + delay(1000) + deleteMockNotificationData(id = id) + + //onSuccess + notificationMutex.lock() + val updatedList = notificationLiveData.value?.data?.items?.toMutableList()?.apply { + removeIf { it.id == id } + } + val updatedData = + notificationLiveData.value?.data?.copy(items = updatedList) ?: NotificationsModel() + _notificationLiveData.value = + _notificationLiveData.value?.copyWithData(newData = updatedData) + notificationMutex.unlock() + //TODO onError 이벤트 추가(Livedata, eventWrapper 등 필요할 수도 있음) + } + } + + fun addNotification(model: NotificationModel) { + if (updateNotificationJobMap[model.id]?.isCompleted == false) return + updateNotificationJobMap[model.id] = createJobWithUnlockOnCompleted { + //TODO 푸쉬알림으로 인한 알림 목록 추가 + + } + } + + private fun createJobWithUnlockOnCompleted(action: suspend () -> Unit) = viewModelScope.launch { + action.invoke() + }.apply { + invokeOnCompletion { + if (notificationMutex.isLocked) notificationMutex.unlock() + } + } + + fun requestSettingMenu() { + requestSettingMenusJob?.cancel() + requestSettingMenusJob = viewModelScope.launch { + //TOOD repository 구현 + _settingMenusLiveData.value = mockSettingMenus + } + } + + private data class NotificationJobData(val priority: Int, val job: Job) + + private fun NotificationJobData?.getPriorityOrZero() = this?.priority ?: 0 + + override fun setHorizontalScrollPosition(id: Int, position: Int) { + notificationHorizontalScrollPositionMap[id] = position + } + + override fun getHorizontalScrollPosition(id: Int): Int = + notificationHorizontalScrollPositionMap[id] ?: 0 + + override fun getIsRefreshedSuccess(): Boolean = + isRefreshed && notificationLiveData.value is ModelState.Success + + companion object { + const val NOTIFICATION_PAGE_SIZE = 10 + private const val INIT_NOTIFICATIONS_PRIORITY = 3 + private const val REFRESH_NOTIFICATIONS_PRIORITY = 2 + private const val MORE_NOTIFICATIONS_PRIORITY = 1 + } +} + +private fun getMockNotificationData(nextOffset: Int, pageSize: Int): NotificationsModel { + return NotificationsModel( + hasNextPage = nextOffset + pageSize < mockNotification.size, + nextOffset = nextOffset + pageSize, + items = mockNotification.subList( + nextOffset, + minOf(mockNotification.size, nextOffset + pageSize) + ) + ) +} + +private fun deleteMockNotificationData(id: Int) { + mockNotification.removeIf { it.id == id } +} + +private val mockNotification = MutableList(187) { + when (it % 4) { + 0 -> NotificationModel.CompleteLink( + id = it, + date = "${it}일 전", + content = SpannableString("연동 완료"), + nickName = "닉네임${it}", + profileImageUrl = "https://picsum.photos/id/${it + 1}/200/300" + ) + + 1 -> NotificationModel.LevelDown( + id = it, + date = "${it}일 전", + content = SpannableString("레벨 감소"), + ) + + 2 -> NotificationModel.LevelUp( + id = it, + date = "${it}일 전", + content = SpannableString("레벨 업") + ) + + else -> NotificationModel.RequestLink( + id = it, + date = "${it}일 전", + content = SpannableString("연동 요청"), + nickName = "닉네임${it}", + profileImageUrl = "https://picsum.photos/id/${it + 1}/200/300" + ) + } +} + +private val mockSettingMenus = mutableListOf( + SettingMenuModel(type = SettingMenuType.All, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.Link, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.Level, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.RequestStamp, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.RequestGift, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.CompleteStampBoard, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.ReceiveGift, isChecked = false), + SettingMenuModel(type = SettingMenuType.Menu.BreakPromise, isChecked = false) +) \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/kid/KidNotificationFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/kid/KidNotificationFragment.kt deleted file mode 100644 index 69fe318b..00000000 --- a/app/src/main/java/com/polzzak_android/presentation/feature/notification/kid/KidNotificationFragment.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.polzzak_android.presentation.feature.notification.kid - -import com.polzzak_android.R -import com.polzzak_android.presentation.common.base.BaseFragment -import com.polzzak_android.databinding.FragmentKidNotificationBinding - -class KidNotificationFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_kid_notification - -} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationItemDecoration.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationItemDecoration.kt new file mode 100644 index 00000000..f5cb471b --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationItemDecoration.kt @@ -0,0 +1,37 @@ +package com.polzzak_android.presentation.feature.notification.list + +import android.graphics.Rect +import android.view.View +import androidx.annotation.Px +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.polzzak_android.presentation.common.util.BindableItemAdapter +import com.polzzak_android.presentation.feature.notification.list.item.NotificationItem +import com.polzzak_android.presentation.feature.notification.list.item.NotificationSkeletonLoadingItem + +class NotificationItemDecoration( + @Px private val paddingPx: Int, + @Px private val betweenMarginPx: Int, +) : ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + val adapter = (parent.adapter as? BindableItemAdapter) ?: return + val position = parent.getChildAdapterPosition(view) + val currentItem = adapter.currentList.getOrNull(position) + if (!isContentItem(currentItem)) return + val prevItem = adapter.currentList.getOrNull(position - 1) + val nextItem = adapter.currentList.getOrNull(position + 1) + outRect.top = + if (isContentItem(prevItem)) 0 else paddingPx + outRect.bottom = + if (isContentItem(nextItem)) betweenMarginPx else paddingPx + } + + private fun isContentItem(item: Any?): Boolean = + item is NotificationItem || item is NotificationSkeletonLoadingItem +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationItemStateController.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationItemStateController.kt new file mode 100644 index 00000000..ec0f7f19 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationItemStateController.kt @@ -0,0 +1,7 @@ +package com.polzzak_android.presentation.feature.notification.list + +interface NotificationItemStateController { + fun setHorizontalScrollPosition(id: Int, position: Int) + fun getHorizontalScrollPosition(id: Int): Int + fun getIsRefreshedSuccess(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationListClickListener.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationListClickListener.kt new file mode 100644 index 00000000..f1ad9260 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/NotificationListClickListener.kt @@ -0,0 +1,5 @@ +package com.polzzak_android.presentation.feature.notification.list + +interface NotificationListClickListener { + fun onClickDeleteNotification(id: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/base/BaseNotificationListFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/base/BaseNotificationListFragment.kt new file mode 100644 index 00000000..85d92489 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/base/BaseNotificationListFragment.kt @@ -0,0 +1,186 @@ +package com.polzzak_android.presentation.feature.notification.list.base + +import android.util.DisplayMetrics +import androidx.annotation.IdRes +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import com.polzzak_android.R +import com.polzzak_android.databinding.FragmentNotificationListBinding +import com.polzzak_android.presentation.common.base.BaseFragment +import com.polzzak_android.presentation.common.item.LoadMoreLoadingSpinnerItem +import com.polzzak_android.presentation.common.model.MemberType +import com.polzzak_android.presentation.common.model.ModelState +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.common.util.BindableItemAdapter +import com.polzzak_android.presentation.common.util.toPx +import com.polzzak_android.presentation.feature.notification.NotificationViewModel +import com.polzzak_android.presentation.feature.notification.list.NotificationItemDecoration +import com.polzzak_android.presentation.feature.notification.list.NotificationListClickListener +import com.polzzak_android.presentation.feature.notification.list.item.NotificationEmptyItem +import com.polzzak_android.presentation.feature.notification.list.item.NotificationItem +import com.polzzak_android.presentation.feature.notification.list.item.NotificationRefreshItem +import com.polzzak_android.presentation.feature.notification.list.item.NotificationSkeletonLoadingItem +import com.polzzak_android.presentation.feature.notification.list.model.NotificationModel +import com.polzzak_android.presentation.feature.notification.list.model.NotificationRefreshStatusType +import dagger.hilt.android.AndroidEntryPoint + + +//TODO 하단 네비게이션 바 만큼 marign 필요 +@AndroidEntryPoint +abstract class BaseNotificationListFragment : BaseFragment(), + NotificationListClickListener { + override val layoutResId: Int = R.layout.fragment_notification_list + + private val notificationViewModel by viewModels(ownerProducer = { + parentFragment ?: this@BaseNotificationListFragment + }) + + private val smoothScroller by lazy { + object : LinearSmoothScroller(context) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { + return super.calculateSpeedPerPixel(displayMetrics) * 4f + } + } + } + + @get:IdRes + abstract val actionToSettingFragment: Int + abstract val memberType: MemberType + + override fun initView() { + super.initView() + initRecyclerView() + binding.ivBtnSetting.setOnClickListener { + findNavController().navigate(actionToSettingFragment) + } + } + + private fun initRecyclerView() { + with(binding.rvNotifications) { + val layoutManager = LinearLayoutManager(context) + this.layoutManager = layoutManager + val paddingPx = NOTIFICATIONS_PADDING_DP.toPx(context) + val betweenMarginPx = NOTIFICATIONS_BETWEEN_MARGIN_DP.toPx(context) + val itemDecoration = NotificationItemDecoration( + paddingPx = paddingPx, + betweenMarginPx = betweenMarginPx + ) + addItemDecoration(itemDecoration) + adapter = BindableItemAdapter() + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (!recyclerView.canScrollVertically(1)) notificationViewModel.requestMoreNotifications() + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + when (notificationViewModel.notificationLiveData.value?.data?.refreshStatusType) { + NotificationRefreshStatusType.Normal -> onScrollStateChangedRefreshNormal( + recyclerView = recyclerView, + newState = newState + ) + + else -> { + //do nothing + } + } + } + }) + } + } + + private fun onScrollStateChangedRefreshNormal(recyclerView: RecyclerView, newState: Int) { + val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return + val firstCompletelyVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() + if (firstCompletelyVisibleItem == 0) { + notificationViewModel.refreshNotifications() + return + } + val firstVisibleItem = + layoutManager.findFirstVisibleItemPosition().takeIf { it >= 0 } ?: return + if (firstVisibleItem < 1 && newState != RecyclerView.SCROLL_STATE_DRAGGING) { + smoothScroller.targetPosition = 1 + layoutManager.startSmoothScroll(smoothScroller) + } + } + + override fun initObserver() { + super.initObserver() + initNotificationObserver() + } + + private fun initNotificationObserver() { + notificationViewModel.notificationLiveData.observe(viewLifecycleOwner) { + val layoutManager = + (binding.rvNotifications.layoutManager as? LinearLayoutManager) ?: return@observe + val adapter = + (binding.rvNotifications.adapter as? BindableItemAdapter) ?: return@observe + val refreshStatusType = + it.data?.refreshStatusType ?: NotificationRefreshStatusType.Disable + val items = + mutableListOf>(NotificationRefreshItem(statusType = refreshStatusType)) + var updateCallback: (() -> Unit)? = null + when (it) { + is ModelState.Loading -> { + if (it.data?.items == null) { + items.addAll(createSkeletonLoadingItems()) + } else { + items.addAll(createNotificationItems(data = it.data?.items)) + if (!notificationViewModel.isRefreshed) items.add( + LoadMoreLoadingSpinnerItem( + marginTopDp = 8 + ) + ) + } + } + + is ModelState.Success -> { + items.addAll(createNotificationItems(data = it.data.items)) + if (notificationViewModel.isRefreshed) { + updateCallback = { + layoutManager.scrollToPositionWithOffset(1, 0) + } + } + } + + is ModelState.Error -> { + //TODO 에러처리 + } + } + + adapter.updateItem(item = items, commitCallback = updateCallback) + } + } + + private fun createSkeletonLoadingItems() = List(LOADING_SKELETON_ITEM_COUNT) { + NotificationSkeletonLoadingItem() + } + + private fun createNotificationItems(data: List?): List> { + return if (data.isNullOrEmpty()) listOf(NotificationEmptyItem()) else data.map { + NotificationItem( + model = it, + itemStateController = notificationViewModel, + clickListener = this@BaseNotificationListFragment + ) + } + } + + override fun onClickDeleteNotification(id: Int) { + notificationViewModel.deleteNotification(id = id) + } + + companion object { + private const val LOADING_SKELETON_ITEM_COUNT = 5 + private const val NOTIFICATIONS_PADDING_DP = 16 + private const val NOTIFICATIONS_BETWEEN_MARGIN_DP = 8 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationEmptyItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationEmptyItem.kt new file mode 100644 index 00000000..690dc44f --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationEmptyItem.kt @@ -0,0 +1,18 @@ +package com.polzzak_android.presentation.feature.notification.list.item + +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemNotificationEmptyBinding +import com.polzzak_android.presentation.common.util.BindableItem + +class NotificationEmptyItem : BindableItem() { + override val layoutRes: Int = R.layout.item_notification_empty + + override fun areItemsTheSame(other: BindableItem<*>): Boolean = other is NotificationEmptyItem + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = + other is NotificationEmptyItem + + override fun bind(binding: ItemNotificationEmptyBinding, position: Int) { + //do nothing + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationItem.kt new file mode 100644 index 00000000..d17e9d88 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationItem.kt @@ -0,0 +1,93 @@ +package com.polzzak_android.presentation.feature.notification.list.item + +import android.view.View +import androidx.core.view.doOnPreDraw +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemNotificationBinding +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.common.util.loadCircleImageUrl +import com.polzzak_android.presentation.common.util.toPx +import com.polzzak_android.presentation.feature.notification.list.NotificationItemStateController +import com.polzzak_android.presentation.feature.notification.list.NotificationListClickListener +import com.polzzak_android.presentation.feature.notification.list.model.NotificationModel + +//TODO 버튼 클릭 상태 적용 +class NotificationItem( + private val model: NotificationModel, + private val itemStateController: NotificationItemStateController, + private val clickListener: NotificationListClickListener +) : BindableItem() { + override val layoutRes: Int = R.layout.item_notification + override fun areItemsTheSame(other: BindableItem<*>): Boolean = + other is NotificationItem && this.model.id == other.model.id + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = + other is NotificationItem && this.model == other.model && !itemStateController.getIsRefreshedSuccess() + + override fun bind(binding: ItemNotificationBinding, position: Int) { + with(binding) { + val context = root.context + tvEmoji.text = context.getString(model.emojiStringRes) + tvTitle.text = context.getString(model.titleStringRes) + tvDate.text = model.date + tvContent.text = model.content + ivBtnRemoveNotification.setOnClickListener { + clickListener.onClickDeleteNotification(id = model.id) + } + bindBtnLayout(binding = binding) + bindProfile(binding = binding) + bindHorizontalScroll(binding = binding) + } + } + + private fun bindHorizontalScroll(binding: ItemNotificationBinding) { + with(binding) { + clNotification.doOnPreDraw { + it.updateLayoutParams { + width = it.width + } + clRemoveNotification.updateLayoutParams { + width = + it.width + NOTIFICATION_REMOVE_LAYOUT_WIDTH_DP.toPx(context = binding.root.context) + } + hsvNotification.doOnPreDraw { + hsvNotification.scrollX = + itemStateController.getHorizontalScrollPosition(id = model.id) + } + } + hsvNotification.setOnScrollChangeListener { _, scrollX, _, _, _ -> + clRemoveNotification.visibility = if (scrollX > 0) View.VISIBLE else View.INVISIBLE + itemStateController.setHorizontalScrollPosition(id = model.id, position = scrollX) + } + clRemoveNotification.visibility = + if (hsvNotification.scrollX == 0) View.INVISIBLE else View.VISIBLE + } + } + + private fun bindBtnLayout(binding: ItemNotificationBinding) { + with(binding) { + clBtnLayout.isVisible = model.isButtonVisible + tvBtnAccept.setOnClickListener { + //TODO 수락 버튼 클릭 + } + tvBtnReject.setOnClickListener { + //TODO 거절 버튼 클릭 + } + } + + } + + private fun bindProfile(binding: ItemNotificationBinding) { + with(binding) { + clProfile.isVisible = (model.userInfo != null) + ivProfileImage.loadCircleImageUrl(imageUrl = model.userInfo?.profileImageUrl) + tvNickName.text = model.userInfo?.nickName + } + } + + companion object { + private const val NOTIFICATION_REMOVE_LAYOUT_WIDTH_DP = 56 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationRefreshItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationRefreshItem.kt new file mode 100644 index 00000000..f953ad29 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationRefreshItem.kt @@ -0,0 +1,44 @@ +package com.polzzak_android.presentation.feature.notification.list.item + +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemNotificationRefreshBinding +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.feature.notification.list.model.NotificationRefreshStatusType + +class NotificationRefreshItem(private val statusType: NotificationRefreshStatusType) : + BindableItem() { + override val layoutRes: Int = R.layout.item_notification_refresh + + override fun areItemsTheSame(other: BindableItem<*>): Boolean = + other is NotificationRefreshItem && this.statusType == other.statusType + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = + other is NotificationRefreshItem && this.statusType == other.statusType + + override fun bind(binding: ItemNotificationRefreshBinding, position: Int) { + binding.ivRefresh.isVisible = (statusType != NotificationRefreshStatusType.Disable) + statusType.progressDrawableRes?.let { + val progressDrawable = ContextCompat.getDrawable( + binding.root.context, + it + ) + binding.ivRefresh.setImageDrawable(progressDrawable) + if (statusType == NotificationRefreshStatusType.Loading) { + val rotateAnimation = RotateAnimation( + 0f, 360f, Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f + ).apply { + duration = 1000 + interpolator = LinearInterpolator() + repeatCount = Animation.INFINITE + } + binding.ivRefresh.startAnimation(rotateAnimation) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationSkeletonLoadingItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationSkeletonLoadingItem.kt new file mode 100644 index 00000000..89f27b20 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/item/NotificationSkeletonLoadingItem.kt @@ -0,0 +1,18 @@ +package com.polzzak_android.presentation.feature.notification.list.item + +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemNotificationLoadingSkeletonBinding +import com.polzzak_android.presentation.common.util.BindableItem + +class NotificationSkeletonLoadingItem : BindableItem() { + override val layoutRes: Int = R.layout.item_notification_loading_skeleton + override fun areItemsTheSame(other: BindableItem<*>): Boolean = + other is NotificationSkeletonLoadingItem + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = + other is NotificationSkeletonLoadingItem + + override fun bind(binding: ItemNotificationLoadingSkeletonBinding, position: Int) { + //do nothing + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/kid/KidNotificationListFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/kid/KidNotificationListFragment.kt new file mode 100644 index 00000000..931a328d --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/kid/KidNotificationListFragment.kt @@ -0,0 +1,10 @@ +package com.polzzak_android.presentation.feature.notification.list.kid + +import com.polzzak_android.R +import com.polzzak_android.presentation.common.model.MemberType +import com.polzzak_android.presentation.feature.notification.list.base.BaseNotificationListFragment + +class KidNotificationListFragment: BaseNotificationListFragment(){ + override val actionToSettingFragment: Int = R.id.action_kidNotificationFragment_to_notificationSettingFragment + override val memberType: MemberType = MemberType.Kid("") +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationModel.kt new file mode 100644 index 00000000..18123c03 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationModel.kt @@ -0,0 +1,75 @@ +package com.polzzak_android.presentation.feature.notification.list.model + +import android.text.Spannable +import androidx.annotation.StringRes +import com.polzzak_android.R + +//TODO api response 확인 후 알림타입추가 +sealed interface NotificationModel { + val id: Int + + @get:StringRes + val emojiStringRes: Int + + @get:StringRes + val titleStringRes: Int + val date: String + val content: Spannable + val isButtonVisible: Boolean + val userInfo: NotificationUserModel? + + data class NotificationUserModel( + val profileImageUrl: String, + val nickName: String + ) + + class RequestLink( + override val id: Int, + override val date: String, + override val content: Spannable, + nickName: String, + profileImageUrl: String, + ) : NotificationModel { + override val emojiStringRes: Int = R.string.notification_request_link_emoji + override val titleStringRes: Int = R.string.notification_request_link_title + override val isButtonVisible = true + override val userInfo = + NotificationUserModel(profileImageUrl = profileImageUrl, nickName = nickName) + } + + class CompleteLink( + override val id: Int, + override val date: String, + override val content: Spannable, + nickName: String, + profileImageUrl: String, + ) : NotificationModel { + override val emojiStringRes: Int = R.string.notification_complete_link_emoji + override val titleStringRes: Int = R.string.notification_complete_link_title + override val isButtonVisible = false + override val userInfo = + NotificationUserModel(profileImageUrl = profileImageUrl, nickName = nickName) + } + + class LevelUp( + override val id: Int, + override val date: String, + override val content: Spannable + ) : NotificationModel { + override val emojiStringRes: Int = R.string.notification_level_up_emoji + override val titleStringRes: Int = R.string.notification_level_up_title + override val isButtonVisible = false + override val userInfo = null + } + + class LevelDown( + override val id: Int, + override val date: String, + override val content: Spannable + ) : NotificationModel { + override val emojiStringRes: Int = R.string.notification_level_down_emoji + override val titleStringRes: Int = R.string.notification_level_down_title + override val isButtonVisible = false + override val userInfo = null + } +} diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationRefreshStatusType.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationRefreshStatusType.kt new file mode 100644 index 00000000..165f66dc --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationRefreshStatusType.kt @@ -0,0 +1,21 @@ +package com.polzzak_android.presentation.feature.notification.list.model + +import androidx.annotation.DrawableRes +import com.polzzak_android.R + +sealed interface NotificationRefreshStatusType { + @get:DrawableRes + val progressDrawableRes: Int? + + object Disable : NotificationRefreshStatusType { + override val progressDrawableRes: Int? = null + } + + object Loading : NotificationRefreshStatusType { + override val progressDrawableRes: Int = R.drawable.ic_refresh_loading_spinner + } + + object Normal : NotificationRefreshStatusType { + override val progressDrawableRes: Int = R.drawable.ic_pulling + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationsModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationsModel.kt new file mode 100644 index 00000000..3d952d24 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/model/NotificationsModel.kt @@ -0,0 +1,9 @@ +package com.polzzak_android.presentation.feature.notification.list.model + +//TODO API 모델 확인 후 수정 필요 +data class NotificationsModel( + val hasNextPage: Boolean = true, + val nextOffset: Int = 0, + val items: List? = null, + val refreshStatusType: NotificationRefreshStatusType = NotificationRefreshStatusType.Disable +) diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/protector/ProtectorNotificationListFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/protector/ProtectorNotificationListFragment.kt new file mode 100644 index 00000000..d08d49b9 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/list/protector/ProtectorNotificationListFragment.kt @@ -0,0 +1,11 @@ +package com.polzzak_android.presentation.feature.notification.list.protector + +import com.polzzak_android.R +import com.polzzak_android.presentation.common.model.MemberType +import com.polzzak_android.presentation.feature.notification.list.base.BaseNotificationListFragment + +class ProtectorNotificationListFragment : BaseNotificationListFragment() { + override val actionToSettingFragment: Int = + R.id.action_protectorNotificationFragment_to_notificationSettingFragment + override val memberType: MemberType = MemberType.Parent("") +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/protector/ProtectorNotificationFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/protector/ProtectorNotificationFragment.kt deleted file mode 100644 index 36c766d9..00000000 --- a/app/src/main/java/com/polzzak_android/presentation/feature/notification/protector/ProtectorNotificationFragment.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.polzzak_android.presentation.feature.notification.protector - -import com.polzzak_android.R -import com.polzzak_android.presentation.common.base.BaseFragment -import com.polzzak_android.databinding.FragmentProtectorNotificationBinding - -class ProtectorNotificationFragment : BaseFragment() { - override val layoutResId: Int = R.layout.fragment_protector_notification -} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/NotificationSettingFragment.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/NotificationSettingFragment.kt new file mode 100644 index 00000000..9fb5b736 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/NotificationSettingFragment.kt @@ -0,0 +1,74 @@ +package com.polzzak_android.presentation.feature.notification.setting + +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.polzzak_android.R +import com.polzzak_android.databinding.FragmentNotificationSettingBinding +import com.polzzak_android.presentation.common.base.BaseFragment +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.common.util.BindableItemAdapter +import com.polzzak_android.presentation.common.util.toPx +import com.polzzak_android.presentation.feature.notification.NotificationViewModel +import com.polzzak_android.presentation.feature.notification.setting.item.NotificationSettingAllItem +import com.polzzak_android.presentation.feature.notification.setting.item.NotificationSettingItem +import com.polzzak_android.presentation.feature.notification.setting.model.SettingMenuModel +import com.polzzak_android.presentation.feature.notification.setting.model.SettingMenuType + +//TODO 하단 툴바 숨김 +class NotificationSettingFragment : + BaseFragment() { + override val layoutResId: Int = R.layout.fragment_notification_setting + + private val notificationViewModel by viewModels(ownerProducer = { + parentFragment ?: this@NotificationSettingFragment + }) + + override fun initView() { + super.initView() + with(binding) { + ivHeaderBtnBack.setOnClickListener { + findNavController().popBackStack() + } + } + initRecyclerView() + notificationViewModel.requestSettingMenu() + } + + private fun initRecyclerView() { + val context = binding.root.context + with(binding) { + rvSetting.layoutManager = LinearLayoutManager(context) + rvSetting.adapter = BindableItemAdapter() + val dividerHeightPx = DIVIDER_HEIGHT_DP.toPx(context) + val allMenuDividerColor = context.getColor(R.color.gray_300) + val otherMenuDividerColor = context.getColor(R.color.gray_200) + val itemDecoration = NotificationSettingItemDecoration( + allMenuDividerColor = allMenuDividerColor, + otherMenuDividerColor = otherMenuDividerColor, + heightPx = dividerHeightPx + ) + rvSetting.addItemDecoration(itemDecoration) + } + } + + override fun initObserver() { + super.initObserver() + notificationViewModel.settingMenusLiveData.observe(viewLifecycleOwner) { + val adapter = (binding.rvSetting.adapter as? BindableItemAdapter) ?: return@observe + adapter.updateItem(item = createSettingMenuItem(model = it)) + } + } + + private fun createSettingMenuItem(model: List): List> = + model.map { + when (it.type) { + is SettingMenuType.All -> NotificationSettingAllItem(model = it.type) + is SettingMenuType.Menu -> NotificationSettingItem(model = it.type) + } + } + + companion object { + private const val DIVIDER_HEIGHT_DP = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/NotificationSettingItemDecoration.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/NotificationSettingItemDecoration.kt new file mode 100644 index 00000000..6f556556 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/NotificationSettingItemDecoration.kt @@ -0,0 +1,38 @@ +package com.polzzak_android.presentation.feature.notification.setting + +import android.graphics.Canvas +import android.graphics.Paint +import androidx.annotation.ColorInt +import androidx.annotation.Px +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import com.polzzak_android.presentation.common.util.BindableItemAdapter + +class NotificationSettingItemDecoration( + @ColorInt private val allMenuDividerColor: Int, + @ColorInt private val otherMenuDividerColor: Int, + @Px private val heightPx: Int +) : ItemDecoration() { + private val allMenuPaint = Paint().apply { + color = allMenuDividerColor + } + private val otherMenuPaint = Paint().apply { + color = otherMenuDividerColor + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(c, parent, state) + val adapter = (parent.adapter as? BindableItemAdapter) ?: return + val left = (parent.paddingStart).toFloat() + val right = (parent.run { width - paddingEnd }).toFloat() + parent.children.forEachIndexed { index, view -> + val position = parent.getChildAdapterPosition(view) + if (position == adapter.currentList.lastIndex) return@forEachIndexed + val top = view.bottom.toFloat() + val bottom = (top + heightPx) + val paint = if (position == 0) allMenuPaint else otherMenuPaint + c.drawRect(left, top, right, bottom, paint) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/item/NotificationSettingAllItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/item/NotificationSettingAllItem.kt new file mode 100644 index 00000000..300084d4 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/item/NotificationSettingAllItem.kt @@ -0,0 +1,19 @@ +package com.polzzak_android.presentation.feature.notification.setting.item + +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemNotificationSettingAllBinding +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.feature.notification.setting.model.SettingMenuType + +class NotificationSettingAllItem(val model: SettingMenuType.All) : + BindableItem() { + override val layoutRes: Int = R.layout.item_notification_setting_all + + //TODO 비교함수 구현 + override fun areItemsTheSame(other: BindableItem<*>): Boolean = false + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = false + override fun bind(binding: ItemNotificationSettingAllBinding, position: Int) { + binding.tvTitle.text = binding.root.context.getString(model.titleStringRes) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/item/NotificationSettingItem.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/item/NotificationSettingItem.kt new file mode 100644 index 00000000..068effee --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/item/NotificationSettingItem.kt @@ -0,0 +1,24 @@ +package com.polzzak_android.presentation.feature.notification.setting.item + +import com.polzzak_android.R +import com.polzzak_android.databinding.ItemNotificationSettingBinding +import com.polzzak_android.presentation.common.util.BindableItem +import com.polzzak_android.presentation.feature.notification.setting.model.SettingMenuType + +class NotificationSettingItem(val model: SettingMenuType.Menu) : + BindableItem() { + override val layoutRes: Int = R.layout.item_notification_setting + + //TODO 비교함수 구현 + override fun areItemsTheSame(other: BindableItem<*>): Boolean = false + + override fun areContentsTheSame(other: BindableItem<*>): Boolean = false + + override fun bind(binding: ItemNotificationSettingBinding, position: Int) { + with(binding) { + val context = root.context + tvTitle.text = context.getString(model.titleStringRes) + tvContent.text = context.getString(model.contentStringRes) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/model/SettingMenuModel.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/model/SettingMenuModel.kt new file mode 100644 index 00000000..0fd64927 --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/model/SettingMenuModel.kt @@ -0,0 +1,6 @@ +package com.polzzak_android.presentation.feature.notification.setting.model + +data class SettingMenuModel( + val type: SettingMenuType, + val isChecked: Boolean = false +) diff --git a/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/model/SettingMenuType.kt b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/model/SettingMenuType.kt new file mode 100644 index 00000000..b3e809bb --- /dev/null +++ b/app/src/main/java/com/polzzak_android/presentation/feature/notification/setting/model/SettingMenuType.kt @@ -0,0 +1,75 @@ +package com.polzzak_android.presentation.feature.notification.setting.model + +import androidx.annotation.StringRes +import com.polzzak_android.R + +//TODO 서버타입과 이름 맞추기(현재 임의로 이름 지음) +sealed interface SettingMenuType { + @get:StringRes + val titleStringRes: Int + + object All : SettingMenuType { + override val titleStringRes: Int = R.string.notification_setting_all_title + } + + sealed interface Menu : SettingMenuType { + @get:StringRes + val contentStringRes: Int + + object Link : Menu { + override val titleStringRes: Int = R.string.notification_setting_link_title + override val contentStringRes: Int = R.string.notification_setting_link_content + } + + object Level : Menu { + override val titleStringRes: Int = R.string.notification_setting_level_title + override val contentStringRes: Int = R.string.notification_setting_level_content + } + + object RequestStamp : Menu { + override val titleStringRes: Int = R.string.notification_setting_request_stamp_title + override val contentStringRes: Int = R.string.notification_setting_request_stamp_content + } + + object RequestGift : Menu { + override val titleStringRes: Int = R.string.notification_setting_request_gift_title + override val contentStringRes: Int = R.string.notification_setting_request_gift_content + } + + object CompleteStampBoard : Menu { + override val titleStringRes: Int = + R.string.notification_setting_complete_stamp_board_title + override val contentStringRes: Int = + R.string.notification_setting_complete_stamp_board_content + } + + object ReceiveGift : Menu { + override val titleStringRes: Int = R.string.notification_setting_receive_gift_title + override val contentStringRes: Int = R.string.notification_setting_receive_gift_content + } + + object BreakPromise : Menu { + override val titleStringRes: Int = R.string.notification_setting_break_promise_title + override val contentStringRes: Int = R.string.notification_setting_break_promise_content + } + + object NewStampBoard : Menu { + override val titleStringRes: Int = R.string.notification_setting_new_stamp_board_title + override val contentStringRes: Int = + R.string.notification_setting_new_stamp_board_content + } + + object PaymentCoupon : Menu { + override val titleStringRes: Int = R.string.notification_setting_payment_coupon_title + override val contentStringRes: Int = + R.string.notification_setting_payment_coupon_content + } + + object CheckDeliveryGift : Menu { + override val titleStringRes: Int = + R.string.notification_setting_check_delivery_gift_title + override val contentStringRes: Int = + R.string.notification_setting_check_delivery_gift_content + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_switch_off.xml b/app/src/main/res/drawable/bg_switch_off.xml new file mode 100644 index 00000000..e7a7f57c --- /dev/null +++ b/app/src/main/res/drawable/bg_switch_off.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/bg_switch_on.xml b/app/src/main/res/drawable/bg_switch_on.xml new file mode 100644 index 00000000..69ca700b --- /dev/null +++ b/app/src/main/res/drawable/bg_switch_on.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml new file mode 100644 index 00000000..b207efe8 --- /dev/null +++ b/app/src/main/res/drawable/ic_dot.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pulling.xml b/app/src/main/res/drawable/ic_pulling.xml new file mode 100644 index 00000000..e607cef8 --- /dev/null +++ b/app/src/main/res/drawable/ic_pulling.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_refresh_loading_spinner.xml b/app/src/main/res/drawable/ic_refresh_loading_spinner.xml new file mode 100644 index 00000000..37bb4472 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_loading_spinner.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_switch_thumb.xml b/app/src/main/res/drawable/ic_switch_thumb.xml new file mode 100644 index 00000000..ed973a14 --- /dev/null +++ b/app/src/main/res/drawable/ic_switch_thumb.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_bg_switch.xml b/app/src/main/res/drawable/selector_bg_switch.xml new file mode 100644 index 00000000..a9a8fa9f --- /dev/null +++ b/app/src/main/res/drawable/selector_bg_switch.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rectangle_gray_200_r8.xml b/app/src/main/res/drawable/shape_rectangle_gray_200_r8.xml new file mode 100644 index 00000000..6c43159c --- /dev/null +++ b/app/src/main/res/drawable/shape_rectangle_gray_200_r8.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rectangle_white_stroke_gray_200_r8.xml b/app/src/main/res/drawable/shape_rectangle_white_stroke_gray_200_r8.xml new file mode 100644 index 00000000..deea1e4c --- /dev/null +++ b/app/src/main/res/drawable/shape_rectangle_white_stroke_gray_200_r8.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_kid_notification.xml b/app/src/main/res/layout/fragment_kid_notification.xml index 1583ca0a..e361bdf6 100644 --- a/app/src/main/res/layout/fragment_kid_notification.xml +++ b/app/src/main/res/layout/fragment_kid_notification.xml @@ -4,7 +4,7 @@ + tools:context=".presentation.feature.notification.list.kid.KidNotificationListFragment"> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_notification_setting.xml b/app/src/main/res/layout/fragment_notification_setting.xml new file mode 100644 index 00000000..10d639ab --- /dev/null +++ b/app/src/main/res/layout/fragment_notification_setting.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_load_more_loading.xml b/app/src/main/res/layout/item_load_more_loading.xml new file mode 100644 index 00000000..cc75d2b9 --- /dev/null +++ b/app/src/main/res/layout/item_load_more_loading.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification.xml b/app/src/main/res/layout/item_notification.xml new file mode 100644 index 00000000..9977f1d8 --- /dev/null +++ b/app/src/main/res/layout/item_notification.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_empty.xml b/app/src/main/res/layout/item_notification_empty.xml new file mode 100644 index 00000000..594caa23 --- /dev/null +++ b/app/src/main/res/layout/item_notification_empty.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_loading_skeleton.xml b/app/src/main/res/layout/item_notification_loading_skeleton.xml new file mode 100644 index 00000000..dada7d6b --- /dev/null +++ b/app/src/main/res/layout/item_notification_loading_skeleton.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_refresh.xml b/app/src/main/res/layout/item_notification_refresh.xml new file mode 100644 index 00000000..81de765f --- /dev/null +++ b/app/src/main/res/layout/item_notification_refresh.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_setting.xml b/app/src/main/res/layout/item_notification_setting.xml new file mode 100644 index 00000000..ecbd7b27 --- /dev/null +++ b/app/src/main/res/layout/item_notification_setting.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_setting_all.xml b/app/src/main/res/layout/item_notification_setting_all.xml new file mode 100644 index 00000000..1ea6abdd --- /dev/null +++ b/app/src/main/res/layout/item_notification_setting_all.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/kid_nav_graph.xml b/app/src/main/res/navigation/kid_nav_graph.xml index 0ffb1765..7948a3e3 100644 --- a/app/src/main/res/navigation/kid_nav_graph.xml +++ b/app/src/main/res/navigation/kid_nav_graph.xml @@ -40,8 +40,17 @@ app:startDestination="@id/kidNotificationFragment"> + android:name="com.polzzak_android.presentation.feature.notification.list.kid.KidNotificationListFragment" + android:label="KidNotificationFragment"> + + + + diff --git a/app/src/main/res/navigation/protector_nav_graph.xml b/app/src/main/res/navigation/protector_nav_graph.xml index a14cd140..3a06a417 100644 --- a/app/src/main/res/navigation/protector_nav_graph.xml +++ b/app/src/main/res/navigation/protector_nav_graph.xml @@ -18,12 +18,23 @@ android:id="@+id/protectorCouponFragment" android:name="com.polzzak_android.presentation.feature.coupon.main.protector.ProtectorCouponFragment" android:label="ProtectorCouponFragment" - tools:layout="@layout/fragment_protector_coupon"/> + tools:layout="@layout/fragment_protector_coupon" /> + tools:layout="@layout/fragment_protector_notification"> + + + + + + + tools:layout="@layout/fragment_make_stamp"> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index eca37557..4c162edd 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -31,6 +31,7 @@ #59B9FF + #00000000 #66000000 #99000000 #CC000000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc51ea05..633fde5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ 연동 신청 수락 거절 + 알림 참 잘 했어요 도장 쾅! @@ -119,4 +120,43 @@ 닉네임 검색 이미 해당 회원에게 연동 요청을 받았어요 이미 연동됐어요 + + + 💌 + 연동 요청 + %s님이 회원님께 연동 요청을 보냈어요 + 🤝🏻 + 연동 완료 + %s님과 연동이 완료되었어요! 도장판을 만들러 가볼까요? :) + 🥳 + 레벨 UP + 폴짝! %s으로 올라갔어요! + 🚨 + 레벨 DOWN + 조심! %s으로 내려왔어요 + 알림이 없어요 + 알림 설정 + 모든 알림 + 연동 알림 + 연동 요청이 들어오거나 연동에 성공한 경우 알림을 받을래요 + 레벨 알림 + 레벨 변동에 대한 알림을 받을래요 + 도장요청 알림 + 아이의 도장 요청 알림을 받을래요 + 선물 조르기 알림 + 아이의 선물 조르기 알림을 받을래요 + 도장판 완성 알림 + 도장판이 모두 채워졌다는 알림을 받을래요 + 선물 수령 알림 + 아이가 선물을 받았다고 보내는 감사 인사를 받을래요 + 선물 약속 미이행 알림 + 선물 약속 날짜를 어겼다는 알림을 받을래요 + 새로운 도장판 알림 + 새로운 도장판이 만들어졌다는 알림을 받을래요 + 쿠폰 발급 알림 + 보호자가 선물 쿠폰을 발급해줬다는 알림을 받을래요 + 선물 전달 확인 알림 + 선물 전달이 완료되었는지 확인하는 알림을 받을래요 + + \ No newline at end of file diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml index 8f75ee5b..cc9d54b3 100644 --- a/app/src/main/res/values/style.xml +++ b/app/src/main/res/values/style.xml @@ -110,6 +110,11 @@ @font/pretendard_medium + +