2. 컨테이너 : 목록 만들기
위젯의 위치를 다룰 때 레이아웃을 사용해싿면 위젯이나 다른 레이아웃에 데이터를 동적으로 표현해줄 때 컨테이너를 사용합니다. 컨테이너는 데이터를 반복적으로 표시하는 용도로 사용하는데 대표적인 컨테이너로는 목록을 화면에 출력할 때 사용하는 리사이클러뷰(RecyclerView)가 있습니다.
2.1 스피너
스피너(Spinner)는 여러 개의 목록 중에 하나를 선택할 수 있는 선택 도구입니다.
// 스피너에 사용할 목록 생성, 어댑터 생성, 스피너에 어댑터 등록
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
simple_list_item_1 레이아웃 simple_list_item_1 레이아웃은 텍스트뷰 1개만을 가지고 있는 특수한 레이아웃입니다. ArrayAdapter와 같은 기본 어댑터에 사용하면 입력된 데이터에서 문자열 1개를 꺼내서 레이아웃에 그려줍니다. |
스피너의 아이템 선택시 발생하는 이벤트 정의
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(p0: AdapterView<*>?) {
}
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
}
}
대부분 3번째 파라미터 p2만 사용합니다. 3번째 파라미터는 사용자가 몇 번째 아이템을 클릭했는지 해당 위치(position)를 의미합니다.
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
result.text = data.get(p2) //p2 = position
}
2.2 리사이클러뷰
리사이클러뷰(RecyclerView)는 스피너가 조금 더 확장된 형태입니다. 리사이클러뷰도 스피너처럼 목록을 화면에 출력하는데, 레이아웃 매니저를 이용하면 간단한 코드만으로 일반 리스트뷰(ListView)를 그리드뷰(GridView)로 바꿀 수도 있습니다.
[activity_main.xml]
리사이클러뷰 컨테이너를 추가하거나 작성해줍니다.
[item_recycler.xml]
리니어 레이아웃인 아이템 포멧 파일을 생성합니다.
어댑터 정의하기
리사이클러뷰는 리사이클러 어댑터라는 메서드 어댑터를 사용해서 데이터를 연결합니다. 스피너보다는 복잡한 구조이고 상속이 필요합니다. 상속 시 어댑터 관련 대부분의 기능을 사용 할 수 있고, 추가로 필요한 몇 개의 요소만 직접 구현합니다.
리사이클러뷰 어댑터는 개별 데이터에 대응하는 뷰홀더 클래스를 사용합니다. 상속하는 리사이클러뷰 어댑터에 뷰홀더 클래스를 제네릭으로 지정해야 하므로 뷰홀더 클래스를 먼저 만들고 나서, 어댑터 클래스를 생성하는 것이 편합니다.
class 커스텀어댑터 : RecyclerView.Adapter<여기에 사용할 뷰홀더 지정> {
}
상속받는 Adapter 클래스에 제네릭으로 뷰홀더를 지정해두면, Implement Methods로 코드를 자동 완설할 때에 자동 완성된 메서드 중 하나가 파라미터 타입에 제네릭으로 지정해둔 뷰홀더를 사용합니다.
class 커스텀어댑터 : RecyclerView.Adapter<뷰홀더> {
...
override fun onBindViewHolder(뷰홀더, 아이템 위치) {
}
}
뷰홀더 클래스도 기본 기능이 이미 만들어져 있는 ViewHolder 클래스를 상속받아서 만듭니다. 뷰홀더 클래스는 아이템 레이아웃을 포함하고 있는데, 1,000개의 데이터가 있다고 가정했을 때 이것들을 모두 화면에 그리기 위해서 1,000개의 아이템 레이아웃을 생성하면 시스템 자원이 낭비되고, 심각할 경우 앱이 종료될 수 있습니다.
뷰홀더는 현재 화면에 보여지는 개수만큼만 생성되고 목록이 위쪽으로 스크롤 될 경우 가장 위의 뷰홀더를 아래에서 재사용한 후 데이터만 바꿔주기 때문에 앱의 효율이 향상됩니다.
class 홀더(아이템 레이아웃) : RecyclerView.ViewHolder(아이템 레이아웃) {
}
레이아웃이 코드로 변환되면 View가 됩니다. 변수의 XML로 작성된 레이아웃 파일은 코드에서 사용하기 위해서 각각의 레이아웃 클래스로 변환됩니다. 예를 들어 XML 파일에 작성되어 있는 최상위 레이아웃의 태그가 <LinearLayout>이라면 XML 파일은 LinearLayout 클래스로 변환됩니다. 이렇게 변환된 레이아웃 클래스는 모두 View를 상속받아서 만들어지므로 파라미터 itemVIew의 타입에 View를 사용할 수 있습니다. 마치 액티비티가 Context를 상속받아서 만들어진 것과 같습니다. "모든 레이아웃은 코드로 변환되는 순간 View가 된다" |
Holder 내부의 코드가 실행되기 전에 어댑터 클래스 코드가 먼저 선행되어야 하기 때문에 어댑터 클래스를 먼저 수정해야 합니다.
class CustomAdapter : RecyclerView.Adapter<Holder>() {
}
어댑터 클래스의 기본 구성 어댑터가 정상적으로 동작하려면 미리 정의된 Holder 클래스를 제네릭으로 지정한 후 어댑터에 설계되어 있는 3개의 인터페이스를 반드시 구현해야 합니다. onCreateViewHolder() // 한 화면에 그려지는 아이템 개수만큼 레이아웃 생성 getItemCount() // 목록에 보여줄 아이템의 개수 onBindViewHolder() // 생성된 아이템 레이아웃에 값 입력 후 목록에 출력 |
class CustomAdapter : RecyclerView.Adapter<Holder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
}
override fun getItemCount(): Int {
}
override fun onBindViewHolder(holder: Holder, position: Int) {
}
}
해당 어댑터에서 맨 윗줄에 사용할 데이터 목록 변수를 하나 선언합니다. 목록형 컬렉션은 listOf() 계열의 메서드로 초기화 하며, 미리 작성해둔 loadData() 메서드에서 리턴해주는 값을 사용할 것이기 때문에 mutableListOf<Memo>( )를 사용합니다. 데이터가 담기는 listData변수에는 나중에 메인 액티비티에서 직접 호출해서 값을 넣습니다.
이어서 아이템 레이아웃을 생성하는 onCreateViewHolder() 메서드를 구현합니다. 프로그램을 실행하면 한 화면에 보이는 개수만큼 안드로이드가 호출합니다. 한 화면에 여덟 줄이 보여지면 여덟 번 호출됩니다. 한 화면에 여덟 줄이 보여지면 여덟 번 호출됩니다. LayoutInflater를 사용하면 특정 XMl 파일을 개발자가 직접 클래스로 변환할 수 있습니다. LayoutInflater는 화면 요소이므로 컨텍스트가 필요하고, inflate()메서드에 레이아웃을 지정해서 호출하면 클래스로 변환됩니다. 변환된 레이아웃 클래스는 바로 사용할 수는 없고, onCreateViewHolder()의 리턴 타입인 Holder 클래스에 담아서 사용합니다. 안드로이드는 이런 과정을 거쳐서 만들어진 Holder 클래스를 메모리에 저장해두고 사용합니다.
var listData = mutablelistOf<Memo>()
// getItemCount() 구현
override fun getItemCount(): Int {
return listData.size
}
// onCreateViewHolder() 구현
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_recycler,
parent, false)
return Holder(view)
}
한 줄로 작성한 return 편의상 view에 값을 저장한 다음에 return으로 넘겼으나 다음처럼 직접 return 해도 됩니다. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { return Holder(LayoutInflater.from(parent.context).inflate(R.layout.item_recycler, parent, false) |
inflate(resource, root, attachToRoot) 파라미터의 의미 - resource: View로 생성할 레이아웃 파일명(id)입니다. - root: attachToRoot가 true 또는 false에 따라 root의 역할이 결정됩니다. - attachToRoot: true일 경우 attach 해야 하는 대상으로 root를 지정하고 아래에 붙입니다. false일 경우 View의 최상위 레이아웃의 속성을 기본으로 레이아웃이 적용됩니다. |
스크롤이 될 때마다 실제 화면에 데이터와 레이아웃을 연결하는 onBindViewHolder() 메서드를 구현합니다. 먼저 listData에서 현재 위치에 해당하는 메모를 하나 꺼내 memo변수에 저장한 후 홀더에 전달합니다. (setMemo(Memo)를 따로 구현합니다.)
override fun onBindViewHolder(holder: Holder, position: Int) {
val memo = listData.get(position)
holder.setMemo(memo)
}
마찬가지로 나머지 2개의 위젯도 메모 데이터와 연결합니다. 날짜에 해당하는 timestamp값은 SimpleDateFormat을 사용해서 날짜 형식으로 먼저 변환합니다. SimpleDateFormat을 import하면 선택지가 2개 나타나는데 java.text의 SimpleDateFormat을 선택합니다. SimpleDateFormat을 생성하면서 생성자에 날짜가 보여질 형식을 'yyyy/MM/dd'로 정의합니다. SimpleDateFormat을 사용할 때는 Alt+Enter키를 눌러 import를 선택해야 합니다.
itemView.textTitle.text = memo.title
var sdf = SimpleDateFormat("yyyy/MM/dd")
var formattedDate = sdf.format(memo.timestamp)
itemView.textDate.text = formattedDate
SimpleDateFormat이란 이름 그대로 날짜 형식을 우리가 원하는 문자열 형태로 변환하는 도구입니다. SimpleDateFormat을 선언하면서 생성자에 다음 형식으로 지정해주면 됩니다. yyyy: 연도 4자리 MM: 월 d:일 h:시간 m:분 s:초 ex) yyyy/MM/dd/HH:mm:ss |
[MainActivity.kt]
class MainActivity : AppCompatActivity() {
lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 데이터 생성
val data:MutableList<Memo> = loadData()
// 어댑터 생성 및 listData변수에 데이터 목록 저장
var adapter = CustomAdapter()
adapter.listData = data
// 리사이클러뷰 인스턴스 생성 및 어댑터 연결
recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = adapter
// 리사이클러뷰를 화면에 보여주는 형태를 결정하는 레이아웃 매니저를 연결
recyclerView.layoutManager = LinearLayoutManager(this)
}
fun loadData(): MutableList<Memo> {
var data:MutableList<Memo> = mutableListOf()
for(no in 1..100) {
var title = "이것이 코틀린 안드로이드다 ${no + 1}"
var date = System.currentTimeMillis()
var memo = Memo(no, title, date)
data.add(memo)
}
return data
}
}
[Memo.kt]
class Memo(var no: Int, var title: String, var timestamp: Long) {
}
[CustomAdapter.kt]
// 각 텍스트
class CustomAdapter : RecyclerView.Adapter<Holder>(){
var listData = mutableListOf<Memo>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
var view = LayoutInflater.from(parent.context).inflate(R.layout.item_recycler,
parent, false)
return Holder(view)
}
override fun getItemCount(): Int {
return listData.size
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val memo = listData.get(position)
holder.setMemo(memo)
}
}
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun setMemo(memo: Memo) {
// textView 변수 생성 및 데이터 삽입
var textNo: TextView = itemView.findViewById<TextView>(R.id.textNo)
var textTitle: TextView = itemView.findViewById<TextView>(R.id.textTitle)
var textDate: TextView = itemView.findViewById<TextView>(R.id.textDate)
textNo.text = "${memo.no}"
textTitle.text = memo.title
var sdf = SimpleDateFormat("yyyy/MM/dd")
var formattedDate = sdf.format(memo.timestamp)
textDate.text = formattedDate
}
}
레이아웃 매니저의 종류
리사이클러뷰에서 사용할 수 있는 레이아웃 매니저의 종류는 세 가지입니다. 이중에서 세 번째 StaggeredGridLayoutManager는 핀터레스트 같은 사진 앱에서 자주 사용되는 형태입니다.
종류 | 내용 |
LinearLayoutManager | 세로 스크롤 : 기본으로 세로 스크롤을 하며 일반 리스트처럼 한 줄로 목록을 생성합니다. 추가로 설정하면 가로스크롤도 할 수 있습니다. 앞에서 사용한 것 같이 생성자에 컨텍스트 1개만 입력하면 됩니다. -> LinearLayoutManager(this) 가로 스크롤 : 컬럼 개수를 지정해서 개수만큼 그리드 형태로 목록을 생성합니다. 리니어 레이아웃 매니저의 두 번째 파라미터에 가로 스크롤 옵션을 설정합니다. -> LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) |
GridLayoutManager | 데이터의 사이즈에 따라 그리드의 크기가 결정됩니다. 두 번째 파라미터에 한 줄에 몇 개의 아이템을 표시할 것인지 개수를 설정합니다. -> GridLayoutManager(this, 3) |
StaggeredGridLayoutManager | 세로 스크롤 : 컨텍스트를 사용하지 않으므로 this를 넘기지 않아도 됩니다. 첫 번째 파라미터에는 한 줄에 표시되는 아이템의 개수, 두 번째 파라미터에는 세로 방량을 설정합니다. -> StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL) 가로 스크롤 : 두 번째 파라미터에 가로 방향 설정을 합니다. -> StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.HORIZONTAL) |
목록 클릭 이벤트 처리
이번에는 목록에서 아이템 1개가 클릭되었을 때 처리하는 방법을 알아보겠습니다.
간단하게 홀더가 가지고 있는 아이템뷰에 클릭리스너를 달고, 리스너 블록에 실행할 코드만 추가하면 목록이 클릭될 때마다 해당 코드가 실행됩니다.
init {
itemView.setOnClickListener {
Toast.makeText(itemView?.context, "클릭된 아이템 = ${itemView.textTitle.text}",
Toast.LENGTH_LONG).show()
}
}
모든 뷰는 컨텍스트를 가지고 있습니다. 토스트(Toast)와 같은 화면 요소를 사용하기 위해서는 컨텍스트가 필요합니다. 액티비티 이외의 다른 클래스에는 컨텍스트가 없기 때문에 액티비티로부터 미리 받아서 저장해두고 사용하기도 하는데, 단계가 복잡해지는 단점이 있습니다. 이럴 때 해당 클래스에서 뷰를 사용한다면 모든 종류의 뷰가 컨텍스트를 가지고 있기 때문에 뷰.context의 형태로 직접 꺼내서 사용할 수 있습니다. |
- 미니 퀴즈 5-2 -
1. 이것은 목록으로 뿌려지는 선택형 컨테이너로 리사이클러뷰의 기초 형태입니다. 이것은 무엇인가요?
답 : 스피너(Spinner)
2. 리사이클러뷰에서 데이터와 아이템 레이아웃을 연결해주는 것은 무엇인가요?
답 : 리사이클러 어댑터
3. XML로 작성된 레이아웃 파일을 코드에서 사용할 수 있도록 변환해주는 메서드의 이름은 무엇인가요?
답 : inflate(resource, root, attachToRoot) 메서드
4. 리사이클러뷰에서 생성되는 아이템 레이아웃을 재사용할 수 있도록 제공되는 클래스는 무엇인가요?
답 : ViewHolder(itemView) 클래스
'책 요약하기 > 이것이 안드로이드다' 카테고리의 다른 글
#5.5 탭 메뉴로 화면 구성 ViewPager와 TabLayout 2021-03-14 (0) | 2021.03.14 |
---|---|
#5-3. 프래그먼트 2021-03-12 (0) | 2021.03.12 |
#5-1. 화면 구성하기 2021-03-12 (0) | 2021.03.12 |
#4. 위젯과 리소스 다루기 2021-03-06 (0) | 2021.03.06 |
#3. 코틀린 사용을 위한 기본 문법 2021-03-04 (0) | 2021.03.04 |