안드로이드 리스트뷰에 여러 종류의 아이템뷰 사용하기. (Android ListView with Multi Item)
1. 아이템이 다른 ListView
지금까지 안드로이드 ListView와 관련하여 기본 사용법, 커스텀 아이템, 속성, 아이템 다루기 등 몇 가지 주제들에 대해 살펴보았습니다. 여기까지 살펴본 내용 만으로도 충분히 쓸만하고 멋진 ListView를 만들 수 있을 거라 생각되지만, 조금 더 다이나믹한 UI를 구성하기 위한 방법을 고민해보기로 하죠.
지금까지 예제들에서는 ListView의 아이템으로 오직 한 종류의 View만 사용되었습니다. TextView만으로 구성되어 있든, ImageView를 같이 사용하든 아이템의 위젯에 지정한 값만 다를 뿐 모든 아이템은 동일한 View로 구성되었죠.
그런데 만약 아이템마다 표시하는 내용 뿐만 아니라 View의 형태, 즉 View에 속한 위젯까지 다양하게 구성하려면 어떻게 하면 될까요?
아주 직관적인 방법으로, 사용하고자 하는 모든 위젯들을 하나의 View에 배치한 다음, 아이템에 지정된 데이터에 따라 View의 위젯을 show 또는 hide 시키면 되겠네요. 음.. 나쁘지 않은 방법인 것 같습니다. 하지만 뛰어난 해결책은 아니군요. 단순히 show/hide 정도만 사용되는 경우라면 무리없이 사용할 수 있겠지만 아이템에 지정된 데이터에 따라 위젯의 위치가 변경되거나, 폰트와 관련된 속성이 변경되거나, 텍스트 정렬이 변하는 등등의 작업이 수행되어야 한다면 그것대로 골치아픈 작업이 될 듯 합니다.
그럼 어떤 방법이 있을까요? 바로 제목에 나와 있듯이 아이템이 다른 ListView를 만드는 것입니다. 정확히 말하자면 두 종류 이상의 View를 ListView의 아이템으로 사용하도록 만드는 것이죠. 지금까지 처럼 한 가지의 아이템으로 구성하는 것보다 좀 더 다이나믹한 ListView를 만들 수 있을 것입니다.
1.1 아이템 View 타입에 대한 ListView의 동작 방식
ListView에서 다양한 아이템 View 타입을 지원하는 방식은 별로 복잡하지 않습니다. 지금까지 ListView를 다룰 때와 마찬가지로 Adapter에서 거의 대부분의 작업이 이루어지며, 몇 개의 함수를 새로 만들거나 조금 수정해주면 됩니다. 물론, 새로운 View에 대한 Layout 리소스는 추가해야겠지요.
예제 코드를 작성하기에 앞서 구체적인 작업 내용을 미리 언급하자면 아래와 같습니다.
- 새로운 아이템 View에 대한 Layout 리소스 추가.
- Adatper에서 제공할 View 타입에 대한 상수 정의.
- View 타입과 관련하여 Adapter에서 제공하는 함수 오버라이딩.
- getViewTypeCount() : View 타입에 대한 갯수 리턴
- getItemViewType() : position에 해당하는 View 타입 리턴.
- Adapter의 getView() 내용 수정.
- 현재 position에 따른 View 생성.
2. ListView의 아이템으로 두 가지 이상의 View 사용하기
아이템이 다른 ListView를 만드는 방법을 이해하기 위해서는 커스텀 ListView 만드는 방법에 대해 숙지하고 있어야 합니다. 다행히 커스텀 ListView를 만드는 방법은 [안드로이드 커스텀 리스트뷰 만드는 방법]에서 이미 설명해 놓았으니 참고하시기 바랍니다.
예제에서는 두 개의 View를 하나의 ListView 아이템으로 사용합니다. 각 View는 아래와 같이 구성됩니다.
2.1 워크플로우
예제 작성에 대한 워크 플로우는 [안드로이드 커스텀 리스트뷰 만드는 방법]에서 설명한 방법과 크게 다르지 않습니다. 한 개가 아닌 두 개의 아이템을 정의한다는 것 외에는 추가해야할 내용이 많지 않습니다.
2.2 Activity에 ListView 추가
ListView가 표시될 위치를 결정하여 ListView를 추가합니다. 예제에서는 MainActivity에 ListView를 추가하므로 "activity_main.xml" 파일(또는 "content_main.xml")에 관련 코드를 작성합니다. 아이템 간 영역 구분을 좀 더 명확하게 표시하기 위해 [안드로이드 리스트뷰 속성 활용]에서 설명한 "divider", "dividerHeight" 속성을 사용하였습니다.
[STEP-1] "activity_main.xml" - MainActivity에 ListView 추가
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.recipes4dev.examples.listviewdifferentitemexample1.MainActivity"
tools:showIn="@layout/activity_main">
<ListView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/listview1"
android:divider="#000000"
android:dividerHeight="5dp"/>
</RelativeLayout>
2.3 ListView 아이템에 대한 Layout 구성
아이템에 사용되는 두 개의 View 타입에 따라 각각 Layout 리소스 XML을 작성합니다.
먼저 두 개의 TextView와 하나의 CheckBox로 이루어진 아이템에 대한 Layout 리소스 XML 파일 내용입니다.
[STEP-2] "listview_item1.xml" - 첫 번째 아이템에 대한 Layout 리소스 XML 정의
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="9">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/title"
android:textSize="28sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/desc"
android:textSize="16sp"/>
</LinearLayout>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
다음은 ImageView와 TextView로 이루어진 아이템에 대한 Layout 리소스 XML 파일입니다.
[STEP-3] "listview_item2.xml" - 두 번째 아이템에 대한 Layout 리소스 XML 정의
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/image"
android:layout_weight="1"
android:scaleType="center"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Name"
android:id="@+id/name"
android:textSize="20sp"
android:textColor="#000000"
android:gravity="center_vertical"
android:layout_weight="4" />
</LinearLayout>
2.4 ListView 아이템 데이터 클래스 정의
ListView 아이템에 대한 View가 두 종류라 하더라도 각각에 대해 클래스를 정의할 필요는 없습니다. 두 가지 타입의 View에 표시될 데이터를 모두 포함하는 하나의 클래스만 정의한 다음, View 타입에 따라 필요한 멤버만 사용하면 되는거죠.
물론 객체지향 개념에 충실하게 아이템을 추상화하는 부모 클래스를 정의하고 각 View타입에 필요한 멤버를 가지는 두 개의 자식 클래스를 상속한 다음, 아이템이 어떠한 클래스에 대한 객체를 가지고 있는지를 판단하여 객체에 대한 레퍼런스를 사용하는 방법도 있습니다. Adapter에서는 부모 클래스에 대한 리스트만 유지하구요.
아니면 별도로 두 개의 클래스를 정의하고 Adapter에서 두 가지 클래스 객체에 대한 리스트를 유지하는 방법을 사용할 수도 있겠죠.
일단 아이템 데이터 클래스 구현은 본인의 스타일에 따라 편한대로 선택하면 될 것 같습니다. 세 가지 방법 모두 장 단점이 있는 방법이니까요.
살짝 다른 설명이 길었군요. 이제 첫 번째 설명한 방법대로 View들의 모든 위젯 데이터를 포함하는 아이템에 대한 클래스를 정의하겠습니다. "ListViewItem.java" 파일을 생성합니다.
[STEP-4] "ListViewItem.java" - ListView 아이템 데이터 클래스 정의
package com.recipes4dev.examples.listviewdifferentitemexample1;
import android.graphics.drawable.Drawable;
public class ListViewItem {
// 아이템 타입을 구분하기 위한 type 변수.
private int type ;
private String titleStr ;
private String descStr ;
private Drawable iconDrawable ;
private String nameStr ;
public void setType(int type) {
this.type = type ;
}
public void setTitle(String title) {
titleStr = title ;
}
public void setDesc(String desc) {
descStr = desc ;
}
public void setIcon(Drawable icon) {
iconDrawable = icon ;
}
public void setName(String name) {
nameStr = name ;
}
public int getType() {
return this.type ;
}
public Drawable getIcon() {
return this.iconDrawable ;
}
public String getTitle() {
return this.titleStr ;
}
public String getDesc() {
return this.descStr ;
}
public String getName() {
return this.nameStr ;
}
}
2.5 Adapter 구현
[안드로이드 커스텀 리스트뷰 만드는 방법]에서 BaseAdapter를 상속받아 Custom Adapter를 만드는 방법을 살펴보았죠. 거기에 몇 가지 추가 작업을 해주면 다중 아이템 지원을 위한 Adapter를 구현할 수 있습니다.
먼저 Adapter에 아이템 View 타입에 대한 상수를 정의합니다.
public class ListViewAdapter extends BaseAdapter {
private static final int ITEM_VIEW_TYPE_STRS = 0 ;
private static final int ITEM_VIEW_TYPE_IMGS = 1 ;
private static final int ITEM_VIEW_TYPE_MAX = 2 ;
}
그리고 View 타입과 관련하여 Adapter에서 제공하는 함수인 getViewTypeCount(), getItemViewType()를 오버라이딩합니다.
@Override
public int getViewTypeCount() {
return ITEM_VIEW_TYPE_MAX ;
}
// position 위치의 아이템 타입 리턴.
@Override
public int getItemViewType(int position) {
return listViewItemList.get(position).getType() ;
}
위에서 정의한 두 가지 View 타입을 지원하도록 getView함수를 작성합니다.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
int viewType = getItemViewType(position) ;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) ;
// Data Set(listViewItemList)에서 position에 위치한 데이터 참조 획득
ListViewItem listViewItem = listViewItemList.get(position);
switch (viewType) {
case ITEM_VIEW_TYPE_STRS:
convertView = inflater.inflate(R.layout.listview_item1,
parent, false);
TextView titleTextView = (TextView) convertView.findViewById(R.id.title ) ;
TextView descTextView = (TextView) convertView.findViewById(R.id.desc) ;
titleTextView.setText(listViewItem.getTitle());
descTextView.setText(listViewItem.getDesc());
break;
case ITEM_VIEW_TYPE_IMGS:
convertView = inflater.inflate(R.layout.listview_item2,
parent, false);
ImageView iconImageView = (ImageView) convertView.findViewById(R.id.image) ;
TextView nameTextView = (TextView) convertView.findViewById(R.id.name) ;
iconImageView.setImageDrawable(listViewItem.getIcon());
nameTextView.setText(listViewItem.getName());
break;
}
}
return convertView;
}
마지막으로 필수는 아니지만 아이템 추가의 편의를 위해, 각 아이템 별 데이터를 추가하는 함수를 작성하겠습니다.
// 첫 번째 아이템 추가를 위한 함수.
public void addItem(String title, String desc) {
ListViewItem item = new ListViewItem() ;
item.setType(ITEM_VIEW_TYPE_STRS) ;
item.setTitle(title) ;
item.setDesc(desc) ;
listViewItemList.add(item) ;
}
// 두 번째 아이템 추가를 위한 함수.
public void addItem(Drawable icon, String text) {
ListViewItem item = new ListViewItem() ;
item.setType(ITEM_VIEW_TYPE_IMGS) ;
item.setIcon(icon);
item.setText(text);
listViewItemList.add(item);
}
위에서 작업한 모든 코드와 기본적으로 Adapter가 가지는 기능을 포함하는 전체 코드는 아래와 같습니다.
[STEP-5] "ListViewAdapter.java" - BaseAdapter 상속 및 ListViewAdapter 구현.
package com.recipes4dev.examples.listviewdifferentitemexample1;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
public class ListViewAdapter extends BaseAdapter {
private static final int ITEM_VIEW_TYPE_STRS = 0 ;
private static final int ITEM_VIEW_TYPE_IMGS = 1 ;
private static final int ITEM_VIEW_TYPE_MAX = 2 ;
// 아이템 데이터 리스트.
private ArrayList<ListViewItem> listViewItemList = new ArrayList<ListViewItem>() ;
public ListViewAdapter() {
}
// Adapter에 사용되는 데이터의 개수를 리턴. : 필수 구현
@Override
public int getCount() {
return listViewItemList.size() ;
}
@Override
public int getViewTypeCount() {
return ITEM_VIEW_TYPE_MAX ;
}
// position 위치의 아이템 타입 리턴.
@Override
public int getItemViewType(int position) {
return listViewItemList.get(position).getType() ;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
int viewType = getItemViewType(position) ;
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) ;
// Data Set(listViewItemList)에서 position에 위치한 데이터 참조 획득
ListViewItem listViewItem = listViewItemList.get(position);
switch (viewType) {
case ITEM_VIEW_TYPE_STRS:
convertView = inflater.inflate(R.layout.listview_item1,
parent, false);
TextView titleTextView = (TextView) convertView.findViewById(R.id.title ) ;
TextView descTextView = (TextView) convertView.findViewById(R.id.desc) ;
titleTextView.setText(listViewItem.getTitle());
descTextView.setText(listViewItem.getDesc());
break;
case ITEM_VIEW_TYPE_IMGS:
convertView = inflater.inflate(R.layout.listview_item2,
parent, false);
ImageView iconImageView = (ImageView) convertView.findViewById(R.id.image) ;
TextView nameTextView = (TextView) convertView.findViewById(R.id.name) ;
iconImageView.setImageDrawable(listViewItem.getIcon());
nameTextView.setText(listViewItem.getName());
break;
}
}
return convertView;
}
// 지정한 위치(position)에 있는 데이터와 관계된 아이템(row)의 ID를 리턴. : 필수 구현
@Override
public long getItemId(int position) {
return position ;
}
// 지정한 위치(position)에 있는 데이터 리턴 : 필수 구현
@Override
public Object getItem(int position) {
return listViewItemList.get(position) ;
}
// 첫 번째 아이템 추가를 위한 함수.
public void addItem(String title, String desc) {
ListViewItem item = new ListViewItem() ;
item.setType(ITEM_VIEW_TYPE_STRS) ;
item.setTitle(title) ;
item.setDesc(desc) ;
listViewItemList.add(item) ;
}
// 두 번째 아이템 추가를 위한 함수.
public void addItem(Drawable icon, String text) {
ListViewItem item = new ListViewItem() ;
item.setType(ITEM_VIEW_TYPE_IMGS) ;
item.setIcon(icon);
item.setName(text);
listViewItemList.add(item);
}
}
2.7 Adapter 생성 및 ListView 지정
이제 위에서 추가한 Adapter를 Activity의 onCreate() 함수에서 생성한 다음, ListView에 지정해 줍니다.
[STEP-6] "MainActivity.java" - onCreate() 함수에서 ListView 및 Adapter 생성.
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... 코드 계속
ListView listview ;
ListViewAdapter adapter;
// Adapter 생성
adapter = new ListViewAdapter() ;
// 리스트뷰 참조 및 Adapter달기
listview = (ListView) findViewById(R.id.listview1);
listview.setAdapter(adapter);
// 코드 계속 ...
}
2.8 아이템 View 타입 별 데이터 추가
마지막으로 [STEP-6] 바로 아래에 ListView 아이템을 추가하는 코드를 작성합니다.
[STEP-7] "MainActivity.java" - onCreate() 함수에서 ListView 아이템 추가.
// 코드 계속 ...
// 첫 번째 아이템 추가.
adapter.addItem("The First Item", "this is the first item. Two TextView are used for title and desc.") ;
// 두 번째 아이템 추가.
adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_box_black_36dp),
"2nd : Account Box Black 36dp") ;
// 세 번째 아이템 추가.
adapter.addItem("The Third Item", "this is the third item. Two TextView are used for title and desc.") ;
// 네 번째 아이템 추가
adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_circle_black_36dp),
"4th : Account Circle Black 36dp") ;
// 다섯 번째 아이템 추가.
adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_assignment_ind_black_36dp),
"5th : Assignment Ind Black 36dp") ;
3. 아이템이 다른 ListView 예제 실행 화면
예제를 순서대로 작성하고 실행하면 아래와 같은 화면이 출력되며 다른 형태의 View가 ListView 아이템에 표시됨을 확인할 수 있습니다.
4. 참고.
- ListView 에 대한 자세한 도움말.
- [안드로이드 개발 참조문서 ListView 항목]을 참고하세요.
- 안드로이드 커스텀 ListView 만들기 예제.
- [안드로이드 커스텀 리스트뷰 만드는 방법] 내용을 참고하세요.
.END.