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