안드로이드 리스트뷰에 여러 종류의 아이템뷰 사용하기. (Android ListView with Multi Item)

2016.04.12 11:18


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. 참고.

.END.


ANDROID 프로그래밍/LISTVIEW , , , , , , , , ,

  1. Blog Icon

    비밀댓글입니다

  2. 본문 내용 중에 잘못된 내용은 없을 거라 생각하지만,
    각자 설정된 개발환경, 사용하는 SDK, 테스트하는 기기 버전 등이 다르기 때문에,
    정확한 에러 원인은 "에러 메시지"를 확인하는 수 밖에 없습니다.
    그리고 위의 예제에서 이미지를 따로 추가하는 절차는 적어놓지 않았는데, 아마 그 때문에 에러가 생길 수도 있겠네요.

    안드로이드 스튜디오 사용법이 조금 생소하시더라도 "에러 메시지"를 확인하셔서 질문글에 남겨주시면, 문제 해결에 도움을 드릴 수 있을 것 같습니다.

  3. Blog Icon
    황찬우

    아 감사합니다!!
    저 intent 통해서 클릭한 리스트뷰에 해당하는 페이지로 화면전환시키는걸 하려하는데 혹시 리스트뷰에서 클릭할수 있게 어떻게 하나요??

  4. 리스트뷰 클릭 이벤트를 처리하시려면, setOnItemClickListener() 함수를 사용하여 리스트뷰에 클릭에 대한 리스너를 지정하시면 됩니다.

    관련 내용은,
    http://recipes4dev.tistory.com/42 - "2.5 ListView 클릭 이벤트 처리"
    또는
    http://recipes4dev.tistory.com/43 - "2.8 이벤트 처리" 를 확인하시면 됩니다.

    감사합니다.

  5. Blog Icon
    초보개발자

    안녕하세요! 제가 2개의 db Table을 생성하였는데 cursorAdapter를 이용하여 하나의 리스트뷰에 뷰가 다른 2개의 레이아웃을 적용시킬수 있을까요?ㅠㅠㅠ

  6. 일단 멀티뷰를 어떻게 적용할 것인지부터 한번 생각해보면...
    예제에서도 나와 있듯이, 데이터리스트를 뷰의 종류에 따라 별도로 가지지 않고, 하나의 데이터리스트를 가지고 멀티 뷰를 생성하는 것을 볼 수 있을 것입니다.
    하나의 데이터리스트에 들어 있는 아이템 데이터에서 특정 필드 값으로 구분하여 다른 종류의 아이템뷰를 생성하는거죠.

    그렇다면 질문의 내용에 대한 답이 자연스럽게 나오죠.

    테이블이 2개라고 해서 두 개의 CursorAdapter를 만들지 않아도, 또는 CusorAdapter에서 2개의 cursor를 관리하지 않아도..(더 어려울 듯...)
    2개의 테이블에서 각각 데이터를 쿼리하지 말고 SQL JOIN 등을 이용해서 한번에 쿼리하는 것입니다.

    그런 다음, 보통 cursor를 이용하는 것 처럼 데이터를 화면에 뿌리면 되겠죠. 물론, 어떤 종류의 뷰로 출력될 것인지는 쿼리 결과의 Row 값에 따라(특정 필드 값 존재유무?) 결정되야 하겠지요.

    음.. 원하시는 답변이 맞을런지 모르겠네요.
    잘 해결되시길 바랍니다.
    감사합니다.

  7. Blog Icon

    비밀댓글입니다

  8. 질문하신 내용은, 굳이 여러 종류의 아이템뷰를 사용할 필요 없이, 하나의 아이템 뷰만으로도 구현 가능할 것 같은데요.

    일반적인 커스텀 뷰 만드는 방법에서 어댑터의 getView() 함수에서 convertView의 background 색상만 바꿔주면 원하는 결과를 얻을 수 있을거라 생각됩니다.

    Row를 번갈아가면서 해야 한다고 했으니, getView() 함수 내의 position 값으로 표시될 색상을 결정하면 되겠죠.

    생각보다 간단한 작업이니, 쉽게 구현하실 수 있을 것 같네요. 구현하면서 잘 안되는 부분이 있으면 다시 질문글 남겨주세요.

    감사합니다.