안녕하세요? 닉네임간편입니다. 이번 시간에는 확장이 가능한 리스트뷰에 대해서 다루어보겠습니다.
1. 개요
아래 그림처럼 한 아이템을 클릭하면 리스트 형식의 아이템이 보였다가 사라지는 위젯입니다.
ListView의 일종이기에 사용 방법은 유사하지만, ExpandableListView는 부모 아이템과 자식 아이템이 있다는 차이가 있습니다.
2. 만드는 법
앞서 말씀드린듯 ListView를 만드는 방식과 유사합니다.
먼저 이 뷰를 구현하기 위해선 다음과 같은 요소들이 필요합니다.
1) ExpandableListView
이 뷰를 표시할 레이아웃에 위젯을 추가해야 합니다.
전 ConstraintLayout 내부에서 사용했으므로 아래와 같이 추가했습니다.
<ExpandableListView
android:id="@+id/expandableListView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fromText"
app:layout_constraintBottom_toTopOf="@+id/linear_from"
android:groupIndicator="@null">
</ExpandableListView>
이때 주의해야 할 점이 하나 있습니다.
개발자 문서에 따르면 ExpandableListView는 높이를 설정할 때 부모 뷰의 높이가 확실하게 정해져 있지 않다면 wrap_content를 사용하지 않아야 한다고 나와있습니다.
예를 들어서 ScrollView의 경우 높이가 확정적이지 않기 때문에 이 경우 ExpandableListView의 높이를 wrap_content로 설정하면 안 됩니다.
그러나 저는 메서드를 사용해 동적으로 높이를 조정할 것이기 때문에 괜찮습니다 ㅎ
물론 메서드를 사용하여서 높이를 설정할 것이 아니라면 높이를 확정적으로 설정해주셔야 합니다.
2) 부모 아이템 및 레이아웃
뷰에 사용할 부모 아이템과 레이아웃을 만들어줍니다.
아래 코드는 부모 아이템의 정보를 설정할 데이터 클래스입니다.
data class MenuTitle(val title: String, var index: Int)
아래 코드는 부모 아이템의 레이아웃이며, 시연 화면은 다음과 같습니다.
[코드]
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/main_menu">
<TextView
android:id="@+id/expand_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="기술스택"
android:textColor="@color/black"
android:textSize="16sp"
android:layout_margin="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/explainIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_close_explain"
android:layout_marginTop="15dp"
android:layout_marginEnd="20dp"/>
<View
android:id="@+id/custom_line"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#C1C1C1"
android:layout_marginTop="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/expand_title"/>
</androidx.constraintlayout.widget.ConstraintLayout>
3) 자식 아이템 및 레이아웃
부모 아이템을 클릭하면 자식 아이템이 리스트 형식으로 나열됩니다. 이때 필요한 자식 아이템 및 레이아웃을 만들어줍니다.
아래 코드는 자식 아이템의 정보를 설정할 데이터 클래스입니다. 저는 이 정보를 인텐트로 전달하려고 했기 때문에 Parcelize 어노테이션을 붙였습니다.(Parcelize에 대해선 여기를 참조하세요)
@Parcelize
data class MenuSpecific(val title: String, val detail: String?, val from: String, val img: Int?) :
Parcelable
[코드]
<?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"
android:layout_width="match_parent"
android:layout_height="140dp"
android:background="@color/white"
android:padding="15dp">
<TextView
android:id="@+id/expand_child_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:ellipsize="end"
android:maxLines="1"
android:text="Android"
android:textColor="@color/black"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/expand_child_detail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/expand_child_detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="with Kotlin"
android:textColor="@color/gray"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/expand_child_num"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@id/expand_child_title" />
<TextView
android:id="@+id/expand_child_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="by CreativeDuck"
android:textColor="@color/black"
android:textSize="17sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/expand_child_detail" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.70" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/expand_child_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="15dp"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/roundedLittleCorners"
android:src="@drawable/ic_launcher_foreground"
android:background="#88EA8C"/>
</androidx.constraintlayout.widget.ConstraintLayout>
4) BaseExpandableListAdapter()
리스트뷰에서도 어댑터가 필요했던 것처럼 어댑터를 만들어줍니다.
저는 커스텀 어댑터를 만들어 사용했으며, 코드는 아래와 같습니다.
[코드]
class ExpandableListAdapter(
private val context: Context,
private val parents: MutableList<MenuTitle>,
private val childList: MutableList<MutableList<MenuSpecific>>
) : BaseExpandableListAdapter() {
override fun getGroupCount(): Int {
return parents.size
}
override fun getChildrenCount(parent: Int): Int {
return childList[parent].size
}
override fun getGroup(parent: Int): Any {
return parents[parent]
}
override fun getChild(parent: Int, child: Int): Any {
return childList[parent][child]
}
override fun getGroupId(parent: Int): Long {
return parent.toLong()
}
override fun getChildId(parent: Int, child: Int): Long {
return child.toLong()
}
override fun hasStableIds(): Boolean {
return false
}
override fun getGroupView(parent: Int, isExpanded: Boolean, convertView: View?, parentView: ViewGroup?): View {
val binding = ExpandMenuTitleBinding.inflate(LayoutInflater.from(context), parentView, false)
binding.expandTitle.text= parents[parent].title
setArrow(binding, isExpanded)
return binding.root
}
override fun getChildView(parent: Int, child: Int, isLastChild: Boolean, convertView: View?, parentView: ViewGroup?): View {
val binding = ExpandChildBinding.inflate(LayoutInflater.from(context), parentView, false)
val item = getChild(parent, child) as MenuSpecific
binding.expandChildTitle.text= item.title
binding.expandChildDetail.text= item.detail
binding.expandChildFrom.text= from
if(item.img == null) {
binding.expandChildImage.visibility= View.GONE
} else {
binding.expandChildImage.setImageResource(item.img)
}
return binding.root
}
override fun isChildSelectable(p0: Int, p1: Int): Boolean {
return true
}
fun setArrow(binding: ExpandMenuTitleBinding, isExpanded: Boolean) {
if(isExpanded) {
binding.explainIcon.setImageResource(R.drawable.ic_close_explain)
} else {
binding.explainIcon.setImageResource(R.drawable.ic_show_explain)
}
}
}
5) 액티비티에 설정
이제 액티비티에 부모 아이템, 자식 아이템, 어댑터를 만든 후 ExpandableListView 객체에 설정해주면 완성입니다.
[코드]
fun setExpandableList() {
val parentList = mutableListOf<MenuTitle>(MenuTitle("Android", 0),
MenuTitle("IOS", 1),
MenuTitle("Server", 2))
val childList = mutableListOf<MutableList<MenuSpecific>>(
mutableListOf(
MenuSpecific("Kotlin", "사용 횟수",
16, R.drawable.ic_launcher_foreground),
MenuSpecific("Java", "사용 횟수",
20, R.drawable.ic_launcher_foreground)),
mutableListOf(
MenuSpecific("Swift", "사용횟수", 0, null),
MenuSpecific("Object C", "사용횟수", 0, null)
),
mutableListOf(
MenuSpecific("Python", "사용횟수", 0, null),
MenuSpecific("PHP", "사용횟수",0, null),
MenuSpecific("Node.js", "사용횟수", 0, null)
)
)
val adapter = ExpandableListAdapter(requireContext(), parentList, childList)
expandableListView.setAdapter(adapter)
expandableListView.setOnGroupClickListener{parent, view, groupPosition, id->
setListViewHeight(parent, groupPosition)
false
}
코드 하단을 보면 setListViewHeight() 메서드가 나옵니다. 이 메서드는 동적으로 뷰 객체의 높이를 설정하는 코드이며 코드는 아래와 같습니다.
[코드]
fun setListViewHeight(listView: ExpandableListView, group: Int) {
val listAdapter = listView.expandableListAdapteras ExpandableListAdapter
var totalHeight = 0
val desiredWidth: Int = View.MeasureSpec.makeMeasureSpec(
listView.width,
View.MeasureSpec.EXACTLY
)
for (i in 0untillistAdapter.groupCount) {
val groupItem: View = listAdapter.getGroupView(i, false, null, listView)
groupItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED)
totalHeight += groupItem.measuredHeight
if(listView.isGroupExpanded(i) && i != group
|| !listView.isGroupExpanded(i) && i == group) {
for (j in 0untillistAdapter.getChildrenCount(i)) {
val listItem: View = listAdapter.getChildView(
i, j, false, null, listView
)
listItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED)
totalHeight += listItem.measuredHeight
}
}
val params = listView.layoutParams
var height = (totalHeight + listView.dividerHeight* (listAdapter.groupCount-1))
if(height < 10) {
height = 200
}
params.height = height
listView.layoutParams= params
listView.requestLayout()
}
}
시연 화면입니다
3. 마무리
이번 시간에는 ExpandableListView에 대해서 알아보았습니다. 상용 서비스 중에도 이런 기능을 구현한 것이 많기 때문에, 이번 시간에 잘 정리되었으면 좋겠습니다.
'Android > 공부' 카테고리의 다른 글
에디트텍스트(EditText) 관련 짜투리 지식 1 (실시간으로 쉼표 표시하기, 커서 커스텀, 동그란 커서 색상 변경) (0) | 2021.12.01 |
---|---|
오픈소스 라이센스 명시 (0) | 2021.11.26 |
scope(스코프) 함수 (0) | 2021.11.13 |
뱃지 드로어블(BadgeDrawable) - Fab 버튼에 숫자 추가하기 (0) | 2021.10.26 |
스플래시(Splash) 화면 (2) | 2021.10.01 |