/* 본 게시물은 '이것이 안드로이드다 with Kotlin | 고돈호 지음' 의 내용을 토대로 작성되었습니다. */
#리사이클러뷰
안드로이드의 대표적인 컨테이너로 리사이클러뷰가 있다. 컨테이너는 데이터를 반복적으로 표시하는 용도로 사용하는데 대표적인 컨테이너로는 목록을 화면에 출력할 때 사용하는 리사이클러뷰가 있다.
가장 많이 사용되는 것이 리사이클러뷰이고 가장 복잡한 것도 리사이클러뷰이다. 코드의 난이도가 갑자기 올라갈 수 있으니 리사이클러뷰의 축소 버전이라고 할 수 있는 스피너를 먼저 알아보자.
컨테이너는 레이아웃과는 다르게 내부 요소의 위치를 결정할 수 있는 속성이 없으므로 컨테이너를 사용할 때는 다른 레이아웃을 컨테이너 안에 삽입해서 사용한다.
1. 스피너
스피너는 여러 개의 목록 중에 하나를 선택할 수 있는 선택 도구이다. 우측의 그림처럼 스피너 우측에 있는 화살표를 누르면 선택할 수 있는 목록이 나열되는 형태이다. 마치 버튼이나 텍스트뷰 같이 작은 위젯처럼 보이지만 내부는 복수의 데이터를 처리할 수 있는 컨테이너 구조로 되어있다.
스피너는 어댑터라는 연결 도구를 사용해 화면에 나타낼 데이터와 화면에 보여주는 스피너를 연결한다.
스피너를 한 번 만들어보자.
SpinnerActivity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.spinner_example.*
class SpinnerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.spinner_example)
var data = listOf("- 선택하세요 -", "1월", "2월", "3월", "4월", "5월", "6월")
var adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,data)
spinner.adapter = adapter
spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
result.text = data.get(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
- data : 스피너의 데이터 리스트이다.
- adapter : 이미 구현되어 있는 어댑터에 data를 넣어준다. simple_list_item1 레이아웃은 텍스트뷰 1개만을 가지고 있는 특수한 레이아웃이다. ArrayAdapter와 같은 기본 어댑터에 사용하면 입력된 데이터에서 문자열 1개를 꺼내서 레이아웃에 그려준다.
- spinner.adapter : 스피너의 어댑터를 주입한다.
- spinner.onItemSelectedListner : 스피너의 onItemSelectedListener를 주입한다. result TextView를 선택한 데이터로 변경한다.
spinner_example.xml
<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:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="176dp"
android:layout_marginTop="120dp"
android:layout_marginEnd="177dp"
android:text="선택 결과"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="50dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="50dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/result" />
</androidx.constraintlayout.widget.ConstraintLayout>
실행결과
2. 리사이클러뷰
리사이클러뷰는 스피너가 조금 더 확장된 형태이다. 리사이클러뷰도 스피너처럼 목록을 화면에 출력하는데, 레이아웃 매니저를 이용하면 간단한 코드만으로 일반 리스트뷰를 그리드뷰로 바꿀 수도 있다.
리사이클러뷰처럼 목록을 표시하는 컨테이너들은 표시될 데이터와 아이템 레이아웃을 어댑터에서 연결을 해주므로 어댑터에서 어떤 아이템 레이아웃을 사용하느냐에 따라 표시되는 모양을 다르게 만들 수 있다.
리사이클러뷰를 한 번 만들어보자 이번에는 목록에 표시되는 아이템 레이아웃을 직접 만들어서 사용해보겠다.
item_recycler.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/textNo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="01" />
<TextView
android:id="@+id/textTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:text="Title" />
<TextView
android:id="@+id/textDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="2020-01-01" />
</LinearLayout>
Activity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_container.*
class ContainerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_container)
val data:MutableList<Memo> = loadData() // 데이터 생성및 저장
var adapter = CustomAdapter() //adapter 생성
adapter.listData = data //adapter 객체의 listData에 data 대입
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
fun loadData(): MutableList<Memo>{
val data:MutableList<Memo> = mutableListOf()
for (no in 1..100){
val title = "이것이 코틀린 안드로이드다 ${no + 1}"
val date = System.currentTimeMillis()
var memo = Memo(no, title, date)
data.add(memo)
}
return data
}
}
data class Memo(var no: Int, var title: String, var timestamp: Long)
- data : Memo 객체를 담을 뮤타블리스트이다.
- loadData() : data를 만드는 함수이다.
- adapter : CustomAdapter() 객체를 담을 변수이다.
- adapter.listData : adapter.listData 에 data를 추가한다.
- recyclerView.adapter : recyclerView에 어댑터를 주입한다.
CustomAdapter
리사이클러뷰는 리사이클러 어댑터라는 메서드 어댑터를 사용해서 데이터를 연결한다. 스피너보다는 훨씬 복잡한 구조이고 상속이 필요하다. 상속을 하면 어댑터와 관련된 대부분의 기능을 사용할 수 있고 추가로 필요한 몇 개의 요소만 개발자가 직접 구현한다. 리사이클러뷰 어댑터는 개별 데이터에 대응하는 뷰홀더 클래스를 사용한다. 상속하는 리사이클러뷰 어댑터에 뷰홀더 클래스를 제너릭으로 지정해야 하므로 뷰홀더 클래스를 먼저 만들고 나서, 어댑터 클래스를 생성하는 것이 편하다.
class CustomAdapter : RecyclerView.Adapter<Holder>(){
var listData = mutableListOf<Memo>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { // 프로그램을 실행하면 한 화면에 보이는 개수만큼 안드로이드가 호출
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_recycler,parent,false)
/*inflate(resource, root, attachToRoot) 파라미터 의미
resource : View로 생성할 레이아웃 파일명(id)이다.
root : attachToRoot 가 true 또는 false 에 따라 root 의 역할이 결정된다.
attachroot: true일 경우 attach 해야 하는 대상으로 root를 지정하고 아래에 붙인다. false일 경우 View의 최상위 레이아웃의 속성을 기본으로 레이아웃이 적용된다.
*/
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) { // 스크롤이 될 때 마다 실제 화면에 데이터와 레이아웃을 연결하는 onBindViewHolder() 메서드를 구현
val memo = listData.get(position)
holder.setMemo(memo)
}
override fun getItemCount(): Int {
return listData.size
}
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView){ //itemView 를 ViewHolder의 생성자에 전달한다.
init { // 목록클릭 이벤트 처리
itemView.setOnClickListener{
Toast.makeText(itemView?.context, "클릭된 아이템 = ${itemView.textTitle.text}", Toast.LENGTH_LONG).show()
}
}
fun setMemo(memo: Memo){
itemView.textNo.text = "${memo.no}"
itemView.textTitle.text = memo.title
var sdf = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
var formattedDate = sdf.format(memo.timestamp)
itemView.textDate.text = formattedDate
}
}
- Holder() : RecylcerView.ViewHolder()에 itemView를 전달해줌.
- itemView를 클릭 했을 시 toast를 생성한다.
- onCreateViewHolder() : ViewHolder를 생성하는 콜백함수로 Holder에 item_recycler view를 전달한다. 이 콜백함수는 데이터의 개수만큼 실행된다.
- onBindViewHolder() : 스크롤이 될 때 마다 실제 화면에 데이터와 연결시켜주는 콜백함수이다. Holder.setMemo()를 이용해 데이터를 UI에 업데이트한다.
- getItemCount() : 데이터의 size를 반환하는 콜백함수이다.
실행결과
ViewHolder를 왜 사용할까?
기본 기능이 이미 만들어져 있는 ViewHolder 클래스는 현재 화면에 보여지는 개수만큼만 생성되고 목록이 위쪽으로 스크롤 될 경우 가장 위의 뷰 홀더를 아래에서 재사용한 후 데이터만 바꿔주기 때문에 앱의 효율이 증가한다. ViewHolder 클래스는 아이템 레이아웃을 포함하고 있는데, 1000개의 데이터가 있다고 가정했을 때 이것들을 모두 화면에 그리기 위해서는 1000개의 아이템 레이아웃을 생성하면 시스템 자원이 낭비되고, 심각할 경우 앱이 종료될 수 있다.
레이아웃(xml)이 코드로 변환되면 View가 된다.
변수의 XML로 작성된 레이아웃 파일은 코드에서 사용하기 위해서 각각의 레이아웃 클래스로 변환된다. 예를 들어 XML 파일에 작성되어 있는 최상위 레이아웃의 태그가 <LinearLayout> 이라면 이 XML 파일은 LinearLayout 클래스로 변환된다. 이렇게 변환된 레이아웃 클래스는 모두 View를 상속받아서 만들어지므로 파라미터 itemView의 타입에 View를 사용할 수 있다. 마차 액티비티가 Context를 상속받아서 만들어진 것과 같은 구조이다. "모든 레이아웃은 코드로 변환되는 순간 View가 된다" 라고 생각하면된다.
레이아웃(xml)을 코드로 변환하는 방법
1. 뷰 바인딩를 사용
뷰 바인딩을 사용시 binding 객체를 사용가능 activity의 binding 객체를 넘겨받아 사용
2. LayoutInflater를 사용
LayouInflater를 사용하면 특정 XML 파일을 개발자가 직접 클래스로 변환할 수 있다. LayoutInflater는 화면 요소이므로 컨텍스트가 필요하고, inflate() 메서드에 레이아웃을 지정해서 호출하면 View 클래스로 변환된다.