본문 바로가기

유저 인터페이스/뷰(View)

#11. List 집중공략! - (3) Custom ArrayAdapter를 이용한 ListView



강좌 작성환경
SDK Version : Android SDK 1.5, release 2
ADT Version : 0.9.1

추후 SDK업데이트로 인해 글의 내용과 실제 내용간 차이가 있을 수 있습니다.

이번 강좌에서는 리스트뷰에 사용자가 원하는 레이아웃대로 항목을 표시하도록 만들어보겠습니다.

지난 강좌 (2009/06/04 - [안드로이드 이야기/안드로이드 입문] - #11. List 집중공략! - (1) 기본 다지기) 에서 설명했듯이, ListView는 ListView 하나로 이루어지는 것이 아니라 리스트뷰에 표시할 항목을 담고 있는 리스트 객체, 리스트 객체의 데이터를 리스트뷰에서 표시할 수 있게 해주는 어댑터, 최종적으로 화면에 리스트를 표시해 주는 리스트뷰(ListView)로 구성됩니다.

이 세 구성요소 중 가장 중요한 역할을 하는 것은 단연 어댑터(Adapter)라고 할 수 있습니다.
어댑터는 리스트 객체를 리스트뷰에서 표시해주는 기능, 즉 리스트 객체의 내용과 리스트 항목의 레이아웃을 연결시켜주는 역할을 합니다. 따라서, 어댑터가 없다면 일단 리스트 객체의 데이터를 리스트뷰에 표시하는 것은 불가능합니다.

그럼, 지난 강좌에서 썼던 ArrayAdapter를 한번 살펴볼까요?

ArrayAdapter<String> aa = new ArrayAdapter<String>(this,
        		android.R.layout.simple_list_item_1, list);


ArrayAdapter의 생성자에서 두번째 인자, android.R.layout.simple_list_item_1은 리스트에서 각 항목을 표시할 레이아웃을 뜻합니다. 이 레이아웃은 리스트 한 줄에 한 줄의 텍스트만이 표시되도록 되어있죠.

하지만, 리스트에 표시할 항목이 하나가 아닌 여러 가지가 있는 경우가 있습니다. 당장 연락처 어플리케이션이나 설정 어플리케이션을 봐도 리스트 항목 하나에 텍스트 하나만 표시되지는 않죠. 그래소, 이번 강좌에서는 리스트 항목 하나에 두 줄의 텍스트, 그리고 하나는 큰 텍스트, 하나는 작은 텍스트로 표시되도록 만들어 볼 것입니다.

이렇게, 리스트에 사용자가 원하는 레이아웃을 표시하기 위해서는 사용자 정의 어댑터를 만들어야 합니다. ArrayAdapter는 한 줄에 하나의 텍스트만을 표시하도록 코딩되어있으므로, 우리는 이와는 달리 두 줄로, 텍스트 두 개가 표시되게끔 어댑터를 만들어야겠지요?


이번 강좌에서는 위 스크린샷처럼 두 개의 텍스트를 표시하고, 위에는 이름, 아래는 전화번호를 표시하는 어댑터를 만들어보도록 하겠습니다.

이를 구현하기 위해 이름과 전화번호 데이터를 각각 String 형식으로 저장하고 있는 Person 클래스를 만들었으며, 이 객체의 저장은 ArrayList를 이용하였습니다. 새로 만들 어댑터의 이름은 PersonAdapter로 지정하였습니다.

일단, 전체 소스 코드와 레이아웃을 살펴보도록 하겠습니다.

[소스코드 : ListExample.java]

package com.androidhuman.ListExample;

import java.util.ArrayList;
import android.app.ListActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

public class ListExample extends ListActivity{ // ListActivity를 상속받습니다.
       
        
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ArrayList<Person> m_orders = new ArrayList<Person>();
        
        Person p1 = new Person("안드로이드", "011-123-4567"); // 리스트에 추가할 객체입니다.
        Person p2 = new Person("구글", "02-123-4567"); // 리스트에 추가할 객체입니다.
        
        m_orders.add(p1); // 리스트에 객체를 추가합니다.
        m_orders.add(p2); // 리스트에 객체를 추가합니다.
        
        PersonAdapter m_adapter = new PersonAdapter(this, R.layout.row, m_orders); // 어댑터를 생성합니다.
        setListAdapter(m_adapter); // 
                
    }
    
    private class PersonAdapter extends ArrayAdapter<Person> {

        private ArrayList<Person> items;

        public PersonAdapter(Context context, int textViewResourceId, ArrayList<Person> items) {
                super(context, textViewResourceId, items);
                this.items = items;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
                View v = convertView;
                if (v == null) {
                    LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    v = vi.inflate(R.layout.row, null);
                }
                Person p = items.get(position);
                if (p != null) {
                        TextView tt = (TextView) v.findViewById(R.id.toptext);
                        TextView bt = (TextView) v.findViewById(R.id.bottomtext);
                        if (tt != null){
                        	tt.setText(p.getName());                            
                        }
                        if(bt != null){
                        		bt.setText("전화번호: "+ p.getNumber());
                        }
                }
                return v;
        }
}
    class Person {
        
        private String Name;
        private String Number;
        
        public Person(String _Name, String _Number){
        	this.Name = _Name;
        	this.Number = _Number;
        }
        
        public String getName() {
            return Name;
        }

        public String getNumber() {
            return Number;
        }

    }
}

[ 레이아웃 : main.xml / 액티비티의 레이아웃 ]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   >
<ListView
    android:id="@+id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    />
<TextView
    android:id="@+id/android:empty"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:text="tets"/>
</LinearLayout>



[레이아웃 : row.xml / 리스트 항목의 레이아웃 ]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="6dip" android:orientation="vertical">
        <TextView
            android:id="@+id/toptext"
            android:layout_width="fill_parent"
            android:gravity="center_vertical"
        android:layout_height="wrap_content" android:textSize="20px"/>
        <TextView
            android:layout_width="fill_parent"
            android:id="@+id/bottomtext"
            android:singleLine="true"
            android:ellipsize="marquee"
        android:layout_height="wrap_content"/>
    
</LinearLayout>



우선, 이름과 전화번호 데이터를 저장할 수 있는 객체인 Person 클래스부터 살펴보겠습니다. 아주 간단~한 구조로 이루어져 있습니다.


    class Person {
        
        private String Name;
        private String Number;
        
        public Person(String _Name, String _Number){
        	this.Name = _Name;
        	this.Number = _Number;
        }
        
        public String getName() {
            return Name;
        }

        public String getNumber() {
            return Number;
        }

    }

Person 클래스 내에는 각각 이름(Name)과 전화번호(Number)를 저장하는 String형 변수가 2개 있고, 생성자를 통해 각각의 값을 초기화시키며 getName()메소드와 getNumber()를 통해 각각의 데이터를 반환하는 구조입니다.

그럼, 이번 강좌의 핵심인 어댑터, PersonAdapter를 보도록 하겠습니다.

private class PersonAdapter extends ArrayAdapter<Person> {

        private ArrayList<Person> items;

        public PersonAdapter(Context context, int textViewResourceId, ArrayList<Person> items) {
                super(context, textViewResourceId, items);
                this.items = items;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
                View v = convertView;
                if (v == null) {
                    LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    v = vi.inflate(R.layout.row, null);
                }
                Person p = items.get(position);
                if (p != null) {
                        TextView tt = (TextView) v.findViewById(R.id.toptext);
                        TextView bt = (TextView) v.findViewById(R.id.bottomtext);
                        if (tt != null){
                        	tt.setText(p.getName());                            
                        }
                        if(bt != null){
                        		bt.setText("전화번호: "+ p.getNumber());
                        }
                }
                return v;
        }
}


PersonAdapter 클래스는 ArrayAdapter 클래스를 상속하여, ArrayList의 데이터를 받아와 이를 ListView에 표시되게 해 줍니다. ArrayAdapter 클래스가 하나의 TextView만을 제공했던 것과 달리, PersonAdapter는 우리가 이 강좌에서 사용하는 Person 객체에 맞게끔 구현되어 있습니다.

그럼, PersonAdapter의 코드를 차근차근 보도록 하죠.
    private class PersonAdapter extends ArrayAdapter<Person> {

        private ArrayList<Person> items;

        public PersonAdapter(Context context, int textViewResourceId, ArrayList<Person> items) {
                super(context, textViewResourceId, items);
                this.items = items;
        }

일단, PersonAdapter 클래스 내부에 우리가 리스트에 표시할 항목을 저장할 리스트객체 (ArrayList items)가 보이네요. 이는 PersonAdapter 생성자를 통해 넘어온 리스트객체의 데이터를 저장하는 역할을 합니다. 생성자 내부에서는 생성자의 인자로 넘어온 리스트 객체(ArrayList items)를 PersonAdapter 내부의 리스트 객체 (this.items)로 연결시켜주는 모습을 확인할 수 있습니다.

@Override
        public View getView(int position, View convertView, ViewGroup parent) {
                View v = convertView;
                if (v == null) {
                    LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    v = vi.inflate(R.layout.row, null);
                }
                Person p = items.get(position);
                if (p != null) {
                        TextView tt = (TextView) v.findViewById(R.id.toptext);
                        TextView bt = (TextView) v.findViewById(R.id.bottomtext);
                        if (tt != null){
                        	tt.setText(p.getName());                            
                        }
                        if(bt != null){
                        		bt.setText("전화번호: "+ p.getNumber());
                        }
                }
                return v;


getView()메소드는 PersonAdapter 클래스의 핵심이라 할 수 있습니다. 우리가 원하는 기능 (리스트 항목에 두 줄의 텍스트가 표시되도록..) 을 이곳에서 구현하고 있거든요. 우선, getView()메소드의 API부터 보도록 하죠,


public abstract View getView (int position, View convertView, ViewGroup parent)
Get a View that displays the data at the specified position in the data set. You can either create a View manually or inflate it from an XML layout file. When the View is inflated, the parent View (GridView, ListView...) will apply default layout parameters unless you use inflate(int, android.view.ViewGroup, boolean) to specify a root view and to prevent attachment to the root.

Parameters
position
  The position of the item within the adapter's data set of the item whose view we want.
convertView  The old view to reuse, if possible.
Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view.
parent  The parent that this view will eventually be attached to

Returns
A View corresponding to the data at the specified position.


getView()메소드는 리스트 전체의 레이아웃을 책임지는(?) 것이 아니라, 리스트 각 항목에 대한 레이아웃만을 책임집니다. API에서도 position; 리스트 항목의 인덱스를 받아 그에 해당하는 레이아웃을 출력해주는 것을 볼 수 있습니다.


그럼, 각 항목에 대한 레이아웃은 어떻게 지정해줄까요?
아래의 코드에서 확인해보죠.

View v = convertView;
                if (v == null) {
                    LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    v = vi.inflate(R.layout.row, null);
                }


화면의 구성 단위인 View에 우리가 원하는 레이아웃을 적용시켜주기 위해 LayoutInflater를 사용하였습니다. LayoutInflater에 시스템 서비스를 받아온 후, inflate()메소드를 통해 레이아웃을 적용시켜주면 됩니다. inflate()메소드에 대한 API는 여기를 참조하세요.

Person p = items.get(position);
                if (p != null) {
                        TextView tt = (TextView) v.findViewById(R.id.toptext);
                        TextView bt = (TextView) v.findViewById(R.id.bottomtext);
                        if (tt != null){
                        	tt.setText(p.getName());                            
                        }
                        if(bt != null){
                        		bt.setText("전화번호: "+ p.getNumber());
                        }
                }
                return v;

이 부분부터는 실질적으로 Person 객체 내의 데이터를 화면에 표시해주는 역할을 해주고 있습니다.
아까 Person객체를 저장하고 있는 ArrayList를 PersonAdapter 내의 리스트에 저장했는데, 이 List로부터 리스트의 해당 인덱스의 데이터를 받아오게 됩니다. 그리고, 이 데이터가 null(데이터 없음) 이 아니라면, 우리가 지금까지 해왔던 방법과 똑같이 findViewById()메소드를 통해 레이아웃 객체를 참조하여 데이터를 화면에 표시해주게 됩니다.
데이터와 레이아웃을 연결해주는 작업이 끝나면, 최종적으로 작업이 완료된 View를 반환하여 화면에 표시하도록 합니다.


휴...
지금까지 그 어떤 강좌보다도 공부할 때에도 애를 많이 먹었고, 강좌 쓰는 시간도 많이 걸렸네요.
하지만, 그만큼 더 이것저것 많이 알게 된 계기가 되지 않않나 싶습니다 ^^;;

위의 방법을 사용해서 리스트에 이미지가 나오게도 할 수 있으니, 응용해서 한번 연습해보세요~ ㅎㅎ
다음 강좌는 일단 데이터베이스(SQLite)를 다루는 것을 목표로.... 서서히 공부해야나가야겠습니다 :)

p.s 강좌 소스 코드 첨부합니다.