안드로이드 커스텀 리스트뷰에서 검색된 아이템 보여주기. (Android Custom ListView Item Filtering)

2016. 12. 12. 11:32


1. 커스텀 ListView와 아이템 필터링(filtering).

이전 글 [안드로이드 리스트뷰에서 검색된 아이템 보여주기. [textFilterEnabled] ]에서 TextView 하나로 구성된 기본 ListView의 아이템을 필터링(Filtering)하는 방법에 대해 살펴보았습니다.


[안드로이드 리스트뷰에서 검색된 아이템 보여주기. [textFilterEnabled] ]에서 설명했듯이, 안드로이드 ListView에는 아이템 필터링을 지원하기 위한 옵션이 이미 준비되어 있습니다. textFilterEnabled 속성이 바로 그것이죠.


하지만 ListView에 textFilterEnabled 속성이 제공된다고 하더라도 필터링 기능 자체가 ListView에서 구현되어 있다고 착각하면 안됩니다. 앞 서 여러 번 강조했듯이, ListView에 표시될 데이터를 처리하는 것은 Adapter의 역할이기 때문에, 실질적으로 아이템을 필터링하는 기능 또한 Adapter에 구현되어야 하기 때문입니다.

2. 필터링(filtering)을 지원하는 Adapter 구현.

Adapter가 필터링 기능을 지원하도록 만들기 위한 필수 요건은, Adapter가 Filterable 인터페이스를 implements해야 한다는 것입니다. [안드로이드 리스트뷰에서 검색된 아이템 보여주기. [textFilterEnabled] ]에서 ArrayAdapter를 사용한 것도, ArrayAdapter가 Filterable 인터페이스를 implements하고 있기 때문이죠.


그리고 Filterable 인터페이스의 implements와 더불어 Filter 클래스를 상속한 커스텀 Filter도 구현해야 합니다.


음, Filterable 인터페이스와 Filter 클래스에 대한 설명에 앞서, 둘 간의 관계 및 처리 흐름을 나타내는 아래의 그림을 먼저 보도록 하겠습니다.

커스텀 리스트뷰 필터링 처리 과정


2.1 Filterable 인터페이스

Filterable 인터페이스는 필터링 기능이 필요한 곳에서 사용되는 인터페이스입니다.

Filterable 인터페이스


필터링 기능을 제공해야 하는 클래스를 정의할 때, Filterable 인터페이스를 implements 한 다음, 해당 클래스의 참조를 전달하는 형식으로 사용하는 것이죠. 여기서는 Adapter가 그 역할을 수행하므로 커스텀 Adapter를 정의할 때 Filterable 인터페이스를 implements하면 됩니다.

class ListViewAdapter extends BaseAdapter implements Filterable {

    @Override
    public Filter getFilter() {
        // TODO : return custom filter.
    }
}

그런데 Filterable 인터페이스에 실질적인 필터링을 처리하는 함수가 정의되어 있을 것이란 예상과 달리, Filterable 인터페이스에 정의된 public 메소드는 getFilter() 함수가 유일합니다.

Filterable getFilter() 함수


그렇다면 실질적인 필터링 기능은 어디서 구현해야 하는 것일까요? 바로, getFilter() 함수의 정의와 설명으로 유추할 수 있듯이, getFilter() 함수가 리턴하는 Filter 클래스에 필터링 기능을 구현해야 합니다. Filterable 인터페이스를 implements하는 것은, 이 Filter 클래스의 참조를 획득하기 위한 과정인 것이죠.

2.2 Filter 클래스

Filterable 인터페이스의 getFilter() 함수를 통해 리턴되는 Filter 클래스는 추상(abstract) 클래스로 정의되어 있습니다.

Filter 클래스


개발자는 필터링 기능을 구현하기 위해, Filter 클래스를 상속한 커스텀 Filter 클래스를 정의한 다음, protected로 정의된 추상 함수인 performFiltering() 함수와 publishResults() 함수를 override해야 합니다.

abstract Filter.FilterResults performFiltering(CharSequence constraint) ;
abstract void publishResults(CharSequence constraint, Filter.FilterResults results) ;

performFiltering() 함수는 이름 그대로, 필터링을 수행하는 함수입니다. 즉, 필터링을 수행하는 루프를 이 함수에 구현한 다음, 필터링된 결과 리스트를 FilterResults에 담아서 리턴하면 됩니다.


publishResults() 함수는 performFiltering() 함수에서 필터링된 결과를 UI에 갱신시키는 역할을 수행합니다. 즉, publishResults() 함수에서 커스텀 Adapter를 통한 ListView 갱신 작업을 구현하면 됩니다.

3. 커스텀 ListView에서 아이템 필터링(filtering) 하기

이제 커스텀 ListView를 사용할 때, 아이템을 필터링(filtering)하는 예제를 직접 구현하도록 하겠습니다. 커스텀 ListView에 대한 전체적인 설명 및 구현 방법은 [안드로이드 커스텀 리스트뷰 만드는 방법]에서 확인하실 수 있습니다.


또한 예제의 전체적인 화면은 [안드로이드 리스트뷰에서 검색된 아이템 보여주기. [textFilterEnabled] ]에서 작성한 예제와 동일하게 구성하고, ListView의 아이템은 [안드로이드 커스텀 리스트뷰 만드는 방법]의 예제 화면을 사용하겠습니다.

커스텀 ListView 필터링 예제 화면 구성도


3.1 MainActivity의 Layout 구성.

먼저, MainActivity의 Layout 리소스 XML을 작성합니다. 프로젝트의 "activity_main.xml" 또는 "content_main.xml" 파일에 작성하면 됩니다. 특히, ListView의 "textFilterEnabled" 속성을 "true"로 지정한 것을 주의하세요. "textFilterEnabled" 속성을 "true"로 지정해야만, setFilterText() 함수를 사용하여 필터링 기능을 사용할 수 있습니다.

[STEP-1] "activity_main.xml" - MainActivity의 Layout 구성.
<?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.listviewcustomfilterexample.MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="26sp"
        android:layout_marginRight="8dp"
        android:id="@+id/textView1"
        android:text="Filter Text" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/textView1"
        android:id="@+id/editTextFilter"/>

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/editTextFilter"
        android:textFilterEnabled="true"
        android:id="@+id/listview1"/>

</RelativeLayout>

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

다음 단계로, ListView 아이템에 대한 화면 구성 작업을 진행합니다. "/res/layout/listview_item.xml" 파일에 아래의 내용을 작성하면 됩니다.

[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>

3.3 ListView 아이템 데이터 클래스 정의.

ListView의 아이템에 표시될 데이터 클래스를 정의합니다. 클래스의 이름은 "ListViewItem"로 정의합니다.

[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 ;
    }
}

3.4 커스텀 Adapter 추가 및 기본 동작 구현.

이제, 커스텀 ListView 기능 동작의 핵심 요소인 커스텀 Adapter를 구현하겠습니다.
이 글의 앞부분에서 언급했듯이, Adapter가 필터링 기능을 지원하기 위해서는 Filterable 인터페이스를 사용해야 합니다. 그래서 아래의 코드와 같이 커스텀 Adapter 클래스를 정의할 때, Filterable 인터페이스를 implements하도록 만들어야 합니다.

public class ListViewAdapter extends BaseAdapter implements Filterable {
    // TODO
}

ListView 아이템 표시를 위한 커스텀 Adapter의 기본 동작은 아래와 같이 구현합니다.

[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.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.ArrayList;

public class ListViewAdapter extends BaseAdapter implements Filterable {
    // Adapter에 추가된 데이터를 저장하기 위한 ArrayList. (원본 데이터 리스트)
    private ArrayList<ListViewItem> listViewItemList = new ArrayList<ListViewItem>() ;
    // 필터링된 결과 데이터를 저장하기 위한 ArrayList. 최초에는 전체 리스트 보유.
    private ArrayList<ListViewItem> filteredItemList = listViewItemList ;

    Filter listFilter ;

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

    }

    // Adapter에 사용되는 데이터의 개수를 리턴. : 필수 구현
    @Override
    public int getCount() {
        return filteredItemList.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(filteredItemList)에서 position에 위치한 데이터 참조 획득
        ListViewItem listViewItem = filteredItemList.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 filteredItemList.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);
    }

    // TODO : filtering item.
}

3.5 Filterable 인터페이스의 getFilter() 함수 override.

바로 위 단계에서 Filterable 인터페이스를 implements 하였으므로, Filterable 인터페이스의 public 메소드인 getFilter() 함수를 override해야 합니다. 참고로 getFilter() 함수를 통해 리턴되는 Filter 클래스는 다음 단계의 구현 내용을 참고하시기 바랍니다.

[STEP-5] "ListViewAdapter.java" - getFilter() 함수 override
public class ListViewAdapter extends BaseAdapter implements Filterable {
 
    Filter listFilter ;

    // 코드 계속 ...

    @Override
    public Filter getFilter() {
        if (listFilter == null) {
            listFilter = new ListFilter() ;
        }
        
        return listFilter ;
    }

    // ... 코드 계속
}

3.6 Filter 클래스 추가 및 구현.

이 글의 초반부에서 설명한 Filter 클래스의 역할에 따라, 커스텀 Adapter 내부에 커스텀 Filter 클래스를 정의하고 구현하도록 하겠습니다.

[STEP-6] "ListViewAdapter.java" - 커스텀 Filter 클래스 정의 및 구현.
public class ListViewAdapter extends BaseAdapter implements Filterable {
 
    // 코드 계속 ...

    private class ListFilter extends Filter {

        @Override
        protected FilterResults performFiltering(CharSequence constraint) {
            FilterResults results = new FilterResults() ;

            if (constraint == null || constraint.length() == 0) {
                results.values = listViewItemList ;
                results.count = listViewItemList.size() ;
            } else {
                ArrayList<ListViewItem> itemList = new ArrayList<ListViewItem>() ;

                for (ListViewItem item : listViewItemList) {
                    if (item.getTitle().toUpperCase().contains(constraint.toString().toUpperCase()) ||
                            item.getDesc().toUpperCase().contains(constraint.toString().toUpperCase()))
                    {
                        itemList.add(item) ;
                    }
                }

                results.values = itemList ;
                results.count = itemList.size() ;
            }
            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {

            // update listview by filtered data list.
            filteredItemList = (ArrayList<ListViewItem>) results.values ;

            // notify
            if (results.count > 0) {
                notifyDataSetChanged() ;
            } else {
                notifyDataSetInvalidated() ;
            }
        }
    }
}

3.7 Adapter 생성 후 ListView에 지정.

이전 단계에서 정의한 Adapter를 생성하여 ListView에 지정하는 코드를 작성합니다.

[STEP-7] "MainActivity.java" - onCreate() 함수에서 Adapter를 생성하여 ListView에 지정.
public class MainActivity extends AppCompatActivity {

    ListView listview = null ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListViewAdapter adapter;

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

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

        // 코드 계속 ...
    }
}

3.8 데이터 추가.

ListView 아이템의 ImageView에 표시될 이미지를 추가합니다.

drawable 리소스에 이미지 추가


그리고 예제 실행 결과를 확인하기 위한 임의의 데이터를 추가하도록 하겠습니다.

[STEP-8] "MainActivity.java" - 데이터 추가.
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // 코드 계속 ...

        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_box_black_36dp),
                "Sam Smith", "I'm not the only one.\r\nStay with me.\r\n") ;
        
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_circle_black_36dp),
                "Bryan Adams", "heaven.\r\nI do it for you.") ;
        
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_assignment_ind_black_36dp),
                "Eric Clapton", "Tears in heaven.\r\nChange the world.") ;

        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_box_black_36dp),
                "Gary Moore", "Still got the blues.\r\nOne day.") ;
        
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_account_circle_black_36dp),
                "Helloween", "A tale that wasn't right.\r\nI want out.") ;
        
        adapter.addItem(ContextCompat.getDrawable(this, R.drawable.ic_assignment_ind_black_36dp),
                "Adele", "Hello.\r\nSomeone like you.") ;
    }

3.9 EditText 텍스트 변경 이벤트 처리.

EditText(@id/editTextFilter)를 통해 ListView의 아이템을 필터링할 텍스트를 입력받은 다음, ListView의 setFilterText() 함수를 호출하여 필터링을 수행하도록 만듭니다.

[STEP-9] "MainActivity.java" - EditText 텍스트 변경 이벤트 처리
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속

        EditText editTextFilter = (EditText)findViewById(R.id.editTextFilter) ;
        editTextFilter.addTextChangedListener(new TextWatcher() {
            @Override
            public void afterTextChanged(Editable edit) {
                String filterText = edit.toString() ;
                if (filterText.length() > 0) {
                    listview.setFilterText(filterText) ;
                } else {
                    listview.clearTextFilter() ;
                }
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }
        }) ;
    }

4. 예제 실행 결과.

예제 코드 작성을 완료하고 앱을 실행하면, 다음과 같은 화면이 표시됩니다.

Custom ListView 필터링 예제 실행화면


EditText에 "g"를 입력하면, ListView 아이템의 텍스트 중에서 "g"(또는 "G")를 포함하는 아이템이 ListView에 표시됩니다. 그리고 입력된 "g"를 지우면 모든 아이템이 표시됩니다.

Custom ListView 필터링 예제 실행화면 2


다시 다른 텍스트를 입력하면, 해당 텍스트에 필터링(filtering)된 출력 결과가 표시되는 것을 확인할 수 있습니다.

ListView 필터링 예제 실행화면 3


5. 선택 수정 사항.

5.1 필터링 대상 요소 결정.

예제에서 ListView 아이템의 문자열 필터링 대상은 아이템 뷰를 구성하는 두 개의 TextView 입니다. 그런데 만약 이 중 하나의 TextView에 대해서만 필터링을 수행하고자 한다면, [STEP-6]의 performFiltering() 함수의 내용을, 아래와 같이 수정하면 됩니다.

    // Desc 항목에 대해서만 필터링하도록 변경.
    //  if (item.getTitle().toUpperCase().contains(constraint.toString().toUpperCase()) ||
    //      item.getDesc().toUpperCase().contains(constraint.toString().toUpperCase())) {}
    if (item.getDesc().toUpperCase().contains(constraint.toString().toUpperCase())) {
        itemList.add(item) ;
    }

5.2 필터링 사용 시 팝업 텍스트 표시하지 않기.

예제의 실행화면을 보면 다른 예제들에서는 볼 수 없는 한 가지 특이한 UI가 표시되는 것을 확인할 수 있습니다. 바로 EditText에 입력한 필터링 텍스트가 별도의 팝업 윈도우 형태로 출력된다는 것인데요. 아래 그림과 같습니다.

ListView 필터링 setFilterText 팝업 텍스트


필터링 텍스트 팝업은 ListView가 표시하는 UI이며, ListView의 setFilterText() 함수를 통해 필터링 텍스트가 전달되면 무조건 표시되게 만들어져 있습니다. 그래서 필터링 텍스트 팝업을 보이지 않게 만들고자 한다면, setFilterText() 함수 이외의 방법을 사용하여 필터링을 수행하면 됩니다. 즉, ListView를 통하지 않고 Adapter로부터 직접 Filter 객체의 참조를 가져와서 filter() 함수를 호출하면 되는 것이죠.


[STEP-9]의 소스 내용 중, afterTextChanged() 함수에서 setFilterText() 함수를 호출하는 코드를 아래와 같이 변경하면 됩니다.

/*
    if (filterText.length() > 0) {
        listview.setFilterText(filterText) ;
    } else {
        listview.clearTextFilter() ;
    }
*/
    ((ListViewAdapter)listview.getAdapter()).getFilter().filter(filterText) ;

6. 참고.

.END.


ANDROID 프로그래밍/LISTVIEW