기록

AndroidStudio/수직-수평 슬라이딩 이벤트 넣기 : 중첩 뷰페이저 활용 본문

Moblie/Android

AndroidStudio/수직-수평 슬라이딩 이벤트 넣기 : 중첩 뷰페이저 활용

youngyin 2022. 12. 25. 09:00

문제

사이드 프로젝트(Android application to organize photos by swiping)중 상하좌우 슬라이딩 이벤트별 액션을 지정해야 했다. 어떤 이벤트에 어떤 액션을 지정할지는 정해지지 않았고, 샘플 앱을 만들어 테스트 한 후에 정하기로 했다. 

테스트1
  • 좌우 스와이프 : 사진 커서 이동
  • 상하 스와이프 : 사진 삭제 또는 사진 저장
테스트2
  • 좌우 스와이프 : 사진 삭제 또는 사진 저장
  • 상하 스와이프 : 사진 커서 이동

해결방법

뷰페이저를 2중으로 감싸서 상하좌우 스와이핑 이벤트에 액션을 지정했다.

테스트1 / 테스트2

테스트1 -> 테스트2로 전환할 때 뷰페이저의 방향만 반대로 바뀌어주면 되어서, 코드의 변경이 적었다.

코드

https://github.com/youngyin/orgnzPhts

1. Parent

- fragment

class SliderFragment : Fragment() {
    private var _binding: FragmentSliderBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        model = ViewModelProvider(this)[SliderViewModel::class.java]
        _binding = FragmentSliderBinding.inflate(inflater, container, false)
        val root: View = binding.root
        
        binding.vpSlide.adapter = PagerAdapter(requireActivity(), it,
            object : SliderListener {
                override fun showPrev() : Boolean {}
                override fun showNext() : Boolean {}
                override fun favorite(photo: Photo) {}
                override fun delete(photo: Photo) {}})

        return root
    }
}

- adapter

class PagerAdapter(
    fa : FragmentActivity,
    var database: ArrayList<Photo>,
    private val listener : SliderListener
) : FragmentStateAdapter(fa) {
    private lateinit var fragment : Fragment

    override fun getItemCount(): Int = database.size
    override fun createFragment(position: Int): Fragment {
        fragment = PhotoFragment(database[position], listener)
        return fragment
    }
}

 

2. child

- fragment

class PhotoFragment(
    var photo: Photo,
    val listener : SliderListener
    ) : Fragment() {

    private var _binding: FragmentPhotoBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentPhotoBinding.inflate(inflater, container, false)
        val adapter = InnerPagerAdapter()
        adapter.setPhoto(photo)

        binding.vpSlide.adapter = adapter
        binding.vpSlide.setCurrentItem(1, false)

        binding.vpSlide.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                when(position){
                    0 -> listener.delete(photo)
                    2 -> listener.favorite(photo)
                }
            }
        })

        return binding.root
    }
}

- adapter

class InnerPagerAdapter : RecyclerView.Adapter<InnerPagerAdapter.ViewHolder>() {
    private val database = ArrayList<Photo>()

    fun setPhoto(photo: Photo){
        database.clear()
        database.add(Photo("", "", 0L, "")) 
        database.add(photo)
        database.add(Photo("", "", 0L, ""))
        notifyItemChanged(1)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType1: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_viewpager_photo, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(database[position])
    }

    override fun getItemCount(): Int = database.size

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        @SuppressLint("ClickableViewAccessibility", "CheckResult")
        fun bind(photo: Photo) {
            itemView.findViewById<TextView>(R.id.tv_photo).text = photo.toString()
        }
    }
}

개선할점/주의할점

1. 멀티뷰타입

adapter를 두개 사용하지만, 각 adapter가 하는 역할이 크게 다르지 않다. 이전에 시도했었던 멀티뷰타입(2022.05.29 - [Moblie/Android] - AndroidStudio/java/ 멀티뷰 타입 Recyclerview)을 활용하면, adapter을 하나만 둘 수 있을 것이다.

2. 뷰페이저 setOffscreenPageLimit 
이전 혹은 다음페이지를 몇개까지 미리 로딩할지 정하는 함수로, default값은 1이다. 즉, 바로 전과 후의 페이지를 미리 로딩한다.
이 때문에 parent adapter에서 아이템 삭제를 해도, (이전에 로딩된 화면을 보여주기 때문에) 삭제된 아이템이 보일 수 있다.
문제 해결을 위해서는
(1) parent adapter에서 아이템 변경이 있을 때 child adapter를 갱신하도록 하던가,
(2) setOffscreenPageLimit를 0으로 설정하여, 페이지 로딩을 미리 하지 않는 방법이 있다.
3. 사진 가져오기

해당 프로젝트는 사진첩에서 사진을 가져와 삭제할 사진과 저장할 사진을 쉽게 구분할 수 있도록 하는 것이 주된 아이디어이다. 이로 인해 mediaStore을 이용해 갤러리에 접근하는 것이 필요했으며, 해당 내용은 추후 정리하여 공유할 예정이다.

4. 프로젝트 구조

스프링을 접하고 난 이후에 한 첫 프로젝트라, 각 요소를 spring처럼 써보려고 했다. 사진첩에서 사진 데이터를 직접 요청하는 로직, 퍼미션을 체크하는 로직 등을 서비스로 빼고, viewmodel은 연관 있는 서비스를 호출하는데에만 사용하려고 했다. 이후 내부 DB를 사용하게 되면, dao와 service도 나누어 볼 생각이다.

Comments