240125 TIL #304 Android #17 JetPack - 2

김춘복·2024년 1월 25일
0

TIL : Today I Learned

목록 보기
304/494

Today I Learned

오늘도 안드 공부


Jetpack

RecyclerView

리싸이클러 뷰. 목록 화면을 만들 때 사용하는 뷰

  • 목록은 리스트뷰로도 구성할 수 있지만, 기능적으로 리사이클러뷰가 더 나아 더 많이 사용한다.

구성요소

뷰홀더는 각 항목을 구성하는 뷰 객체를 가진다. 어댑터는 뷰홀더에 있는 뷰 객체에 적절한 데이터를 대입해 항목을 완성한다. 레이아웃매니저는 어댑터가 만든 항목을 배치해 리사이클러 뷰에 출력한다.

  1. ViewHolder : 항목에 필요한 뷰 객체를 가진다
  2. Adapter : 항목을 구성한다
  3. LayoutManager : 항목을 배치한다
  4. ItemDecoration : (선택) 항목을 꾸민다

사용방법

  1. 의존성 추가
implementation("androidx.recyclerview:recyclerview:1.2.1")
  1. 레이아웃 XML 파일에 등록
<androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recyclerView"/>
  1. 목록에 표시할 항목을 디자인한 레이아웃 xml도 등록

  2. 뷰홀더 준비
    RecyclerView.ViewHolder를 상속받아 작성

  3. 어댑터 준비
    RecyclerView.Adapter를 상속받아 작성. 아래의 함수 재정의
    getItemCount() : 항목 개수를 판단하려고 자동으로 호출
    onCreateViewHolder() : 항목의 뷰를 가지는 뷰홀더를 준비하려고 자동으로 호출.
    onBindViewHolder() : 뷰홀더의 뷰에 데이터를 출력하려고 자동으로 호출. getItemCount()가 반환한 숫자만큼 여기서 호출되어 항목 생성

  4. 리사이클러 뷰 출력
    레이아웃 매니저를 등록해 화면에 출력

  5. 항목을 동적으로 추가 및 제거
    adapter의 notifyDataSetChanger()함수를 호출해서 동적으로 관리


레이아웃 매니저

RecyclerView.LayoutManager를 상속받은 클래스.

  • LinearLayoutManager
    항목을 가로, 세로 방향으로 배치. 보통 가장 많이 사용한다.
  • GridLayoutManager
    항목을 그리드로 배치. 생성자의 숫자는 그리드에서 열의 개수를 의미. 가로세로로 배치 가능.

  • StaggeredGridLayoutManager
    항목을 높이가 불규칙한 그리드로 배치. 각 뷰의 크기가 다르면 지그재그로 배치된다.


아이템 데커레이션

리사이클러 뷰를 다양하게 꾸밀대 사용. 레이아웃 매니저가 항목을 배치하기 전후로 설정할 수 있다. 필수는 아니다.

class MyDecoration(val context: Context): RecyclerView.ItemDecoration(){
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
    }
}
  • onDraw() : 항목이 배치되기 전에 호출
  • onDrawOver() : 항목이 모두 배치된 후 호출
  • getItemOffsets() : 개별 항목을 꾸밀 때 각각 호출
  • 매개변수인 Canvas 객체를 통해 그림을 그릴 수 있다.
  • 리사이클러 뷰에 적용할땐 addItemDecotaion()함수를 이용한다.

ViewPager2

화면을 탭해 좌우로 미는 스와이프 이벤트로 화면을 전환할 때 사용한다.
ex. 인스타그램

  • 플랫폼API에선 제공하지 않아 androidx를 사용한다. 2019년에 viewpager와 별개로 기능이 많아진 viewpager2가 나와 이를 사용한다.
  • 의존성 추가
implementation ("androidx.viewpager2:viewpager2:1.0.0")
  • 레이아웃XML
    <androidx.viewpager2.widget.ViewPager2
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/viewpager"/>
  • 뷰페이저2는 화면을 항목으로 본다. 항목이 순서대로 나열되어있고 한 화면에 항목 하나가 나온다는 개념이다. 따라서 리사이클러뷰의 어댑터를 적용해야 한다.
    (RecyclerView.Adapter나 FragmentStateAdapter를 사용)

  • RecyclerView.Adapter는 위의 리사이클러 어댑터와 차이가 없고 뷰페이저2의 어댑터로 적용만 하면 된다. 하지만 이 방법은 복잡할 수 있다.

  • 각 항목을 보통 프래그먼트로 작성하고, FragmentStateAdapter로 뷰페이저2를 구성하는 경우가 많다.

class MyFragmentPagerAdapter(activity: FragmentActivity): FragmentStateAdapter(activity){
    val fragments: List<Fragment>
    init{
        fragments = listOf(OneFragment(), TwoFragment(), ThreeFragment())
        Log.d("event", "fragment size : ${fragments.size}")
    }
    
    // 항목의 개수를 구하는 함수
    override fun getItemCount(): Int = fragments.size
    // 항목을 구성하는 프래그먼트 객체를 얻어 여기서 반환하는 객체가 각 항목에 출력
    override fun createFragment(position: Int): Fragment = fragments[position]
}
  • 기본적으로 가로방향으로 나오지만 세로로 바꿀 수도 있다.
binding.viewpager.orientation = ViewPager2.ORIENTATION_VERTICAL

DrawerLayout

액티비티 화면에 보이지 않던 내용이 왼족이나 오른쪽에서 손가락 움직임에 따라 밀려나오는 기능. 서랍처럼 메뉴 구성시 사용

  • 레이아웃 XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/linear"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:text="hi"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start">

        <TextView
            android:text="text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </FrameLayout>

</androidx.drawerlayout.widget.DrawerLayout>
  • 레이아웃 XML에서 최상위에는 androidx.drawerlayout.widget.DrawerLayout 태그를 달고, 하위태그로 2개를 꼭 달아야 적용이 된다.

  • 첫번째 태그는 첫화면에 알아서 나오고, 두번째 태그의 android:layout_gravity 속성에따라 화면에서 나오는 방향을 지정해 해당 방향에서 숨겨져있다가 나온다.

  • 툴바영역에 토글 버튼을 함께 제공하는데, ActionBarDrawerToggle 클래스를 이용하면 된다.

class MainActivity : AppCompatActivity() {
    lateinit var toggle: ActionBarDrawerToggle
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        toggle = ActionBarDrawerToggle(this, binding.drawer, R.string.drawer_opened, R.string.drawer_closed)
        // 토글 버튼으로 사용할 아이콘이 출력된다.(이것만 있으면 <-버튼)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        // 이거까지 있으면 네비게이션아이콘(가로줄3개)
        toggle.syncState()
    }
	
    // 이 함수에서 메뉴 이벤트 처리를 해줘야한다.
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        
        if (toggle.onOptionsItemSelected(item)) {
            return true
        }
        
        return super.onOptionsItemSelected(item)
    }
    
}
  • 두번째 매개변수는 여닫는 드로어 객체, 세네번째는 문자열 리스소르 드로어가 열리고 닫힐때 상태를 표현한 문자열이다.

실습

  • 제트팩으로 화면 구성하기
    androidx로 툴바, 메뉴, 프래그먼트, 리사이클러 뷰, 뷰페이저2, 드로어 레이아웃 이용.
    프래그먼트로 화면 3개 만든 후 뷰페이저 2 이용

  • 메인화면

  • 드로어 레이아웃

  • 뷰페이저2

  1. themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.AndroidLab" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>
  1. fragment_one.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"/>
  1. OneFragment.kt
class MyViewHolder(val binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root)

class MyAdapter(val datas: MutableList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>(){
    override fun getItemCount(): Int {
        return datas.size;
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
        MyViewHolder(ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent,
            false))

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val binding = (holder as MyViewHolder).binding
        binding.itemData.text = datas[position]
    }

}

class MyDecoration(val context: Context): RecyclerView.ItemDecoration() {
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)

        val width = parent.width
        val height = parent.height

        val dr: Drawable? = ResourcesCompat.getDrawable(context.resources, R.drawable.kbo, null)
        val drWidth = dr?.intrinsicWidth
        val drHeight = dr?.intrinsicHeight

        val left = width/2 - drWidth?.div(2) as Int
        val top = height/2 - drHeight?.div(2) as Int

        c.drawBitmap(BitmapFactory.decodeResource(context.resources, R.drawable.kbo),
                left.toFloat(), top.toFloat(), null)
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        val index = parent.getChildAdapterPosition(view)+1
        if(index % 3 == 0)
            outRect.set(10, 10, 10, 60)
        else
            outRect.set(10, 10, 10, 0)

        view.setBackgroundColor(Color.parseColor("#28A0FF"))
        ViewCompat.setElevation(view, 20.0f)
    }
}
class OneFragment : Fragment() {

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

        val datas = mutableListOf<String>()
        for(i in 1..10){
            datas.add("Item $i")
        }

        val adapter = MyAdapter(datas)
        val layoutManager = LinearLayoutManager(activity)
        binding.recyclerView.layoutManager = layoutManager
        binding.recyclerView.adapter = adapter
        binding.recyclerView.addItemDecoration(MyDecoration(activity as Context))
        return binding.root

    }

}
  1. menu_main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/menu_search"
        android:title="search"
        app:actionViewClass="androidx.appcompat.widget.SearchView"
        app:showAsAction="always"/>
</menu>
  1. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <androidx.appcompat.widget.Toolbar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/toolbar"
            android:background="?attr/colorPrimary"/>
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </LinearLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="start">

        <TextView
            android:layout_width="300dp"
            android:layout_height="match_parent"
            android:background="#FF0000"
            android:text="Drawer!!"
            android:textSize="20dp"
            android:textStyle="bold"
            android:textColor="#FFFFFF"
            android:gravity="center_horizontal"
            android:fitsSystemWindows="true" />

    </FrameLayout>

</androidx.drawerlayout.widget.DrawerLayout>
  1. MainActivity.kt
class MainActivity : AppCompatActivity() {
    lateinit var toggle: ActionBarDrawerToggle

    class MyFragmentPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity){
        val  fragments: List<Fragment>
        init {
            fragments = listOf(OneFragment(), TwoFragment(), ThreeFragment())
        }

        override fun getItemCount(): Int = fragments.size
        override fun createFragment(position: Int): Fragment = fragments[position]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)

        toggle = ActionBarDrawerToggle(this, binding.drawer, R.string.drawer_opened, R.string.drawer_closed)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        toggle.syncState()

        val adapter = MyFragmentPagerAdapter(this)
        binding.viewpager.adapter = adapter
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        val inflater = menuInflater
        inflater.inflate(R.menu.menu_main, menu)
        val menuItem = menu?.findItem(R.id.menu_search)
        val searchView = menuItem?.actionView as SearchView
        searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
            override fun onQueryTextChange(newText: String?): Boolean {
                return true
            }

            override fun onQueryTextSubmit(query: String?): Boolean {
                Log.d("event", "search text : $query")
                return true
            }
        })
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {

        if (toggle.onOptionsItemSelected(item)){
            return true
        }
        return super.onOptionsItemSelected(item)
    }


}
profile
꾸준히 성장하기 위해 매일 log를 남깁니다!

0개의 댓글