Grusie 안드로이드 개발 기술 블로그

[Android] 크롭 이미지 DiffUtil 연동 (선택 / 수정 / 삭제) 본문

안드로이드 개발/뷰

[Android] 크롭 이미지 DiffUtil 연동 (선택 / 수정 / 삭제)

grusie 2024. 6. 12. 11:34
728x90
반응형
SMALL

지난 번 만들었던 이미지 크롭 리스트들을, 리사이클러뷰에 DiffUtil을 활용하여 넣도록 구현하였다.

베스트 컷, 퍼니 컷이라는 속성을 가진 이 아이템들을 각각 수정하거나 삭제할 수 있는 기능을 구현했어야 했다.

우선 기존에 크롭 이미지는 뷰페이저로 만들어 각 프래그먼트에서 이미지를 크롭하고 캐시 디렉토리에 저장한 뒤, ActivityViewModels로 뷰모델을 공유해서 Activity에서 Intent시켜줄 때, Uri리스트를 전달하도록 구현하였다.

 

디자인

우선 만들어야하는 뷰를 확인해보자, 넘어온 크롭 이미지 리스트를 화면에 뿌려주고, 베스트컷/퍼니컷을 선택 할 수 있도록 구현해야 하고, 햄버거 버튼을 클릭 하면 편집/삭제가 가능하도록 해야한다.

이미지 수정 다이얼로그

편집하기/삭제하기는 popup으로 만들어서 화면에 띄워주는 형태를 가져가면 될 것 같다.

 

ViewHolder

리사이클러뷰 아이템 뷰홀더에서는 디자인과 같이, 베스트컷/퍼니컷을 지정해주어야 하며, popup을 띄워 메뉴를 선택 할 수 있도록 구현해야한다.

fun bind(photoSimpleItem: PhotoSimpleItem) {
    binding.run {
        this.photoSimpleItem = photoSimpleItem

        tvBest.setOnSingleClickListener {
            photoClickListener(
                photoSimpleItem,
                PhotoSimpleAdapter.BEST_CUT_STATE
            )
        }
        tvFunny.setOnSingleClickListener {
            photoClickListener(
                photoSimpleItem,
                PhotoSimpleAdapter.FUNNY_CUT_STATE
            )
        }
        ivImageMore.setOnSingleClickListener {
            val popUpBinding =
                ViewMoreMenuPopupBinding.inflate(LayoutInflater.from(itemView.context))
            val popup = PopupWindow(
                popUpBinding.root,
                POPUP_WIDTH.dpToPx(itemView.context),
                ViewGroup.LayoutParams.WRAP_CONTENT,
                true
            )
            val location = calculateViewLocation(binding.root)
            popup.showAtLocation(
                binding.root,
                Gravity.NO_GRAVITY,
                location.x + POPUP_MARGIN_START.dpToPx(itemView.context),
                location.y + POPUP_MARGIN_TOP.dpToPx(itemView.context)
            )

            popUpBinding.tvMenu1.setOnSingleClickListener {
                photoClickListener(photoSimpleItem, PhotoSimpleAdapter.MODIFY_STATE)
                popup.dismiss()
            }
            popUpBinding.tvMenu2.setOnSingleClickListener {
                photoClickListener(photoSimpleItem, PhotoSimpleAdapter.DELETE_STATE)
                popup.dismiss()
            }
        }
    }
}

popUpWindow()함수를 사용해, more버튼을 클릭 했을 때, 화면에 띄워지도록 구현하였고

각 동작들에 클릭이벤트를 달아주었다.

DiffUtil

object PhotoSimpleDiffUtilCallback : DiffUtil.ItemCallback<PhotoSimpleItem>() {
    override fun areItemsTheSame(oldItem: PhotoSimpleItem, newItem: PhotoSimpleItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: PhotoSimpleItem, newItem: PhotoSimpleItem): Boolean {
        return oldItem == newItem
    }

    override fun getChangePayload(oldItem: PhotoSimpleItem, newItem: PhotoSimpleItem): Any? {
        if (oldItem.id == newItem.id) {
            return if (oldItem.isFunnyCut != newItem.isFunnyCut) {
                val diff = Bundle()
                newItem.isFunnyCut.let {
                    diff.putBoolean(PhotoSimpleAdapter.FUNNY_CUT_STATE, it)
                }
                diff
            } else if (oldItem.isBestCut != newItem.isBestCut) {
                val diff = Bundle()
                newItem.isBestCut.let {
                    diff.putBoolean(PhotoSimpleAdapter.BEST_CUT_STATE, it)
                }
                diff
            } else {
                super.getChangePayload(oldItem, newItem)
            }
        }
        return super.getChangePayload(oldItem, newItem)
    }
}

 

아이템의 차이가 있을 경우 변경을 해주어야 하기에, id값 비교 / 객체 비교를 진행하고, 베스트컷/퍼니컷 같은 경우 payloads로 처리하기 위해, 해당 로직을 구현하였다.

 

Activity

클릭 리스너

private val photoAdapter: PhotoSimpleAdapter by lazy {
    PhotoSimpleAdapter(
        photoClickListener = { selectedItem, cutState ->
            Log.d(
                "confirm photoClick",
                "$selectedItem, ${cutState}}"
            )

            val position = photoAdapter.currentList.indexOf(selectedItem)

            when (cutState) {
                PhotoSimpleAdapter.BEST_CUT_STATE -> {
                    viewModel.setBestCutPosition(position)
                }

                PhotoSimpleAdapter.FUNNY_CUT_STATE -> {
                    viewModel.setFunnyCutPosition(position)
                }

                PhotoSimpleAdapter.DELETE_STATE -> {
                    viewModel.deleteImage(position)
                }

                PhotoSimpleAdapter.MODIFY_STATE -> {
                    intentToImageCropView(selectedItem.originUri.toString(), position)
                }
            }
        },
        addSimplePhotoClickListener = { intentToImagePicker() }
    )
}

클릭 이벤트를 파악해서 그에 맞는 동작을 하기 위해, Companion object에 상태를 저장하고, 해당하는 동작을 수행해주도록 구현했다.

 

ViewModel

편집하기

우선 MODIFY_STATE 같은 경우, 편집하기를 클릭했을 때인데 기존에 있던 이미지 크롭 뷰로 이동한다.

그 때, 아이템의 원본(크롭 전 - 갤러리 이미지) uri를 넘겨서 화면에 띄워주도록 구현하였고, 수정이 완료 되었을 경우 해당 position에 수정을 해주어야 하기에 position도 같이 넘겼다.

 

기존에는 position을 adapter에서 onBindViewHolder()에서 viewHolder로 넘겨서 사용하도록 하였는데, 아이템 삭제가 진행되었을 시, DiffUtil은 전체가 다 변경되는 것이 아니기에, position을 제대로 받아오지 못하는 문제가 생겨, 클릭 이벤트가 발생했을 때, indexOf()로 해당 아이템의 포지션을 받아오도록 수정하였다.

fun setModifyPhoto(position: Int, uri: String) {
    viewModelScope.launch {
        val tempPhotoList = _photoList.value.toMutableList()
        tempPhotoList[position] = tempPhotoList[position].copy(uri = uri)
        _photoList.emit(tempPhotoList)
    }
}

수정은 크롭뷰에서 가져온 이미지의 url을 data class의 copy() 메서드를 사용해 변경해주면 된다.

삭제하기

DELETE_STATE의 경우, 해당 아이템을 삭제하는 것으로, 뷰모델에 있는 photoList에 변경점을 주어, Activity에서 collect하고 있는 부분에서 submitList()를 사용해 업데이트를 진행하고 있다.

    fun deleteImage(position: Int) {
        viewModelScope.launch {
            val tempPhotoList = _photoList.value.toMutableList()
            tempPhotoList.removeAt(position)
            _photoList.emit(tempPhotoList)

            if (_funnyCutPosition.value >= position) {
                _funnyCutPosition.emit(_funnyCutPosition.value - 1)
            }
            if (_bestCutPosition.value >= position) {
                _bestCutPosition.emit(_bestCutPosition.value - 1)
            }

            if(_funnyCutPosition.value == position){
                setFunnyCutPosition(position)   
            }
            if(_bestCutPosition.value == position) {
                setBestCutPosition(position)
            }
        }
    }

뷰모델에서 deleteImage는 기본적으로, 기존 photoList에서 해당 포지션의 아이템을 삭제하고 변경해준다.

추 후, 퍼니컷과 베스트컷의 position을 넘겨주어야 하기 때문에, 따로 관리를 하고 있어, 삭제 시 로직 처리를 해주어야 하며, 만약 삭제된 아이템이 funnyCut이나 bestCut일 경우, 바로 뒤에 있는 아이템이 대신 해야 하기에 setFunnyCutPosition()과 setBestCutPosition()을 따로 구현하였다.

 

결과

 

1. 베스트컷 / 퍼니컷 선택

2. 이미지 삭제

3. 이미지 삭제

 

 

 

후기

커스텀 뷰를 활용해서 이렇게까지 디테일하게 작업했던 것은 이번이 처음이었던 것 같다.

이런 저런 시행착오를 겪긴 했으나, 그래도 나름 금방 만들었던 것 같다. 이제 서버연동 후 리스트 띄우는 것을 위한 커스텀 뷰를 만들어야 할 것이고, 지도 클러스터링 및 캘린더 부분도 남아있다. 파일 캐시디렉토리에 저장 같은 경우도 기회가 되면 다뤄볼까 한다.

반응형
LIST