[Android / Kotlin] DialogFragment 커스텀 대화상자

Subeen·2024년 1월 21일
0

Android

목록 보기
44/73

주소록 팀과제를 시작할 때 우리만의 주소록 앱을 만들기 위해서는 어떠한 새로운 기능이 필요할까? 에 대해 얘기를 나눴었다. 처음에는 색깔 태그로 그룹을 관리하자는 얘기가 나왔다가 더 업그레이드 하여 사용자가 등록하는 이미지로 그룹을 관리해보기로 하였다.
그러기 위해 사용자가 새로운 태그를 등록할 때 DialogFragment로 대화상자를 띄워 그 안에서 태그 이미지를 불러오고 태그명을 입력 받을 수 있게 구현하였다.
이 과정에서 이미지 권한 관련 문제가 발생하여 애를 먹었는데, 이 부분은 다음 포스팅에 이어서 작성해보겠다.

결과 화면

layout

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

    <ImageView
        android:id="@+id/iv_going_backwards"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="6dp"
        android:src="@drawable/ic_arrow_left"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_dialog_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:text="@string/favorite_tag_add"
        android:textSize="16sp"
        app:layout_constraintBottom_toTopOf="@id/iv_register_tag_image"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        android:background="@color/dark_gray"
        app:layout_constraintTop_toBottomOf="@id/tv_dialog_title" />

    <ImageView
        android:id="@+id/iv_register_tag_image"
        android:layout_width="65dp"
        android:layout_height="65dp"
        android:layout_marginTop="16dp"
        android:background="@drawable/background_shape_oval"
        android:scaleType="centerCrop"
        android:src="@drawable/ic_add_photo"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_dialog_title" />

    <TextView
        android:id="@+id/tv_tag_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="32dp"
        android:text="@string/favorite_tag_name"
        android:textSize="16sp"
        app:layout_constraintEnd_toStartOf="@id/input_tag_name"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_register_tag_image" />


    <EditText
        android:id="@+id/input_tag_name"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="32dp"
        android:background="@drawable/background_radius_black"
        android:gravity="center"
        android:hint="@string/favorite_edittext_hint"
        android:maxLength="6"
        android:padding="6dp"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="@id/tv_tag_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/tv_tag_name"
        app:layout_constraintTop_toTopOf="@id/tv_tag_name" />

    <TextView
        android:id="@+id/tv_text_length"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="14sp"
        app:layout_constraintEnd_toStartOf="@id/tv_max_length"
        app:layout_constraintTop_toBottomOf="@id/input_tag_name" />

    <TextView
        android:id="@+id/tv_max_length"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="40dp"
        android:text="/6"
        android:textSize="14sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/input_tag_name" />

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_register_tag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="32dp"
        android:background="@drawable/custom_button"
        android:enabled="false"
        android:text="@string/favorite_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/input_tag_name" />

</androidx.constraintlayout.widget.ConstraintLayout>

Activity

class FavoriteTagDialogFragment : DialogFragment() {
    companion object {
        const val MAX_LENGTH_NAME = 6
    }

    private val binding: FragmentDialogFavoriteTagBinding by lazy {
        FragmentDialogFavoriteTagBinding.inflate(layoutInflater)
    }

    // Fragment에서 태그 등록 이벤트를 수신하기 위해 인터페이스 정의
    interface OnRegisterTagListener {
        fun onRegisterTag(name: String, file: File)
    }

    private var listener: OnRegisterTagListener? = null

    private lateinit var galleryResultLauncher: ActivityResultLauncher<Intent>

    private var selectedImage: Uri? = null

    fun setOnTagAddListener(listener: OnRegisterTagListener) {
        this.listener = listener
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        initView()
    }

    private fun initView() {
        binding.inputTagName.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(
                charSequence: CharSequence?,
                start: Int,
                before: Int,
                count: Int
            ) = Unit

            override fun onTextChanged(
                charSequence: CharSequence?,
                start: Int,
                before: Int,
                count: Int
            ) {
                onButtonEnabled()
                updateTextLength(charSequence)
                updateTextColor(charSequence)
            }

            override fun afterTextChanged(editable: Editable?) = Unit
        })

        // 갤러리에서 이미지를 볼러오는 결과에 대한 처리
        galleryResultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()
        ) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                val data: Intent? = result.data
                // 이미지 uri
                selectedImage = data?.data!!
                // 이미지의 실제 경로를 저장하기 위한 처리
                val path = requireActivity().absolutelyPath(selectedImage!!)
                val file = File(path)
                loadTagImage(file)
                onButtonEnabled()
            }
        }

        binding.ivRegisterTagImage.clipToOutline = true
        onButtonEvent()
    }

    // 이미지뷰에 가져온 이미지 세팅
    private fun loadTagImage(file: File) {
        binding.ivRegisterTagImage.load(file)
    }

    // 새로운 태그를 등록하기 위해 리스너의 메소드 호출
    private fun registerTag(tagName: String, file: File) {
        loadTagImage(file)
        listener?.onRegisterTag(tagName, file)
    }

    private fun onButtonEvent() {
        // 추가하기 버튼을 클릭했을 때
        binding.btnRegisterTag.setOnClickListener {
            val tagName = binding.inputTagName.text.toString()
            if (selectedImage != null) {
                val path = requireActivity().absolutelyPath(selectedImage!!)
                val file = File(path)
                registerTag(tagName, file)
            }
            dismiss()
        }

        // 뒤로가기 버튼을 클릭했을 때
        binding.ivGoingBackwards.setOnClickListener {
            dismiss()
        }

        // 이미지 추가 버튼을 클릭했을 때
        binding.ivRegisterTagImage.setOnClickListener {
            openGallery()
        }
    }

    // 텍스트 길이가 0이고 선택된 이미지가 없을 경우 추가하기 버튼을 비활성화 처리한다.
    private fun onButtonEnabled() {
        binding.btnRegisterTag.isEnabled = !(binding.inputTagName.text.isBlank() ||
                selectedImage == null)
    }

    // 실시간 입력 되는 태그명의 텍스트 길이를 텍스트뷰에 업데이트 해준다.
    private fun updateTextLength(charSequence: CharSequence?) {
        val textLength = charSequence?.length ?: 0
        binding.tvTextLength.text = textLength.toString()
        binding.tvMaxLength.text = "/$MAX_LENGTH_NAME"
    }

    /*
     * 태그명 텍스트 길이에 따라 텍스트의 색상과 배경 색상을 변경한다.
     * 입력된 텍스트 길이가 MAX_LENGTH_NAME과 같을 경우 길이를 나타내는 텍스트와 EditText의 테두리 색상을 주황색으로 변경한다.
     */
    private fun updateTextColor(charSequence: CharSequence?) {
        val textLength = charSequence?.length ?: 0
        val isMaxLength = (textLength == MAX_LENGTH_NAME)

        binding.inputTagName.setBackgroundResource(
            if (isMaxLength) R.drawable.background_radius_orange
            else R.drawable.background_radius_black
        )

        binding.tvTextLength.setTextColor(
            if (isMaxLength) Color.rgb(255, 165, 0)
            else Color.BLACK
        )
    }

    override fun onResume() {
        super.onResume()
        // DialogFragment 대화 상자의 크기 조절
        dialog?.window?.apply {
            setLayout(
                WindowManager.LayoutParams.MATCH_PARENT,
                WindowManager.LayoutParams.WRAP_CONTENT
            )
            isCancelable = true // 취소가 가능하도록
        }
    }

    // 이미지 선택을 위해 갤러리를 불러오는 인텐트를 생성한다.
    private fun openGallery() {
        val intent =
            Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        galleryResultLauncher.launch(intent)
    }
}

DialogFragment 생성 및 띄우기

class FavoriteFragment : Fragment(), FavoriteTagDialogFragment.OnRegisterTagListener {

...

    private fun onButtonEvent() {
    	// 태그 추가 버튼을 클릭했을 때 
        binding.ivAddTag.setOnClickListener {
        	// DialogFragment 생성 및 띄우기 
            val dialog = FavoriteTagDialogFragment()
            dialog.setOnTagAddListener(this@FavoriteFragment)
            dialog.show(requireActivity().supportFragmentManager, DIALOG_TAG)
        }
    }

	// 전달 받은 데이터를 새로운 리스트에 추가하고 RecyclerView Adapter를 갱신한다.
    override fun onRegisterTag(name: String, path: File) {
        TagMember.registerTag(Tag(name, path))
        tagAdapter?.updateItem(totalTags)
    }

}
profile
개발 공부 기록 🌱

0개의 댓글