Android

[안드로이드/AdapterView] Custom Adapter

gangmini 2023. 2. 23. 02:17
반응형

Custom Adapter Class

  • AdapterView 자체를 커스텀해 특별한 기능을 부여하고 싶을 때 Adapter 클래스를 구현
  • BaseAdapter 를 상속받아 getCount(), getView() 메소드를 구현 (필수 오버라이딩 메소드는 4개, 커스텀에 꼭 필요한건 이 2개)
    • getCount() 메소드 : AdapterView를 통해 보여줄 항목의 개수를 반환
    • getView() 메소드 : AdapterView를 통해 보여줄 항목의 View를 반환

[row.xml]

* 항목 내부에 TextView 뿐만 아니라 버튼을 넣고, 해당 버튼에 리스너를 달아 클릭 이벤트 동작을 실행 (기존의 Adapter Class 에서는 AdapterView에 이벤트를 감지와 동작을 작성하는 것이 불가능했음)

 

✔️ 단순히 항목을 예쁘게 화면에 표시하고 클릭하는 기능 -> ArrayAdapter/simpleAdapter Class 사용 + Custom ListView

✔️ 항목에 버튼을 달고 여러가지 기능 등을 추가 -> Custom Adapter Class 구현 +  Custom ListView

 

 

getView() 메소드

  • 화면에 항목이 보여질때마다 호출 (화면에 항목이 5개 보여지면 5번 호출), 항목 View를 생성해 반환
  • 항목 데이터가 100개면 100개의 View 객체가 생성되고 반환되는게 원리이지만 메모리 부하 문제로 화면에 보이지 않는 View 객체를 재사용 (*recycler view 와 유사) -> 만약 재사용 가능한 뷰가 없을 경우 새로운 뷰 객체 생성
  • 항목 레이아웃 (row.xml)에 커스텀한 버튼 클릭 메소드 등을 구현 
    • 이때 row.xml은 MainActivity 에서 관리하는 레이아웃이 아니라 ListView를 구성하고 있는 항목 view에 대한 레이아웃 이다.  따라서 항목 view 객체에 존재하는 버튼이나 텍스트뷰 같은 아이들은 따로 1)주소값을 찾아주거나 2) row.xml 에 대한 뷰바인딩이 이루어져야 사용할 수 있다. 

강좌에서는 findViewById 를 사용하는 방법은 매번 사용하기 번거롭기 때문에 View.run {...} 를 사용하면 항목뷰 객체가 가지고 있는 뷰들의 주소를 모두 가져올 수 있으므로 이 안에 기능을 구현하라고 소개했다. 

하지만 나는 이 방식을 사용해도 선언되지 않은 뷰를 사용했다고 오류가 났다.

 

해당 강좌는 안드로이드의 extension이 decreated 되기 전에 녹화한 영상이다. 이 당시에는 뭔가 자동으로 되는게 많은 것 같은데 지금은  뷰바인딩을 다 직접 해야 하기 때문에 상황이 달라진 것이 원인이라고 생각했다. 따라서 나는 이 문제를 row.xml 에 대한 뷰바인딩 선언이 제대로 이루어지지 않아서 라고 생각했다.

비슷한 이슈를 구글링 하다가 없어서 좌절하고 있었는데 강좌 커뮤니티에 누가 뷰바인딩 코드를 올려줘서 해결할 수 있었다. 

 

 var rowView = convertView as? RowBinding 

if (rowView == null) { // 재사용 가능 convertView 가 없는 경우
	rowView = RowBinding.inflate(layoutInflater)
}

rowView?.run{
                rowTextView.text = data1[position]
                rowButton1.tag = position
                rowButton2.tag = position

                rowButton1.setOnClickListener {
                    binding.textView1.text = "첫번째 버튼 클릭! : ${it.tag}"
                }
                rowButton2.setOnClickListener {
                    binding.textView1.text = "두번째 버튼 클릭! : ${it.tag}"
                }
            }

return rowView.root

as? 키워드를 사용해 재사용 가능항 항목뷰(convertView)를 RowBinding 객체로 형변환 해주고 만약 실패할 경우 null 로 초기화해 exception을 방지한다. null일 경우 항목뷰 객체를 생성해준다.

 

그리고 rowView?.run { ... } 안에서 rowView 안에 있는 뷰들을 위 코드와 같이 바로 사용할 수 있다. 만약 run {} 을 사용하지 않을 경우 rowView?.rowTextView.text = " "  이런식으로 사용하면 된다. 

 

 

[MainActivity.kt 전체 코드]

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.inflate
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.androidstudy_kotlin.databinding.ActivityMainBinding
import com.example.androidstudy_kotlin.databinding.RowBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    val data1 = arrayOf("데이터1","데이터2","데이터3","데이터4","데이터5")

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

        binding.list1.adapter = adapter1

    }

    val adapter1 = object : BaseAdapter() {

        // 항목의 개수를 반환
        override fun getCount(): Int {
            return data1.size
        }

        override fun getItem(p0: Int): Any? { //반환값이 null일 수 있음
            return null

        }

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

        // 화면에 보여줄 항목의 뷰를 반환 (화면의 5개의 항목이 보여지면 이 메소드가 5번 호출)
        override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
            // 스크롤하면 뷰가 계속 만들어지니까 메모리에 부담
            // 화면상에 항목 5~6개만 보이게 한다고 한다면 스크롤시 안 보이게 되는 뷰를 재사용
            
            // convertView : 더이상 화면에 보이지 않아 재사용이 가능한 항목뷰
            // positon : 항목의 인덱스 번호(0부터 시작)

            var rowView = convertView as? RowBinding 

            if (rowView == null) { // 재사용 가능 항목뷰 가 없는 경우
                // 새로운 항목(row layout) 만들어줌 (리스트뷰의 항목으로 사용하므로 타 레이아웃에 붙이지 않음)
                
                //rowView = getLayoutInflater().inflate(R.layout.row, null) //새로운 항목뷰객체를 만들때 레이아웃을 설정
                rowView = RowBinding.inflate(layoutInflater)
            }

            //항목뷰 내부에 배치되어 있는 뷰들의 주소값을 가져옴
            // (액티비티가 아니라 액티비티 안에 있는 다른 뷰(어댑터뷰,리스트뷰)가 관리하고 있는 뷰이므로 주소값 가져와야 함) -> 바인딩이 불가능 한가?..
            //val text1 = rowView?.findViewById<TextView>(R.id.rowTextView)
            //val btn1 = rowView?.findViewById<Button>(R.id.rowButton1)
            //val btn2 = rowView?.findViewById<Button>(R.id.rowButton2)

            /*
            rowbinding.rowTextView.text = data1[position]
            rowbinding.rowButton1.tag = position // 뷰에 태그를 저장 (태그는 object 타입이므로 아무거나 태그로 저장 가능)
            rowbinding.rowButton2.tag = position


            rowbinding.rowButton1.setOnClickListener {
                binding.textView1.text = "첫번째 버튼 클릭! : ${it.tag}"
            }

            rowbinding.rowButton2.setOnClickListener {
                binding.textView1.text = "두번째 버튼 클릭! : ${it.tag}"
            }

             */

            // findViewById 안 쓰고 rowView가 가지고 있는 모든 뷰들의 주소값을 다 가져와 뷰의 id와 동일한 이름의 변수로 선언
            rowView?.run{
                rowTextView.text = data1[position]
                rowButton1.tag = position
                rowButton2.tag = position

                rowButton1.setOnClickListener {
                    binding.textView1.text = "첫번째 버튼 클릭! : ${it.tag}"
                }
                rowButton2.setOnClickListener {
                    binding.textView1.text = "두번째 버튼 클릭! : ${it.tag}"
                }
            }

			//return rowView // 뷰바인딩 안할 경우
            return rowView.root
        }

    }

}
반응형