안드로이드 커스텀 리스트뷰 만드는 방법. (Android Custom ListView)

2016. 1. 12. 19:59

1. 안드로이드 Custom ListView.

이전 글 안드로이드 리스트뷰 기본 사용법 에서 TextView 위젯으로 구성된 가장 기본적인 형태의 ListView에 대해 설명하였습니다. ListView를 사용하는 방법을 이해하는데 도움은 되지만 실제로 앱에 사용하기엔 많이 부족하죠.


일반적으로 안드로이드 앱에 사용하는 ListView는 단순히 문자열만 표시하기보단 이미지나 버튼 또는 크기가 다른 문자열 등으로 구성하는 경우가 더 많습니다.
단순 문자열만이 아닌 여러 종류의 위젯을 하나의 아이템으로 구성한 ListView를 Custom ListView라고 하며, 안드로이드 앱을 만들 때 가장 많이 사용하는 컴포넌트 중 하나입니다.


지금부터 Custom ListView를 만드는 방법에 대해 설명하겠습니다.

2 Custom ListView 만들기.

앞선 글에서 간단히 언급했듯이 다양한 위젯들로 구성된 ListView를 만들기 위해서는 기본 사용법에서 몇 가지 추가 작업을 해 줘야 합니다.


하나의 이미지(ImageView)와 두 개의 문자열(TextView)로 구성된 ListView를 만드는 간단한 예제를 통해 Custom ListView를 만드는 방법에 대해 알아보겠습니다.

커스텀 리스트뷰 아이템 구성도


2.1 워크 플로우


커스텀 리스트뷰 만들기 절차


2.2 ListView가 표시될 위치 결정. (Layout 리소스 XML에 ListView 추가.)

안드로이드 리스트뷰 기본 사용법에서와 마찬가지로 MainActivity에 ListView를 생성합니다. "activity_main.xml" 파일(또는 "content_main.xml")에 아래의 내용을 작성합니다.

[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.example.madwin.customlistviewexample.MainActivity"
    tools:showIn="@layout/activity_main">

    <ListView
        android:id="@+id/listview1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

2.3 ListView 아이템에 대한 Layout 구성.

자 이제 Custom ListView를 만들기 위한 첫 번째 추가 작업을 해야 하는 순간이 왔습니다. 바로 ListView 아이템에 대한 화면 구성 작업입니다.


안드로이드 리스트뷰 기본 사용법에서는 ListView 아이템 Layout에 대한 구성을 고민하지 않았습니다. 왜냐하면 아이템이 하나의 TextView로 만들어졌기 때문이죠. 그렇다고 Layout에 대한 코드 작업을 전혀 안한 건 아닙니다.

Adapter를 생성하는 과정에서 "android.R.layout.simple_list_item_1"를 전달함으로써 아이템에 대한 Layout을 지정했던 것이죠.


ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, LIST_MENU) ;
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, LIST_MENU)

어댑터와 simple_list_item_1


"android.R.layout.simple_list_item_1"은 안드로이드 시스템 내부에 미리 만들어진 리소스 참조 중 하나입니다. SDK를 설치하면 자동으로 설치되며 어떠한 추가 작업 없이 개발자가 바로 사용할 수 있죠.
"simple_list_item_1" 말고도 미리 만들어진 리소스는 여러 가지가 있습니다.
관련 내용은 http://developer.android.com/reference/android/R.layout.html에서 확인할 수 있습니다.

ListView 아이템에 Layout은 "/res/layout/listview_item.xml"이라는 이름으로 작성하겠습니다. (참고로 Layout 리소스 파일의 이름은 원하는대로 지정해서 사용할 수 있습니다. 단, Java 코드에서 Layout을 참조할 때 "R.layout.listview_item"과 같은 형식으로 작성한 이름을 정확히 지정하기만 하면 됩니다.)


[STEP-2] "/res/layout/listview_item.xml" - ListView 아이템 Layout 작성.
<?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="match_parent"
        android:id="@+id/imageView1"
        android:layout_weight="1" />

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_weight="4">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="New Text"
            android:id="@+id/textView1"
            android:textSize="24dp"
            android:textColor="#000000"
            android:gravity="center_vertical"
            android:layout_weight="2" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="New Text"
            android:id="@+id/textView2"
            android:textSize="16dp"
            android:textColor="#666666"
            android:layout_weight="1" />
    </LinearLayout>

</LinearLayout>

2.4 ListView 아이템 데이터에 대한 클래스 정의.

ListView 아이템을 위한 Layout 리소스를 정의했다면 아이템에 출력될 데이터를 위한 클래스를 정의해야 하는데, 아이템에 표시될 위젯에 맞게 원하는대로 클래스 멤버를 결정하면 됩니다.

ListViewItem 이라는 이름으로 새로운 클래스를 생성합니다. (ListViewItem.java)


예제에서는 하나의 ImageView와 TextView 두 개를 사용하므로 Drawable과 String으로 멤버 변수를 정의합니다.

[STEP-3] "ListViewItem.java" - ListView 아이템 데이터 클래스 정의.
import android.graphics.drawable.Drawable;

public class ListViewItem {
    private Drawable iconDrawable ;
    private String titleStr ;
    private String descStr ;

    public void setIcon(Drawable icon) {
        iconDrawable = icon ;
    }
    public void setTitle(String title) {
        titleStr = title ;
    }
    public void setDesc(String desc) {
        descStr = desc ;
    }

    public Drawable getIcon() {
        return this.iconDrawable ;
    }
    public String getTitle() {
        return this.titleStr ;
    }
    public String getDesc() {
        return this.descStr ;
    }
}
class ListViewItem {
    var iconDrawable: Drawable? = null
    var titleStr: String? = null
    var descStr: String? = null
}


2.5 아이템 View를 위한 Adapter 구현

ListView 아이템 UI를 위한 Layout 리소스도 작성했고 아이템 데이터를 위한 클래스도 만들었으면 이제 할 일은 둘 간을 연결해 줄 Adapter를 구현하는 것입니다.
안드로이드 리스트뷰 기본 사용법에서는 별도의 Adapter를 구현하지 않고 안드로이드 SDK에서 제공하는 ArrayAdapter를 사용하였습니다. 아이템이 TextView만으로 구성되고(simple_list_item_1) 데이터가 String 배열이기 때문에 ArrayAdapter만으로 그 기능을 제공할 수 있기 때문입니다.


하지만 Custom ListView에서는 상황이 다릅니다. 데이터도 새로 정의한 클래스로 확장되었고 ListView 아이템도 여러 위젯으로 구성되죠. 이에 따라 Adapter 기능도 확장해야 합니다.


Adapter를 새롭게 구현할 때 안드로이드 SDK 에서 제공하는 Adapter 중 어떤 Adapter 클래스를 부모로 사용할 지 결정해야 합니다. 본인이 구현하고자 하는 Adapter의 기능에 적합한 것을 선택하면 되는데 보통 ArrayAdapter 또는 BaseAdapter를 많이 사용합니다. 일단 예제에서는 BaseAdapter를 사용하겠습니다.

[STEP-4] "ListViewAdapter.java" - BaseAdapter 상속 및 ListViewAdapter 구현.
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 {
    // Adapter에 추가된 데이터를 저장하기 위한 ArrayList
    private ArrayList<ListViewItem> listViewItemList = new ArrayList<ListViewItem>() ;

    // ListViewAdapter의 생성자
    public ListViewAdapter() {

    }

    // Adapter에 사용되는 데이터의 개수를 리턴. : 필수 구현
    @Override
    public int getCount() {
        return listViewItemList.size() ;
    }

    // position에 위치한 데이터를 화면에 출력하는데 사용될 View를 리턴. : 필수 구현
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final int pos = position;
        final Context context = parent.getContext();

        // "listview_item" Layout을 inflate하여 convertView 참조 획득.
        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.listview_item, parent, false);
        }

        // 화면에 표시될 View(Layout이 inflate된)으로부터 위젯에 대한 참조 획득
        ImageView iconImageView = (ImageView) convertView.findViewById(R.id.imageView1) ;
        TextView titleTextView = (TextView) convertView.findViewById(R.id.textView1) ;
        TextView descTextView = (TextView) convertView.findViewById(R.id.textView2) ;

        // Data Set(listViewItemList)에서 position에 위치한 데이터 참조 획득
        ListViewItem listViewItem = listViewItemList.get(position);

        // 아이템 내 각 위젯에 데이터 반영
        iconImageView.setImageDrawable(listViewItem.getIcon());
        titleTextView.setText(listViewItem.getTitle());
        descTextView.setText(listViewItem.getDesc());

        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(Drawable icon, String title, String desc) {
        ListViewItem item = new ListViewItem();

        item.setIcon(icon);
        item.setTitle(title);
        item.setDesc(desc);

        listViewItemList.add(item);
    }
}

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

// ListViewAdapter의 생성자
class ListViewAdapter : BaseAdapter() {
    // Adapter에 추가된 데이터를 저장하기 위한 ArrayList
    private var listViewItemList = ArrayList<ListViewItem>()

    // Adapter에 사용되는 데이터의 개수를 리턴. : 필수 구현
    override fun getCount(): Int {
        return listViewItemList.size
    }

    // position에 위치한 데이터를 화면에 출력하는데 사용될 View를 리턴. : 필수 구현
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        var view = convertView
        val context = parent.context

        // "listview_item" Layout을 inflate하여 convertView 참조 획득.
        if (view == null) {
            val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            view = inflater.inflate(R.layout.listview_item, parent, false)
        }

        // 화면에 표시될 View(Layout이 inflate된)으로부터 위젯에 대한 참조 획득
        val iconImageView = view!!.findViewById(R.id.imageView1) as ImageView
        val titleTextView = view.findViewById(R.id.textView1) as TextView
        val descTextView = view.findViewById(R.id.textView2) as TextView

        // Data Set(listViewItemList)에서 position에 위치한 데이터 참조 획득
        val listViewItem = listViewItemList[position]

        // 아이템 내 각 위젯에 데이터 반영
        iconImageView.setImageDrawable(listViewItem.icon)
        titleTextView.setText(listViewItem.title)
        descTextView.setText(listViewItem.desc)

        return view
    }

    // 지정한 위치(position)에 있는 데이터와 관계된 아이템(row)의 ID를 리턴. : 필수 구현
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // 지정한 위치(position)에 있는 데이터 리턴 : 필수 구현
    override fun getItem(position: Int): Any {
        return listViewItemList[position]
    }

    // 아이템 데이터 추가를 위한 함수. 개발자가 원하는대로 작성 가능.
    fun addItem(icon: Drawable, title: String, desc: String) {
        val item = ListViewItem()

        item.icon = icon
        item.title = title
        item.desc = desc

        listViewItemList.add(item)
    }
}

2.6 사용자 데이터 정의.

일반적인 상황에서는 DB 또는 파일(XML과 같은)에서 ListView의 데이터를 로드합니다. 여기서 DB에 관한 설명을 하자면 지면이 한참 모자라죠. 나중에 자세히 한번 살펴보는 것으로 하고 여기서는 간단하게 Java 소스에서 정적으로 데이터를 추가하겠습니다. 아래 2.7에서 소스 내용 확인하세요.


한 가지 덧붙이자면 아래 소스에서 사용한 "R.drawable.ic_account_box_black_36dp" 값은 "res/drawable/ic_account_box_black_36dp.png" 파일을 Java 소스에서 참조할 때 사용하는 리소스 ID입니다. "res/drawable"에 리소스를 추가하는 방법은 png, jpg 등의 이미지 파일을 해당 경로에 복사해놓기만 하면 됩니다.

프로젝트에 이미지 추가


2.7 Adapter 생성 후 ListView에 지정.

이제 Adapter를 생성하고 데이터를 추가해주면 ListView의 출력 결과를 확인할 수 있습니다.

[STEP-5] "MainActivity.java" - onCreate() 함수에서 ListView 및 Adapter 생성.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listview ;
        ListViewAdapter adapter;

        // Adapter 생성
        adapter = new ListViewAdapter() ;

        // 리스트뷰 참조 및 Adapter달기
        listview = (ListView) findViewById(R.id.listview1);
        listview.setAdapter(adapter);

        // 첫 번째 아이템 추가.
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_box_black_36dp),
                "Box", "Account Box Black 36dp") ;
        // 두 번째 아이템 추가.
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_circle_black_36dp),
                "Circle", "Account Circle Black 36dp") ;
        // 세 번째 아이템 추가.
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_assignment_ind_black_36dp),
                "Ind", "Assignment Ind Black 36dp") ;
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val listview: ListView
        val adapter: ListViewAdapter

        // Adapter 생성
        adapter = ListViewAdapter()

        // 리스트뷰 참조 및 Adapter달기
        listview = findViewById<View>(R.id.listview1) as ListView
        listview.adapter = adapter

        // 첫 번째 아이템 추가.
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_box_black_36dp)!!,
            "Box", "Account Box Black 36dp"
        )
        // 두 번째 아이템 추가.
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_circle_black_36dp)!!,
            
            "Circle", "Account Circle Black 36dp"
        )
        // 세 번째 아이템 추가.
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_assignment_ind_black_36dp)!!,
            "Ind", "Assignment Ind Black 36dp"
        )

2.8 이벤트 처리.

마지막으로 ListView 아이템 클릭 이벤트에 대한 처리를 해줍니다. ListView 아이템이 클릭되었을 때 호출되는 onItemClick() 함수에서 ListView 아이템 정보를 가져오려면 파라미터로 전달되는 변수들을 사용하면 됩니다. 각 파라미터에 대한 설명은 아래와 같습니다.

  • parent : ListView 자체에 대한 참조.
  • view : 클릭이 발생한 View에 대한 참조.
  • position : Adapter에서의 view의 position.
  • id : 클릭된 아이템의 row id.

여기서 parent를 통해 getItemAtPosition() 함수를 사용하면 position에 해당하는 아이템 데이터를 가져올 수 있습니다. 단, getItemAtPosition() 함수의 리턴 타입은 Object 타입이므로 소스에서 정의한 아이템 데이터 타입으로 형변환을 해줘야 합니다.

[STEP-7] "MainActivity.java" - onCreate() 함수 내에서 클릭 이벤트 처리.
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 기본 생성 코드 및 ListView와 Adapter 생성 코드
        // ...

        // 위에서 생성한 listview에 클릭 이벤트 핸들러 정의.
        listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView parent, View v, int position, long id) {
                // get item
                ListViewItem item = (ListViewItem) parent.getItemAtPosition(position) ;

                String titleStr = item.getTitle() ;
                String descStr = item.getDesc() ;
                Drawable iconDrawable = item.getIcon() ;

                // TODO : use item data.
            }
        }) ;
    }
        // 위에서 생성한 listview에 클릭 이벤트 핸들러 정의.
        listview.onItemClickListener = AdapterView.OnItemClickListener { parent, v, position, id ->
            // get item
            val item = parent.getItemAtPosition(position) as ListViewItem

            val title = item.title
            val desc = item.desc
            val icon = item.icon

            // TODO : use item data.
        }

3. Custom ListView 예제 실행 화면

위의 예제 소스를 순서대로 작성한 뒤 실행하면 다음과 같은 화면이 출력됩니다.
안드로이드 기본 생성 코드에 Material Design이 적용되었는지 여부에 따라 화면이 조금 다르게 나올 수 있으며 기기는 넥서스7 2세대를 사용하였습니다.


커스텀 리스트뷰 예제 실행 화면


4. 참고.

.END.


ANDROID 프로그래밍/LISTVIEW