[Andorid] UMC Study - 3주차

1.ViewPager2, 뷰페이저2

FLO 앱 뿐만 아니라 우리가 사용하는 앱에서 옆으로 swipe 하면 이미지가 바뀌는 배너 화면을 본 적이 있을 것이다.
이는 안드로이드 ViewPager로 구현이 가능하다.

ViewPager : 스와이프할 수 있는 형식으로 뷰 또는 프래그먼트를 표시

ViewPager 말고 새로 나온 ViewPager2를 쓸 것이다.
https://developer.android.com/jetpack/androidx/releases/viewpager2?hl=ko

developers 사이트에 나온 ViewPager2의 이점은 다음과 같은데,

  1. android:orientation 속성으로 가로 페이징은 물론 세로 페이징도 지원
    (또는 setOrientation() 메소드 사용)
<androidx.viewpager2.widget.ViewPager2
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:orientation="vertical" />
    
  1. 오른쪽에서 왼쪽(RTL) 페이징 지원, android:layoutDirection 속성 사용
    (또는 setLayoutDirection() 메소드 사용)
<androidx.viewpager2.widget.ViewPager2
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/pager"
        android:layoutDirection="rtl" />
    
  1. 수정 가능한 프래그먼트 컬렉션을 통해 페이징 지원, 기본 컬렉션이 변경되면 notifyDatasetChanged()를 호출하여 UI 업데이트 가능

  2. DiffUtil : ViewPager2는 RecyclerView를 기반으로 빌드되므로 DiffUtil 유틸리티 클래스에 액세스 가능. 그래서 ViewPager2 객체는 기본적으로 RecyclerView 클래스의 dataset 변경 애니메이션을 활용할 수 있음

위 배너 화면에서, 뷰페이저 안에는 프래그먼트가 들어갔다.

ViewPager2를 쓰려면 implementation을 해줘야 함

implementation "androidx.viewpager2:viewpager2:1.0.0"

뷰 페이저에 안에는 BannerFragment를 보여줄 것이다.

BannerFragment 안에는 이미지 뷰 하나만 들어가 있음.

뷰 페이저 안에 들어갈 뷰(프래그먼트)를 관리할 adapter 생성

    private inner class BannerViewpagerAdapter(fragment : Fragment) : FragmentStateAdapter(fragment) {

        private val fragmentList : ArrayList<Fragment> = ArrayList()

        override fun getItemCount(): Int = fragmentList.size

        override fun createFragment(position: Int): Fragment = fragmentList[position]

        fun addFragment(fragment: Fragment) {
            fragmentList.add(fragment)
            notifyItemInserted(fragmentList.size - 1) // adpater에선 새 프래그먼트가 추가되면 viewpager에 알려줘야 함
        }
    }

뷰페이저의 어댑터에 우리가 만든 BannerViewpagerAdpater를 붙여줘야 한다!
또한 BannerFragment의 인자로 이미지 id 값을 줘서 이미지가 바뀔 수 있게 해야한다.

        val bannerAdapter = BannerViewpagerAdapter(this)
        bannerAdapter.addFragment(BannerFragment(R.drawable.img_home_viewpager_exp))
        bannerAdapter.addFragment(BannerFragment(R.drawable.img_home_viewpager_exp2))

        binding.homeBannerVp.adapter = bannerAdapter
        binding.homeBannerVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

BannerFragment에서 이미지를 바꾸는 방식은 다음과 같다.

class BannerFragment(val imgRes : Int) : Fragment() {
    lateinit var binding: FragmentBannerBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        binding = FragmentBannerBinding.inflate(inflater, container, false)

        binding.bannerImgIv.setImageResource(imgRes)

        return binding.root
    }
}

2. TabLayout, 탭 레이아웃

https://developer.android.com/reference/com/google/android/material/tabs/TabLayout

위 사진같이 수록곡 - 상세정보 - 영상 탭이 있는데 이것을 Tab Layout이라고 하고, 탭이 움직임에 따라 밑에 화면이 바뀌는 것은 Tab Layout과 ViewPager를 연결해 주었기 때문이다!

(탭 레이아웃과 뷰페이저 사용에 관한 문서)
https://developer.android.com/guide/navigation/navigation-swipe-view-2?hl=ko

TabLayoutMediator를 만들어서 TabLAyout을 ViewPager2에 연결하고 첨부해야 한다.

val tabLayoutTextArray = arrayListOf("수록곡", "상세정보", "영상")

TabLayoutMediator(TargetTablayout, TargetViewpager){tab, position->
            tab.text = tabLayoutTextArray[position]
        }.attach()

뷰 페이저에 각 페이지를 나타내는 하위 뷰를 삽입하려면 FragmentStateAdapter에 연결해야 한다.

private inner class AlbumViewpagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

	// 보여 줄 전체 프래그먼트의 수
        override fun getItemCount(): Int = 3

	// 프래그먼트 붙이기
        override fun createFragment(position: Int): Fragment {
            return when(position) {
                0 -> AlbumTrackFragment() // 수록곡
                1 -> AlbumDetailFragment() // 상세정보
                2 -> AlbumVideoFragment() // 영상
                else -> AlbumTrackFragment()
            }
        }
    }

뷰 페이저의 어댑터에 우리가 만든 어댑터를 붙여줘야한다.

AlbumViewpagerAdapter(this) 에서, fragment가 어댑터의 인자이기 때문에 현재 프래그먼트(또는 액티비티)에 뷰페이저가 보일 것이라는 뜻

binding.albumVp.orientation으로 스와이프 방향을 지정해 줄 수 있다!

        binding.albumVp.adapter = AlbumViewpagerAdapter(this)
        binding.albumVp.orientation = ViewPager2.ORIENTATION_HORIZONTAL

3. Bottom Navigation

요건 구글에서 준 가이드라인
https://material.io/components/bottom-navigation

밑 사진 처럼 하단에 탭 비슷한게 있고 누르면 각자 다른 화면을 보여주게끔 하는 것을 bottom navigation이라고 한다.

XML파일에서 bottom navigation을 추가해준다

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/main_bnv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:itemIconSize="25dp"
        app:itemIconTint="@drawable/main_btm_color_selector"
        app:itemTextColor="@drawable/main_btm_color_selector"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_nav_menu" />

activity 파일에서는 아이템을 누르면 아이템 id 값을 가져와 서로 다른 프래그먼트를 띄워주어야 한다.

        binding.mainBnv.setOnItemSelectedListener {
            when (it.itemId) {
                R.id.homeFragment -> { // 1. 홈 화면
                    supportFragmentManager.beginTransaction()
                        .replace(R.id.main_frm, HomeFragment())
                        .commitAllowingStateLoss()
                    return@setOnItemSelectedListener true
                }
                
                // 2. 둘러보기 화면도 똑같이
                // 3. 검색 화면도 똑같이
                // 4. 보관함 화면도 똑같이
            }
            false
        }
    }

    private fun initNavigation() { // navigation 초기화
        supportFragmentManager.beginTransaction().replace(R.id.main_frm, HomeFragment())
            .commitAllowingStateLoss()

    }

4. Adapter, 어댑터

Adapter는 하나의 Object(객체)이고, 보여지는 view와 그 view에 올릴 Data를 연결하는 일종의 Bridge 역할을 한다.

대표적인 어댑터인 데이터 리스트를 아이템 단위의 뷰 또는 뷰 집합으로 표시할 때 사용하는 리스트 뷰 어댑터가 존재한다. 이 때, 어댑터는 데이터의 원본을 받아 관리하고 어댑터 뷰는 출력할 수 있는 형태로 데이터를 제공하는 중간 객체의 역할을 함!

Adapter View는 View에 직접 정보를 넣지 않고 Adapter라는 중간 매개체를 이용하는 뷰이다.
대표적인 어댑터 뷰의 서브 클래스로는 ListView, GridView, Spinner, Gallery 등이 있음!

그리고 어댑터와 연결된 원본 데이터가 변경되면 notifyDataSetChanged 메소드를 호출하여 어댑터 뷰에 원본이 변경되었다고 알려주오 어댑터 뷰가 다시 그림을 그리도록 해야 함.

ViewPager에도 프래그먼트를 보여 줄 어댑터가 필요한데, 이때 어댑터에선 페이지 단위로 화면에 표시하게 된다.

위에서 두개의 예제(Banner, tab-viewpager)를 통해 adapter를 inner class로 구현해 보았다! 물론 꼭 inner로 안해도 된다. adapter 클래스만 담겨있는 폴더를 만들어서 따로 관리해도 좋다.

이번엔 리스트 뷰 어댑터 예제 (여기서 어댑터 뷰는 리스트 뷰가 되겠죠?)

데이터가 존재한다. (이 데이터들을 화면 상에 보이고 싶다)

    var songList = arrayListOf<Song>(
        Song("01", "라일락", "아이유 (IU)", true),
        Song("02","Flu", "아이유 (IU)", false),
        Song("03","Coin", "아이유 (IU)", true),
        Song("04","봄 안녕 봄", "아이유 (IU)", false),
        Song("05","Celebrity", "아이유 (IU)", false),
        Song("06","돌림노래 (Feat. DEAN)", "아이유 (IU)", false),
        Song("07","빈 컵 (Empty Cup)", "아이유 (IU)", false),
        Song("08","아이와 나의 바다", "아이유 (IU)", false),
        Song("09","어푸 (Ah puh)", "아이유 (IU)", false),
        Song("10","에필로그", "아이유 (IU)", false)
    )

데이터들이 보여질 아이템 뷰 (row_track.xml)이 존재한다.

리스트 뷰 어댑터를 생성한다 (이번에도 inner class)
이번에는 어댑터 안에 데이터를 담아준다(items)!

private inner class MyListViewAdapter(val context: Context, val items: ArrayList<Song>) : BaseAdapter() {
		
        override fun getCount(): Int {
            return items.size
        }

        override fun getItem(position: Int): Any? {
            return items[position]
        }

        override fun getItemId(position: Int): Long {
            return 0
        }

        override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        
        // row_track_xml 파일을 inflate 하는 부분
            val view : View = LayoutInflater.from(parent?.context).inflate(R.layout.row_track, parent, false)
            val numberTv = view.findViewById<TextView>(R.id.row_track_number_tv)
            val titleTv = view.findViewById<TextView>(R.id.row_track_title_tv)
            val singerTv = view.findViewById<TextView>(R.id.row_track_singer_tv)
            val playBtn = view.findViewById<ImageButton>(R.id.row_track_play_ib)
            val infoLayout = view.findViewById<LinearLayout>(R.id.track_info_layout)
            val titleCv = view.findViewById<CardView>(R.id.row_track_title_cv)

            val item = items[position]
            numberTv.text = item.number
            titleTv.text = item.title
            singerTv.text = item.singer

            val song = Song(numberTv.text.toString(), titleTv.text.toString(),
                singerTv.text.toString(), item.isTitle)

            Log.d("Log Song Test", song.title + song.singer)
            return view
            
            // item에 대한 클릭이벤트 처리
            // title 정보 표시
            if(item.isTitle) {
                titleCv.visibility = View.VISIBLE
            }
			
            // play버튼 누르면 현재 재생중인 노래의 정보가 바뀌게끔
            playBtn.setOnClickListener {
                val intent = Intent(requireContext(), SongActivity::class.java)
                intent.putExtra("title", song.title)
                intent.putExtra("singer", song.singer)
                startActivity(intent)
            }
			
            // 노래 제목, 가수가 담긴 layout을 누르면 토스트 메세지
            infoLayout.setOnClickListener {
                Toast.makeText(requireContext(), item.title, Toast.LENGTH_SHORT).show()
            }
        }
    }

마지막은 리스트 뷰 어댑터 부착!

        val trackSongListView = binding.trackSongLv
        val adapter = MyListViewAdapter(requireContext(), songList)
        trackSongListView.adapter = adapter

아까 위에서 했던 뷰페이저 안에 수록곡 프래그먼트에는 리스트 뷰가 담겨져 있던 것!!

좋은 웹페이지 즐겨찾기