[Android/Kotlin] 커스텀 NumberPicker 사용하기

코코아의 개발일지·2023년 10월 7일
1

Android-Kotlin

목록 보기
12/36
post-thumbnail

✍🏻 요구사항 분석

이런 식으로 알림 시간을 설정해줘야 했는데,

  1. 오전/오후
  2. 1시 ~ 12시
  3. 00분 or 30분

으로 설정되어야 하는데, 그러려면 넘버 피커가 3개 필요했다. 스타일도 설정해줘야 해서 꽤 힘들었던 기억이 있다.

알림 기본 시간은 hint로 표현했는데, 이 또한 hint 텍스트를 읽어들여 넘버피커에 넣을 값으로 변환해줘야 했고, 힌 번 선택한 시간에 대해서는 알림 시간을 바꾸려 들어갔을 때 똑같은 시간의 넘버피커를 띄워줘야 했다.


💻 코드 작성

1️⃣ 넘버피커 스타일 지정

<style name="AppTheme.NumberPicker" parent="">
        <item name="android:textColorPrimary">@color/white</item>
        <item name="android:textColorHighlight">@color/gray_700</item>
        <item name="android:textSize">20sp</item>
        <item name="selectableItemBackground">@color/Jblue</item>
        <item name="colorControlNormal">@color/colorPrimary</item>
</style>

2️⃣ 넘버피커 화면 만들기 (dialog_alertpicker.xml)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/bottom_nav_bg"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/period_done_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="16dp"
        android:paddingVertical="8dp"
        android:text="Done"
        android:textSize="20sp"
        android:textColor="@color/Jblue"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:orientation="horizontal"
        android:background="@color/gray_700"
        app:layout_constraintTop_toBottomOf="@id/period_done_tv"
        app:layout_constraintStart_toStartOf="parent">

        <NumberPicker
            android:id="@+id/alert_datepicker_ampm"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_marginStart="50dp"
            android:layout_marginEnd="20dp"
            android:selectionDividerHeight="1dp"
            android:theme="@style/AppTheme.NumberPicker"
            tools:targetApi="q" />
        <NumberPicker
            android:id="@+id/alert_datepicker_hour"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:selectionDividerHeight="1dp"
            android:theme="@style/AppTheme.NumberPicker"
            tools:targetApi="q" />
        <NumberPicker
            android:id="@+id/alert_datepicker_minute"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_marginStart="20dp"
            android:layout_marginEnd="50dp"
            android:selectionDividerHeight="1dp"
            android:theme="@style/AppTheme.NumberPicker"
            tools:targetApi="q" />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>


앞선 1에서 만든 Number 스타일을 지정하면 위와 같은 식으로 보인다.
저기 오른쪽 화면에 +가 3개 보이는 것은 각각

  1. 오전/오후
  2. 1시 ~ 12시
  3. 00분 or 30분

넘버피커 자리이다.

3️⃣ 넘버피커 설정하기 (AlertPickerDialog.kt)

넘버피커 값 넣어주기 + 시간 선택하고 보여주기 작업

interface AlertPickerDialogInterface {
    fun onClickDoneButton(id: Int, meridiem: Int, hour: Int, minute: Int)
}

@RequiresApi(Build.VERSION_CODES.O)
class AlertPickerDialog(
    pickerDialogInterface: AlertPickerDialogInterface,
    id: Int,
    meridiem: Int, hour: Int, minute: Int
) : BottomSheetDialogFragment() {
    private lateinit var binding: DialogAlertpickerBinding

    // Initializing a new string array with elements
    private val meridiemArr = arrayOf("오전", "오후") // am, pm
    private val hoursArr = Array(12) { (it + 1).toString() }
    private val minutesArr = arrayOf("00", "30", "00", "30")

    private var pickerDialogInterface: AlertPickerDialogInterface? = null
    private var id: Int? = null

    // 선택된 값
    private var meridiem: Int? = null
    private var hour: Int? = null
    private var minute: Int? = null

    init {
        this.meridiem = meridiem
        this.hour = hour
        this.minute = minute
        this.id = id
        this.pickerDialogInterface = pickerDialogInterface
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DialogAlertpickerBinding.inflate(inflater, container, false)
        val ampmPicker = binding.alertDatepickerAmpm
        val hoursPicker = binding.alertDatepickerHour
        val minutesPicker = binding.alertDatepickerMinute

        // Done 버튼 눌러서 창 닫기
        binding.periodDoneTv.setOnClickListener {
            // 값 가져오기
            meridiem = ampmPicker.value
            hour = hoursPicker.value
            minute = minutesPicker.value

            this.pickerDialogInterface?.onClickDoneButton(id!!, meridiem!!, hour!!, minute!!)
            // 닫기
            dismiss()
        }

        //  순환 안되게 막기
        ampmPicker.wrapSelectorWheel = false

        //  editText 설정 해제
        ampmPicker.descendantFocusability = NumberPicker.FOCUS_BLOCK_DESCENDANTS

        //  최소값 설정
        ampmPicker.minValue = 0
        hoursPicker.minValue = 1
        minutesPicker.minValue = 0

        //  최대값 설정
        ampmPicker.maxValue = meridiemArr.size - 1
        hoursPicker.maxValue = 12
        minutesPicker.maxValue = minutesArr.size - 1

        //  array 값 넣기
        ampmPicker.displayedValues = meridiemArr
        hoursPicker.displayedValues = hoursArr
        minutesPicker.displayedValues = minutesArr

        // 선택된 기본값 설정
        ampmPicker.value = meridiem!!
        hoursPicker.value = hour!!
        minutesPicker.value = minute!!

        return binding.root
    }
}

4️⃣ 넘버피커 띄우기 (AlertLoginFragment.kt)

1. 기본 시간 설정하기

넘버피커를 나오게 할 화면이다.
계획 세우기, 계획 실행 알람에 대해

  1. 오전/오후
  2. 1시 ~ 12시
  3. 00분 or 30분

를 모두 전역 변수로 선언했다.
오전/오후는 0과 1로 구분하고,
기본 알람 시간이 계획 세우기는 오전 9시고, 계획 실행은 오후 9시이기에 해당 값으로 초기화해주었다.

// 계획 세우기
private var planMeridiem: Int = 0
private var planHour: Int = 9
private var planMinute: Int = 0

// 계획 실행
private var actionMeridiem: Int = 1
private var actionHour: Int = 9
private var actionMinute: Int = 0

2. 서버에 보낼 전역 변수 선언하기

또한, 서버에 보낼 때는 계획 세우기와 실행 시간 형태를

{
  "planTime": "string", // 계획 세우기 알림 시간(hh:mm:00 24h)
  "actionTime": "string"// 실전 확인 알림 시간(hh:mm:00 24h)
}

형태로 보내주어야 했기에 해당 부분은

// 서버에 보낼 값
private lateinit var alertTime: AlertBody
private var planTime = ""
private var actionTime = ""

로 String 형태의 전역 변수를 만들어줬다.

3. EditText를 클릭했을 때 넘버피커 띄우기

그리고 계획 세우기와 계획 실행 알림 시간을 보여주는 부분은 모두 EditText로 이루어져 있는데, 이를 클릭할 때 현재 설정된 오전/오후, 시간, 분을 앞선 3번에서 만든 AlertPickerDialog에 전달해 주었다.

// 계획 세우기 알림
binding.myAlertMakingPlanEt.setOnClickListener {
	// 알림 dialog
	val dialog = AlertPickerDialog(this@AlertLoginFragment, 0, planMeridiem, planHour, planMinute)
            activity?.supportFragmentManager?.let { it1 -> dialog.show(it1, dialog.tag) }
}

// 계획 실행 알림
binding.myAlertCarryingPlanEt.setOnClickListener {
	// 알림 dialog
	val dialog = AlertPickerDialog(this@AlertLoginFragment, 1, actionMeridiem, actionHour, actionMinute)
            activity?.supportFragmentManager?.let { it1 -> dialog.show(it1, dialog.tag) }
}

계획 세우기는 태그를 0으로, 계획 시간은 1로 넘겨 넘버피커에서 둘을 구별할 수 있게 하였다.

4. 넘버피커로 시간을 설정했을 때의 값을 받아서 EditText에 알림 시간 보여주기

class AlertLoginFragment : BaseFragment<FragmentLoginAlertBinding> (FragmentLoginAlertBinding::bind, R.layout.fragment_login_alert),
AlertPickerDialogInterface {

override fun onClickDoneButton(id: Int, meridiem: Int, hour: Int, minute: Int) {
        val ampm = if(meridiem == 0) "오전" else "오후"
        val min = if(minute % 2 == 0) "00" else "30"

        if (id == 0) { // 계획 세우기 알림
            // 선택된 value 저장
            planMeridiem = meridiem
            planHour = hour
            planMinute = minute

            // 텍스트 반영
            binding.myAlertMakingPlanEt.setText("${ampm} ${hour}${min}분")

        } else { // 실천 확인 알림
            actionMeridiem = meridiem
            actionHour = hour
            actionMinute = minute

            binding.myAlertCarryingPlanEt.setText("${ampm} ${hour}${min}분")
        }
    }
}

앞서 전역변수에 썼던 계획 세우기 알림, 실천 확인 알림 변수에 각각 넘버피커의 결과로 받아온 값(오전/오후, 시간, 분)을 넣어준다. 이렇게 다시 받아온 값을 통해, 다시 넘버피커를 열더라도 이전에 선택했던 시간으로 보여줄 수 있다.
인터페이스로부터 넘겨받은 id가 0이면 계획 세우기 알림이므로 해당 EditText를 업데이트하고, 아니라면 실천 확인 알림 EditText를 업데이트한다.

5. 서버에 보낼 형태로 받아온 값을 저장하기

  • 서버에 보낼 형태
{
  "planTime": "string", // 계획 세우기 알림 시간(hh:mm:00 24h)
  "actionTime": "string"// 실전 확인 알림 시간(hh:mm:00 24h)
}
  • 해당 형태대로 만들어주는 함수. 서버 통신 버튼을 클릭했을 때 값을 넣어 보내준다.
private fun putUserAlert() {
        // 조합
        var putPlanHour = if (planMeridiem == 1) planHour + 12 else planHour
        if (planHour == 12) { // 정오, 자정 처리
            putPlanHour -= 12
        }
        val planHourStr = if (putPlanHour < 10) "0$putPlanHour" else putPlanHour.toString()
        val planMinStr = if (planMinute == 0) "00" else "30"

        var putActionHour = if (actionMeridiem == 1) actionHour + 12 else actionHour
        if (actionHour == 12) {
            putActionHour -= 12
        }
        val actionHourStr = if (putActionHour < 10) "0$putActionHour" else putActionHour.toString()
        val actionMinStr = if (actionMinute == 0) "00" else "30"

        planTime = "${planHourStr}:${planMinStr}:00"
        actionTime = "${actionHourStr}:${actionMinStr}:00"

        alertTime = AlertBody(planTime, actionTime)

        Log.d("alertTime", alertTime.toString())
    }

(전체)

class AlertLoginFragment : BaseFragment<FragmentLoginAlertBinding> (FragmentLoginAlertBinding::bind, R.layout.fragment_login_alert),
AlertPickerDialogInterface {

    // 계획 세우기
    private var planMeridiem: Int = 0
    private var planHour: Int = 9
    private var planMinute: Int = 0

    // 계획 실행
    private var actionMeridiem: Int = 1
    private var actionHour: Int = 9
    private var actionMinute: Int = 0

    // 서버에 보낼 값
    private lateinit var alertTime: AlertBody
    private var planTime = ""
    private var actionTime = ""

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        initView()
        setOnClickListeners()

        // 푸시 알림 권한 여부 확인
        checkPermission()
    }

    private fun putUserAlert() {
        // 조합
        var putPlanHour = if (planMeridiem == 1) planHour + 12 else planHour
        if (planHour == 12) { // 정오, 자정 처리
            putPlanHour -= 12
        }
        val planHourStr = if (putPlanHour < 10) "0$putPlanHour" else putPlanHour.toString()
        val planMinStr = if (planMinute == 0) "00" else "30"

        var putActionHour = if (actionMeridiem == 1) actionHour + 12 else actionHour
        if (actionHour == 12) {
            putActionHour -= 12
        }
        val actionHourStr = if (putActionHour < 10) "0$putActionHour" else putActionHour.toString()
        val actionMinStr = if (actionMinute == 0) "00" else "30"

        planTime = "${planHourStr}:${planMinStr}:00"
        actionTime = "${actionHourStr}:${actionMinStr}:00"

        alertTime = AlertBody(planTime, actionTime)

        Log.d("alertTime", alertTime.toString())
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun setOnClickListeners() {
        // 뒤로가기
        ...
        // 다음 버튼
        ...
        // 계획 세우기 알림
        binding.myAlertMakingPlanEt.setOnClickListener {
            // 알림 dialog
            val dialog = AlertPickerDialog(this@AlertLoginFragment, 0, planMeridiem, planHour, planMinute)
            activity?.supportFragmentManager?.let { it1 -> dialog.show(it1, dialog.tag) }
        }
        // 계획 실행 알림
        binding.myAlertCarryingPlanEt.setOnClickListener {
            // 알림 dialog
            val dialog = AlertPickerDialog(this@AlertLoginFragment, 1, actionMeridiem, actionHour, actionMinute)
            activity?.supportFragmentManager?.let { it1 -> dialog.show(it1, dialog.tag) }
        }
    }

    private fun activateRegisterBtn() {
        with(binding) {
            val planAlert = myAlertMakingPlanEt.text?.count()
            val actionAlert = myAlertCarryingPlanEt.text?.count()

            if (planAlert!! > 0 && actionAlert!! > 0) {
                nextButtonEnabled(true)
            } else nextButtonEnabled(false)
        }
    }

    private fun editTextChangedListener() {
        with(binding) {
            val watcher = MyEditWatcher()

            // 계획 세우기 알림
            myAlertMakingPlanEt.addTextChangedListener(watcher)
            // 실천 확인 알림
            myAlertCarryingPlanEt.addTextChangedListener(watcher)
        }
    }

서버 통신과 넘버피커와 직접적인 관련이 없는 부분은 빼고 첨부한다.


😎 느낀 점

정말 재미있었다. 생각할 부분이 많긴 했지만, 그리고 전역 변수가 많아 그다지 효율적인 코드라고 생각하지는 않지만 그래도 여러 조건을 생각하고 값을 원하는 형태로 바꿔 보여주면서 꽤 많은 생각을 하게 되었다. 푸시 알림도, 넘버 피커도 처음이었지만 그래서 더 의미있던 경험이었다.


📚 참고 자료

profile
안드로이드 개발자를 꿈꾸는 학생입니다

0개의 댓글