본문 바로가기

Android

ViewPager2와 TabLayout

이번에는 앞서 만든 사진을 나만의 앨범 만들기 버튼을 클릭해서 뷰페이징 형태로 스와이핑하여 볼 수 있도록 하고 

TabLayout과 TabLayoutMediator를 통해서 아래 몇 번째 사진인지 표시하는 방법을 설명하겠습니다. 

 

 

 

[결과사진]



 


 

내용을 보듯이 사진을 스와이핑하여 뷰페이징이 가능하고 아래 탭 레이아웃으로 사진 표시가 되어 몇 번째 장인지 확인이 가능합니다.  코드를 보기전에 우선 ViewPager2에 대해서 좀 더 알아보겠습니다.

 

 


 

ViewPager2

 

ViewPager와 같은 역할을 하지만 더 개선된 버전이며 여러 기능이 추가되었습니다.

 

1) android:orientation [horizontal, vertical] 을 통해 페이징 방향을 수평 또는 수직으로 설정할 수 있음

 

2) android:layoutDirection [rtl] 을 통해 페이징을 오른쪽에서 왼쪽으로 수동 설정이 가능함

 

3) RecyclerView 기반으로 빌드되기 때문에 주로 Fragment와 엮였던 ViewPager의 기능에 더해서 

RecyclerView 기반이 추가되었기에 RecyclerView.Adapter로 ViewPager2의 Adapter를 설정할 수 있음

 

▶ 차이는 같은 레이아웃에 일부 View에 대해서만 교체하는 경우는 RecyclerView.Adapter 사용

 

 

[activity_frame.xml]

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".frames.FrameActivity">

    <androidx.appcompat.widget.Toolbar
        android:layout_width="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_height="?attr/actionBarSize"
        android:background="#F6E972"
        android:id="@+id/toolbar"/>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="@+id/tabLayout"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingHorizontal="12dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:tabBackground="@drawable/selector_tab"
        app:tabIndicator="@null" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

[selector_tab.xml]

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ic_dot_selected" android:state_selected="true"/>
    <item android:drawable="@drawable/ic_dot_unselected" android:state_selected="false"/>
</selector>

 


selector_tab.xml에서는 선택했을때 선택된 dot의 아이템과 선택되지 않았을 때의 dot의 아이템을 두어 색을 다르게 구현하였습니다.

이를 TabLayout의 tabBackground에 넣어줍니다.   단, tabIndicator는 @null로 둡니다.  기존 TabLayout이 제공하는 인디케이터를 사용하지 않고 selector_tab에 정의한 것을 사용하려 하기 때문입니다.

그리고 ViewPager2는 기존 뷰페이저처럼 똑같이 두되, 현재는 선택한 사진들에 대해서 뷰만 가지고 보는 작업을 하는 것이므로 

RecyclerView에서의 어댑터를 활용할 것입니다. 

 

 

[FrameAdapter.kt]

package com.example.mygallery.adapters

import android.content.Context
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.mygallery.databinding.ItemFrameBinding
import com.example.mygallery.frames.FrameActivity

class FrameAdapter(private val list: List<FrameItem.ImageSubItems>) : RecyclerView.Adapter<FrameViewHolder>() {

    companion object {
        const val FRAME_IMAGE = 102
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FrameViewHolder {
        val inflater = parent.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val binding = ItemFrameBinding.inflate(inflater, parent,false)
        return FrameViewHolder(binding)

    }

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

    override fun getItemCount(): Int {
        return list.size
    }
}

class FrameViewHolder(private val binding: ItemFrameBinding) : RecyclerView.ViewHolder(binding.root){
    fun bind(item: FrameItem.ImageSubItems){
        binding.frameImageView.setImageURI(item.uri)
    }
}

sealed class FrameItem {
    data class ImageSubItems (
        val uri: Uri
    )
}

 

ViewPager2에 담아 놓을 RecyclerView.Adapter입니다.

우선 ViewPager2에는 이미지들이 담길 것입니다.   따라서 이미지뷰는 아이템으로 받을 예정이므로 관련 xml을 만들어줍니다.

 

[item_frame.xml]

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/frameImageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

그리고 onCreateView에서는 다음 item_frame.xml을 인플레이팅 작업을 합니다.

어댑터는 자식뷰이기 때문에 엄마 뷰에서 작업할 LayoutInflater를 생성한 다음 이를 ItemFrameBinding에 inflate합니다.

원래는 binding.root를 반환해야 하지만 RecyclerView.ViewHolder를 상속한 FrameViewHolder 클래스에서 

바인딩 작업을 구현할 것이므로 binding을 인자로 넘겨주고 ViewHolder에는 binding.root를 넘겨줍니다.

여기서 Item 요소를 바인딩을 통해 접근하여 바인딩 해주는데, 원래는 onBindViewHolder에서 컬렉션과 뷰의 바인딩 작업이

이뤄지는 것이기 때문에 여기선 position을 기준으로 바인딩 작업을 합니다.

 

다시 설명하자면, RecyclerView.ViewHolder(binding.root)를 상속한 클래스 FrameViewHolder에서는 자식의 바인딩된

뷰를 가지고 있기 때문에 해당 위젯에 대해 실질적인 값을 셋팅해 넣습니다.

하지만, 어댑터가 일반적으로 리스트에 담긴 요소들을 보여줄 때 사용하는 것이기 때문에

리스트 안에 각 요소들은 FrameViewHolder와 바인딩 작업을 해줘야 하므로 이 부분을 position 값을 가지고 있는

onBindViewHolder에서 처리하는 것입니다.

마지막 FrameActivity.kt 파일을 확인하면서 ViewPager2를 어떻게 처리하는지, TabLayout은 어떻게 관리하는지 설명하겠습니다.

 


 

[FrameActivity.kt]

package com.example.mygallery.frames

import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.mygallery.R
import com.example.mygallery.adapters.FrameAdapter
import com.example.mygallery.adapters.FrameItem
import com.example.mygallery.databinding.ActivityFrameBinding
import com.google.android.material.tabs.TabLayoutMediator

class FrameActivity : AppCompatActivity() {

    private lateinit var binding: ActivityFrameBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        binding = ActivityFrameBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        val images = (intent.getStringArrayExtra("images")
            ?: emptyArray()).map { uriString -> FrameItem.ImageSubItems(Uri.parse(uriString)) } // 리스트로 가져옴
        val frameAdapter = FrameAdapter(images)

        binding.toolbar.apply {
            title = "나만의 앨범"
            setSupportActionBar(this)
        }

        supportActionBar?.setDisplayHomeAsUpEnabled(true) // 뒤로가기 버튼 활성화

        binding.viewPager.apply {
            adapter = frameAdapter
        }

        TabLayoutMediator(
            binding.tabLayout,
            binding.viewPager,
        ) { tab, position ->
            binding.viewPager.currentItem = tab.position
        }.attach()
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            androidx.appcompat.R.id.home -> {
                finish()
                true
            }

            else -> {
                super.onOptionsItemSelected(item)
            }
        }
    }
}

 

 

우선은 FrameActivity는 MainActivity에서 Intent로 이동했을때 나오고 거기에 putExtra로 이미지의 uri들을 리스트 형태로 보내고 있다는 것을 감안해야 합니다.

 

[MainActivity.kt] 에서 Intent 설정

   private fun navigateToFrameActivity() {
        val images = imageAdapter?.currentList?.filterIsInstance<ImageItems.Image>()
            ?.map { it.uri.toString() }?.toTypedArray()
        val intent = Intent(this, FrameActivity::class.java)
            .putExtra("images", images)

        startActivity(intent)
    }

 

MainActivity에서도 이미지를 가지고 왔을때 어댑터를 통해서 각 이미지 Uri를 리스트로 관리해 RecyclerView에 넣고 있습니다.

따라서 해당 어댑터에서 현재 리스트 내에 data class가 되고 있는 Images.Image를 인스턴스로 필터해 가져옵니다.

이럴 경우, 갤러리에서 선택한 이미지 uri 리스트가 필터될 것이며 uri를 토대로 매핑하여 배열로 만듭니다.

이것을 이제 FrameActivity로 보내는 것입니다.

 

[FrameActivity.kt] 에서 Intent 설정

        val images = (intent.getStringArrayExtra("images")
            ?: emptyArray()).map { uriString -> FrameItem.ImageSubItems(Uri.parse(uriString)) } // 리스트로 가져옴
        val frameAdapter = FrameAdapter(images)

 

배열로 보낸걸 배열로 다시 받은 다음에 빈 값인 경우 빈 배열로 셋팅하고 그 상태에서 배열은 urlString 값으로 구현되어 있을 것이므로 it 부분을 urlString으로 하여 매핑 처리 합니다.

FrameItem으로 sealed class 셋팅한 곳에서 ImageSubItems data class를 접근합니다.

이 때, 보낸 uriString과 매핑하여 찾아내고 이를 Uri 파싱 작업을 합니다.

뭔가 어려워 보이지만, 쉽게 보자면 메인에서 보낸 이미지 Uri를 String 배열 형태로 받아서 리스트로 재구성하는 과정입니다.

저렇게 하면 images는 map 함수에 의해 리스트가 될 것이며 이를 FrameAdapter의 인자인 리스트로 넣어줍니다. 

 

binding.viewPager.apply {
    adapter = frameAdapter
}

 

해당 어댑터는 viewPager에 어댑터로 껴넣습니다.   이제 ViewPager2에 RecyclerView.Adapter가 꼽혀진 상태로

이미지 Uri가 파싱된 리스트가 어댑터 형태로 꼽히며, 어댑터의 아이템 (ImageView)에 각각 바인딩 되는 절차를 거칠 것입니다.

이후에는 TabLayoutMediator를 통해 현재 tabLayout과 viewPager를 인자로 넣어주고 콜백으로 tab과 position을 받는데

이 부분을 viewPager의 현재 아이템이 tab.postion과 같다라고 셋팅하면 됩니다.

그러면 현재 viewPager의 아이템인 이미지뷰(1번, 2번, ..., n번)가 각 탭의 포지션(1번, 2번, ... n번)으로 잡힐 것이고

선택된 포지션은 앞서 selector_tab.xml에 의해 구분되어 보여질 것입니다.    (처음 대표 사진과 동일)

 

       TabLayoutMediator(
            binding.tabLayout,
            binding.viewPager,
        ) { tab, position ->
            binding.viewPager.currentItem = tab.position
        }.attach()

 


 

이상 ViewPager2와 TabLayout & TabLayoutMediator에 대해 알아보았습니다.

중요한 것은 ViewPager2는 이전과 다르게 Fragment로만 사용되는 것이 아니라 단순 뷰의 변경사항일 경우 

RecyclerView를 활용하여 어댑터 관리가 가능하다는 점이며, TabLayoutMediator를 통해서 TabLayout과 ViewPager2를

손쉽게 엮어 화면에 표시할 수 있음을 확인하였습니다.

단! TabLayout 셋팅할 때 tabBackground에 미리 정의한 selector_tab.xml 셋팅하고 tabIndicator를 null로 두어

정의한 selector를 활용하겠다고 명시해야 함을 잊지 마셔야 합니다.   

이상으로 나만의 사진첩 2편에 대해서 정리하며 관련 기술 스택에 대한 개념을 체크했습니다. 

다음 포스팅에서 뵙겠습니다.     정말 감사합니다.

반응형

'Android' 카테고리의 다른 글

Network - OkHttpClient / Socket 통신  (0) 2024.08.25
WebView & ViewPager2 / Fragment  (0) 2024.08.16
Permission 처리와 ListAdapter 활용법  (0) 2024.08.01
RoomDB 활용 방법과 Coroutine 사용  (0) 2024.07.29
단어장앱-1 (RecyclerView)  (2) 2024.07.26