-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 약관동의 바텀시트 및 팝업 UI 구현 #577
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
Changes from all commits
3b842f0
09d9271
3e8c071
b0b2f1a
df3d8ac
a45cbde
5eea022
a070c9d
f7a3b06
e06c169
3109354
2820bf7
f50c604
34c7fba
a546646
af0b82e
61b0564
f1bb411
ecc8417
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.into.websoso.ui.main.home.dialog | ||
|
|
||
| import android.os.Bundle | ||
| import android.view.View | ||
| import com.into.websoso.R | ||
| import com.into.websoso.core.common.ui.base.BaseDialogFragment | ||
| import com.into.websoso.databinding.DialogTermsAgreementPopupMenuBinding | ||
| import com.into.websoso.ui.termsAgreement.TermsAgreementDialogBottomSheet | ||
|
|
||
| class TermsAgreementDialogFragment : | ||
| BaseDialogFragment<DialogTermsAgreementPopupMenuBinding>(R.layout.dialog_terms_agreement_popup_menu) { | ||
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
| isCancelable = false | ||
| onTermsAgreementPopupMenuUpdateClick() | ||
| } | ||
|
|
||
| private fun onTermsAgreementPopupMenuUpdateClick() { | ||
| binding.tvTermsAgreementPopupMenuUpdate.setOnClickListener { | ||
| showTermsAgreementBottomSheet() | ||
| dismiss() | ||
| } | ||
| } | ||
|
|
||
| private fun showTermsAgreementBottomSheet() { | ||
| TermsAgreementDialogBottomSheet.newInstance(isFromHome = true) | ||
| .show(parentFragmentManager, "TermsAgreementDialogBottomSheet") | ||
| } | ||
|
|
||
| companion object { | ||
| const val TERMS_AGREEMENT_TAG = "TermsAgreementDialog" | ||
|
|
||
| fun newInstance(): TermsAgreementDialogFragment { | ||
| return TermsAgreementDialogFragment() | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| package com.into.websoso.ui.termsAgreement | ||
|
|
||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import android.os.Bundle | ||
| import android.view.View | ||
| import androidx.core.os.bundleOf | ||
| import androidx.fragment.app.viewModels | ||
| import com.google.android.material.bottomsheet.BottomSheetBehavior | ||
| import com.google.android.material.bottomsheet.BottomSheetDialog | ||
| import com.into.websoso.R | ||
| import com.into.websoso.R.drawable.bg_novel_rating_date_primary_100_radius_12dp | ||
| import com.into.websoso.R.drawable.bg_profile_edit_gray_70_radius_12dp | ||
| import com.into.websoso.R.drawable.ic_terms_agreement_selected | ||
| import com.into.websoso.R.drawable.ic_terms_agreement_unselected | ||
| import com.into.websoso.R.string.string_terms_agreement_complete | ||
| import com.into.websoso.R.string.string_terms_agreement_next | ||
| import com.into.websoso.core.common.ui.base.BaseBottomSheetDialog | ||
| import com.into.websoso.core.common.util.collectWithLifecycle | ||
| import com.into.websoso.databinding.DialogTermsAgreementBinding | ||
| import com.into.websoso.ui.termsAgreement.model.AgreementType | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
|
|
||
| @AndroidEntryPoint | ||
| class TermsAgreementDialogBottomSheet : | ||
| BaseBottomSheetDialog<DialogTermsAgreementBinding>(R.layout.dialog_terms_agreement) { | ||
|
|
||
| private val termsAgreementViewModel: TermsAgreementViewModel by viewModels() | ||
|
|
||
| override fun onViewCreated( | ||
| view: View, | ||
| savedInstanceState: Bundle?, | ||
| ) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
|
|
||
| setupBottomSheetDialog() | ||
| onRequiredTermsAgreementClick() | ||
| onTermsAgreementToggleClick() | ||
| onTermsAgreementCompleteButtonClick() | ||
| setupViewModel() | ||
| updateCompleteButtonText() | ||
| } | ||
|
|
||
| private fun setupBottomSheetDialog() { | ||
| (dialog as BottomSheetDialog).apply { | ||
| behavior.state = BottomSheetBehavior.STATE_EXPANDED | ||
| behavior.isDraggable = false | ||
| behavior.isHideable = false | ||
| setCancelable(false) | ||
| } | ||
| } | ||
|
|
||
| private fun onRequiredTermsAgreementClick() { | ||
| binding.tvTermsAgreementService.setOnClickListener { | ||
| startActivity( | ||
| Intent( | ||
| Intent.ACTION_VIEW, | ||
| Uri.parse(getString(R.string.terms_agreement_service)), | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| binding.tvTermsAgreementPrivacy.setOnClickListener { | ||
| startActivity( | ||
| Intent( | ||
| Intent.ACTION_VIEW, | ||
| Uri.parse(getString(R.string.terms_agreement_privacy)), | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun onTermsAgreementToggleClick() { | ||
| binding.ivTermsAgreementAll.setOnClickListener { termsAgreementViewModel.updateTermsAgreementsAll() } | ||
|
|
||
| binding.ivTermsAgreementService.setOnClickListener { | ||
| termsAgreementViewModel.updateTermsAgreements( | ||
| AgreementType.SERVICE, | ||
| ) | ||
| } | ||
|
|
||
| binding.ivTermsAgreementPrivacy.setOnClickListener { | ||
| termsAgreementViewModel.updateTermsAgreements( | ||
| AgreementType.PRIVACY, | ||
| ) | ||
| } | ||
|
|
||
| binding.ivTermsAgreementMarketing.setOnClickListener { | ||
| termsAgreementViewModel.updateTermsAgreements( | ||
| AgreementType.MARKETING, | ||
| ) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| private fun onTermsAgreementCompleteButtonClick() { | ||
| binding.btnTermsAgreementComplete.setOnClickListener { dismissIfAgreementsCompleted() } | ||
| } | ||
|
|
||
| private fun dismissIfAgreementsCompleted() { | ||
| if (termsAgreementViewModel.isRequiredAgreementsChecked.value) { | ||
| dismiss() | ||
| } | ||
| } | ||
|
|
||
| private fun setupViewModel() { | ||
| termsAgreementViewModel.agreementStatus.collectWithLifecycle(viewLifecycleOwner) { status -> | ||
| updateAgreementIcons(status) | ||
| updateAllAgreementIcon(status.values.all { it }) | ||
| } | ||
|
|
||
| termsAgreementViewModel.isRequiredAgreementsChecked.collectWithLifecycle(viewLifecycleOwner) { | ||
| updateCompleteButtonState(it) | ||
| } | ||
| } | ||
|
|
||
| private fun updateAgreementIcons(status: Map<AgreementType, Boolean>) { | ||
| binding.apply { | ||
| ivTermsAgreementService.setImageResource(getToggleIcon(status[AgreementType.SERVICE] == true)) | ||
| ivTermsAgreementPrivacy.setImageResource(getToggleIcon(status[AgreementType.PRIVACY] == true)) | ||
| ivTermsAgreementMarketing.setImageResource(getToggleIcon(status[AgreementType.MARKETING] == true)) | ||
| } | ||
| } | ||
|
|
||
| private fun getToggleIcon(isChecked: Boolean): Int { | ||
| return if (isChecked) ic_terms_agreement_selected else ic_terms_agreement_unselected | ||
| } | ||
|
|
||
| private fun updateAllAgreementIcon(isChecked: Boolean) { | ||
| binding.ivTermsAgreementAll.setImageResource( | ||
| if (isChecked) ic_terms_agreement_selected else ic_terms_agreement_unselected, | ||
| ) | ||
| } | ||
|
|
||
| private fun updateCompleteButtonState(isEnabled: Boolean) { | ||
| binding.btnTermsAgreementComplete.setBackgroundResource( | ||
| when (isEnabled) { | ||
| true -> bg_novel_rating_date_primary_100_radius_12dp | ||
| false -> bg_profile_edit_gray_70_radius_12dp | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| private fun updateCompleteButtonText() { | ||
| val isFromHome = arguments?.getBoolean(IS_FROM_HOME_TAG, false) ?: false | ||
|
|
||
| binding.btnTermsAgreementComplete.text = | ||
| if (isFromHome) getString(string_terms_agreement_complete) | ||
| else getString(string_terms_agreement_next) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val IS_FROM_HOME_TAG = "IS_FROM_HOME" | ||
|
|
||
| fun newInstance(isFromHome: Boolean = false): TermsAgreementDialogBottomSheet { | ||
| return TermsAgreementDialogBottomSheet().apply { | ||
| arguments = bundleOf(IS_FROM_HOME_TAG to isFromHome) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package com.into.websoso.ui.termsAgreement | ||
|
|
||
| import androidx.lifecycle.ViewModel | ||
| import androidx.lifecycle.viewModelScope | ||
| import com.into.websoso.ui.termsAgreement.model.AgreementType | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.SharingStarted | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.stateIn | ||
| import kotlinx.coroutines.flow.update | ||
| import javax.inject.Inject | ||
|
|
||
| @HiltViewModel | ||
| class TermsAgreementViewModel @Inject constructor() : ViewModel() { | ||
|
|
||
| private val _agreementStatus = MutableStateFlow( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. c: 전반적으로 StateFlow를 적용하신 이유가 궁금합니다!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. stateflow를 사용한 이유는 최신 상태를 유지하면서도 flow 기반의 비동기 처리가 가능하기 때문입니다 livedata는 생명주기가 활성 상태 이상일 때만 데이터를 전달하고 비활성 상태일 때는 업데이트를 보류하게 됩니다! 하지만 약관 동의 상태는 ui가 보이지 않더라도 계속 유지되어야해서 livedata보다는 stateflow가 더 적절하다고 판단했습니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㄴ 사실 현재 코드로는 StateFlow를 사용한다고 해서 얻는 이점은 없어요! StateFlow 또한 결국 View Lifecycle에 의존시켜야하고, 현재 구현상 별다른 Flow 비동기처리를 하고 있지 않기 때문입니다. |
||
| mapOf( | ||
| AgreementType.SERVICE to false, | ||
| AgreementType.PRIVACY to false, | ||
| AgreementType.MARKETING to false, | ||
| ), | ||
| ) | ||
| val agreementStatus: StateFlow<Map<AgreementType, Boolean>> = _agreementStatus.asStateFlow() | ||
|
|
||
| val isAllChecked: StateFlow<Boolean> = agreementStatus | ||
| .map { it.values.all { checked -> checked } } | ||
| .stateIn(viewModelScope, SharingStarted.Lazily, false) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. c: stateIn 확장함수가 필요한가요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 음 전체동의를 눌렀을 때 밑에 약관동의 항목 3가지 모두가 체크상태로 변경되어야 하고 반대로 개별적으로 체크가 모두 되어지는 경우에도 전체동의가 활성화가 되어야해서 즉각적으로 반응하기 위해서 사용을 했는데 다른 대안이나 불필요하다고 여겨지는 부분이 있으신가요?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreementStatus를 활용해 전체가 true인지 확인하는 로직으로 보여집니다. |
||
|
|
||
| val isRequiredAgreementsChecked: StateFlow<Boolean> = agreementStatus | ||
| .map { isRequiredAgreementChecked(it) } | ||
| .stateIn(viewModelScope, SharingStarted.Lazily, false) | ||
|
|
||
| private fun isRequiredAgreementChecked(status: Map<AgreementType, Boolean>): Boolean { | ||
| return status[AgreementType.SERVICE] == true && status[AgreementType.PRIVACY] == true | ||
| } | ||
|
|
||
| fun updateTermsAgreementsAll() { | ||
| val newStatus = _agreementStatus.value.values.any { !it } | ||
| _agreementStatus.update { it.mapValues { _ -> newStatus } } | ||
| } | ||
|
|
||
| fun updateTermsAgreements(agreementType: AgreementType) { | ||
| _agreementStatus.value[agreementType]?.let { currentValue -> | ||
| _agreementStatus.value = _agreementStatus.value.toMutableMap().apply { | ||
| this[agreementType] = !currentValue | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.into.websoso.ui.termsAgreement.model | ||
|
|
||
| enum class AgreementType { | ||
| SERVICE, | ||
| PRIVACY, | ||
| MARKETING, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:width="24dp" | ||
| android:height="24dp" | ||
| android:viewportWidth="24" | ||
| android:viewportHeight="24"> | ||
| <path | ||
| android:pathData="M12,0C5.373,0 0,5.373 0,12C0,18.627 5.373,24 12,24C18.627,24 24,18.627 24,12C24,5.373 18.627,0 12,0Z" | ||
| android:fillColor="#6A5DFD"/> | ||
| <path | ||
| android:pathData="M18.472,7.265C18.663,7.456 18.77,7.715 18.77,7.985C18.77,8.254 18.663,8.513 18.472,8.704L10.845,16.332C10.744,16.433 10.624,16.513 10.493,16.567C10.361,16.622 10.22,16.65 10.077,16.65C9.934,16.65 9.793,16.622 9.662,16.567C9.53,16.513 9.41,16.433 9.309,16.332L5.52,12.543C5.422,12.449 5.345,12.337 5.292,12.212C5.238,12.088 5.21,11.955 5.209,11.819C5.208,11.684 5.234,11.55 5.285,11.425C5.336,11.3 5.411,11.187 5.507,11.091C5.603,10.995 5.716,10.92 5.841,10.869C5.966,10.818 6.1,10.792 6.235,10.793C6.371,10.794 6.504,10.822 6.628,10.876C6.752,10.929 6.865,11.007 6.959,11.104L10.077,14.222L17.033,7.265C17.127,7.171 17.24,7.096 17.363,7.044C17.486,6.993 17.619,6.967 17.753,6.967C17.886,6.967 18.019,6.993 18.142,7.044C18.266,7.096 18.378,7.171 18.472,7.265Z" | ||
| android:fillColor="#F1EFFF" | ||
| android:fillType="evenOdd"/> | ||
| </vector> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:width="24dp" | ||
| android:height="24dp" | ||
| android:viewportWidth="24" | ||
| android:viewportHeight="24"> | ||
| <path | ||
| android:pathData="M12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12C0,5.373 5.373,0 12,0Z" | ||
| android:fillColor="#DDDDE3" | ||
| android:fillType="evenOdd"/> | ||
| <path | ||
| android:pathData="M18.473,7.265C18.664,7.456 18.771,7.715 18.771,7.985C18.771,8.254 18.664,8.513 18.473,8.704L10.846,16.332C10.745,16.433 10.625,16.513 10.493,16.567C10.362,16.622 10.221,16.65 10.078,16.65C9.935,16.65 9.794,16.622 9.663,16.567C9.531,16.513 9.411,16.433 9.31,16.332L5.521,12.543C5.423,12.449 5.346,12.337 5.293,12.212C5.239,12.088 5.211,11.955 5.21,11.819C5.209,11.684 5.235,11.55 5.286,11.425C5.337,11.3 5.412,11.187 5.508,11.091C5.604,10.995 5.717,10.92 5.842,10.869C5.967,10.818 6.101,10.792 6.236,10.793C6.372,10.794 6.505,10.822 6.629,10.876C6.753,10.929 6.866,11.007 6.96,11.104L10.078,14.222L17.034,7.265C17.128,7.171 17.24,7.096 17.364,7.044C17.487,6.993 17.62,6.967 17.754,6.967C17.887,6.967 18.02,6.993 18.143,7.044C18.267,7.096 18.379,7.171 18.473,7.265Z" | ||
| android:fillColor="#EEEEF2" | ||
| android:fillType="evenOdd"/> | ||
| </vector> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a: 뎁스를 최대한 줄이면서 단일 책임을 가져가려는 모습 좋네요 👍