선택 기능을 지원하는 커스텀 리스트뷰 만드는 방법. (How to make custom choice ListView)

2016. 8. 9. 11:20


1. 선택 기능을 가진 커스텀 ListView의 필요 조건.

"다중 선택" 기능을 지원하는 "커스텀 ListView"를 만들기 위해서 앞서 살펴본 [안드로이드 커스텀 리스트뷰 만드는 방법]의 내용과 [안드로이드 리스트뷰 다중 선택 처리하기]의 내용을 적절히 혼합하면 될 것 같지만, 중요한 한 가지가 빠져 있습니다. 그것은 바로 ListView 아이템이 선택 가능(Checkable)한지, 그리고 현재 선택(Checked)되어 있는지 판단할 수 있는 기능이 추가되어야 한다는 것입니다. 무슨 의미인지 선뜻 이해가 되지 않죠?


일반적인 Custom ListView 만드는 방법으로 다중 선택 기능을 만든다고 가정해보죠.


일반적으로 선택(Checked) 여부를 표시하기 위해 ListView 아이템 Layout에 CheckBox를 배치할 것입니다. CheckBox는 선택(Checked) 기능을 포함하고 있으므로, CheckBox를 배치하는 것만으로 ListView 아이템 선택 기능을 사용할 수 있을 것 같네요. 하지만 그렇지 않습니다. 여기서 필요한 건 ListView 아이템 자체가 선택 가능(Checkable)한지 여부인 것이지, ListView 아이템 내의 CheckBox가 선택(Checked)되어 있는지가 아니기 때문입니다.

리스트뷰 선택 범위


"ListView 아이템에 속한 CheckBox의 선택(Checked) 여부와 ListView 아이템 자체의 선택(Checked) 여부가 무슨 차이가 있는가?", "어차피 CheckBox도 ListView 아이템에 속한 것이니 똑같은 거 같은데?" 이렇게 생각할 수도 있습니다.


그렇다면 [안드로이드 리스트뷰 다중 선택 처리하기]에서 본 대로, ListView의 선택된 아이템을 얻어오기 위해 ListView의 멤버 함수인 getCheckedItemPositions() 함수를 호출하게 되는 경우를 생각해보죠. getCheckedItemPositions() 함수는 ListView의 아이템이 선택(Checked) 상태인지를 리턴합니다. 아이템 내의 CheckBox 상태를 확인하는 것이 아니고, ListView 아이템 자체의 선택(Checked) 상태를 확인하는 것입니다.


만약 CheckBox의 선택(Checked) 상태를 확인하려면 ListView를 상속받는 새로운 클래스를 만들고 getCheckedItemPositions() 함수를 오버라이딩 한 다음, 해당 함수 내에서 CheckBox의 선택 여부를 판단해야겠죠. 음... 복잡하네요. 역시 ListView 아이템 자체가 선택 가능(Checkable)하도록 만드는 게 더 명료하겠네요.


결론적으로 선택(Checked) 기능을 지원하는 Custom ListView를 만들기 위해서는 ListView 아이템 자체에 선택(Checked) 여부를 판단할 수 있는 기능이 추가되어야 합니다.

1.1 Checkable 인터페이스

ListView 아이템에 선택(Checked) 여부를 판단할 수 있는 기능을 추가하려면 어떻게 해야 할까요? 바로 ListView 아이템의 루트(Root) 역할을 하는 ViewGroup이 Checkable 인터페이스를 implements 하도록 만들면 됩니다.


여기서 말하는 "루트(Root) 역할을 하는 ViewGroup"이란 ListView 아이템을 구성할 때 사용하는 Layout 리소스 XML 파일의 최상위 Layout 클래스(LinearLayout, RelativeLayout 등)를 지칭하는 것입니다.


예제를 작성하는 과정에서 다시 보겠지만, Checkable 인터페이스를 implements하는 Layout 클래스를 추가하여 사용하는 방법은 간단합니다. 원하는 종류의 Layout 클래스에서 상속받은 다음 Layout 클래스를 만들어서, 넣기만하면 되니까요.

package com.recipes4dev.examples.customchoicelistviewexample1;

class CheckableLinearLayout extends LinearLayout implements Checkable {
    // ... 
}
<com.recipes4dev.examples.customchoicelistviewexample1.CheckableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Deploy widgets for ListView Item -->

</com.recipes4dev.examples.customchoicelistviewexample1.CheckableLinearLayout>

2. 다중 선택 기능을 지원하는 커스텀 ListView 만드는 방법.

2.1 다중 선택 예제 구성도

다중 선택 예제를 위한 ListView 아이템은 ImageView, TextView, CheckBox 위젯으로 구성됩니다. 다중 선택 예제를 위한 예제 구성도는 다음과 같습니다.

custom choice listview item layout


2.2 ListView 추가.

먼저 ListView가 추가된 Layout 리소스 XML을 작성합니다. 다중 선택 기능을 위해 ListView의 "choiceMode" 속성에 "multipleChoice" 값을 지정합니다.

[STEP-1] "activity_main.xml" - MainActitivy에 ListView 추가.
    <ListView
        android:id="@+id/listview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:choiceMode="multipleChoice" />

2.3 ListView 아이템 Layout 구성.

ListView 아이템을 위한 Layout 리소스 XML을 작성합니다. 이 때 사용하는 아이템의 루트(Root)는, 위에서 잠깐 설명한대로 Checkable 인터페이스를 implements한 ViewGroup이어야 하므로 다음 단계에서 정의할 "CheckableLinearLayout"을 지정합니다.

[STEP-2] "listview_item.xml" - ListView 아이템 Layout 구성.
<?xml version="1.0" encoding="utf-8"?>
<com.recipes4dev.examples.customchoicelistviewexample1.CheckableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:descendantFocusability="blocksDescendants">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/imageView1"
        android:layout_weight="1" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="TEXT"
        android:id="@+id/textView1"
        android:textSize="20sp"
        android:gravity="center"
        android:layout_weight="5" />

    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/checkBox1"
        android:focusable="false"
        android:clickable="false"
        android:layout_weight="1" />

</com.recipes4dev.examples.customchoicelistviewexample1.CheckableLinearLayout>

2.4 Checkable 인터페이스를 implements한 LinearLayout 클래스 정의

다음으로 LinearLayout 클래스를 상속하고, Checkable 인터페이스를 implements한 클래스를 추가합니다.

[STEP-3] "CheckableLinearLayout.java" - Checkable 인터페이스를 implements 한 LinearLayout 클래스 정의.
public class CheckableLinearLayout extends LinearLayout implements Checkable {

    // 만약 CheckBox가 아닌 View를 추가한다면 아래의 변수 사용 가능.
    // private boolean mIsChecked ;

    public CheckableLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        // mIsChecked = false ;
    }

    @Override
    public boolean isChecked() {
        CheckBox cb = (CheckBox) findViewById(R.id.checkBox1) ;

        return cb.isChecked() ;
        // return mIsChecked ;
    }

    @Override
    public void toggle() {
        CheckBox cb = (CheckBox) findViewById(R.id.checkBox1) ;

        setChecked(cb.isChecked() ? false : true) ;
        // setChecked(mIsChecked ? false : true) ;
    }

    @Override
    public void setChecked(boolean checked) {
        CheckBox cb = (CheckBox) findViewById(R.id.checkBox1) ;

        if (cb.isChecked() != checked) {
            cb.setChecked(checked) ;
        }

        // CheckBox 가 아닌 View의 상태 변경.
    }
}

Checkable 인터페이스를 implements 하면 3개의 abstract 함수를 override해야 합니다. 각 함수에 대한 간략한 설명은 다음과 같습니다.

  1. boolean isCheck() : 현재 Checked 상태를 리턴.
  2. void setChecked(boolean checked) : Checked 상태를 checked 변수대로 설정.
  3. void toggle() : 현재 Checked 상태를 바꿈. (UI에 반영)

위의 세 가지 함수를 의미에 맞게 override하면 나머지는 ListView에서 발생하는 이벤트에 따라 적절한 함수가 호출됩니다.


그리고 예제에서는 아이템의 Checked 상태를 표시하기 위해 CheckBox 위젯을 사용했습니다. CheckBox는 자체적으로 Checked 상태를 관리할 수 있는 함수를 제공하기 때문에 별 다른 상태 관리 없이 제공되는 함수를 그대로 이용할 수 있죠. 하지만 만약 CheckBox 대신 ImageView 또는 TextView 위젯등을 사용한다면, Checked 상태를 별도로 관리해야 하므로, 위의 코드에서 주석으로 작성된 "mIsChecked" 변수를 사용하면 됩니다.

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

ListView 아이템에 출력될 데이터를 위한 클래스를 정의합니다. ListView 아이템이 ImageView와 TextView로 구성되므로, 각각 Drawable과 String으로 변수를 정의합니다. (CheckBox는 아이템 선택 여부를 표시하기 위한 용도이므로 매칭되는 변수를 정의하지 않았지만, 기능에 따라 boolean 변수를 추가하여 CheckBox의 초기 선택 여부를 위한 변수로 활용할 수 있습니다.)

[STEP-4] "ListViewItem.java" - ListView 아이템 데이터 클래스 정의.
public class ListViewItem {
    private Drawable icon ;
    private String text ;

    public void setIcon(Drawable icon) {
        this.icon = icon ;
    }
    public void setText(String text) {
        this.text = text ;
    }

    public Drawable getIcon() {
        return this.icon ;
    }

    public String getText() {
        return this.text ;
    }
}

2.6 ListView 아이템 View를 위한 Adapter 구현.

Adapter 클래스를 생성 및 정의합니다.

[STEP-5] : "CustomChoiceListViewAdapter.java" - BaseAdapter 상속 및 CustomChoiceListViewAdapter 구현.
public class CustomChoiceListViewAdapter extends BaseAdapter {
    // Adapter에 추가된 데이터를 저장하기 위한 ArrayList
    private ArrayList<ListViewItem> listViewItemList = new ArrayList<ListViewItem>() ;

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

    }

    // 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 textTextView = (TextView) convertView.findViewById(R.id.textView1) ;

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

        // 아이템 내 각 위젯에 데이터 반영
        iconImageView.setImageDrawable(listViewItem.getIcon());
        textTextView.setText(listViewItem.getText());

        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 text) {
        ListViewItem item = new ListViewItem();

        item.setIcon(icon);
        item.setText(text);

        listViewItemList.add(item);
    }
}

2.7 Adapter 생성 및 아이템 추가.

MainActivity 클래스의 onCreate() 함수에서 Adapter를 생성하고, ListView에 지정합니다.

[STEP-6] : Adapter 생성 및 아이템 추가.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        // 코드 계속 ... 

        ListView listview ;
        CustomChoiceListViewAdapter adapter;

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

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

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

3. 다중 선택 커스텀 ListView 실행 화면.

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

다중 선택 커스텀 ListView 실행 화면


그리고 이 글의 앞 부분에서 설명한대로 CheckBox가 아닌 ListView 아이템 영역 전체를 선택하면 아이템 선택 표시가 변경되는 것을 확인할 수 있습니다.

다중 선택 커스텀 ListView 실행 화면2


4. 추가 작업.

4.1 단일 선택(singleChoice) 모드로 사용하기.

예제의 소스 코드는 다중 선택(multipleChoice) 모드를 기준으로 작성되어 있는데, 만약 단일 선택(singleChoice)로 사용하고자 한다면, [STEP-1]의 코드 내용 중, "android:choiceMode" 속성을 "singleChoice"로 변경하면 됩니다.

4.2 아이템 추가, 삭제, 수정 기능 추가하기.

아이템에 대한 추가, 수정, 삭제 기능은 단일 선택(singleChoice) 모드를 사용할 경우, [안드로이드 리스트뷰 아이템 추가,수정,삭제]의 내용을 참고하시면 되고, 다중 선택(multipleChoice) 모드를 사용한다면 [안드로이드 리스트뷰 다중 선택 처리하기]의 예제 코드를 그대로 활용할 수 있습니다.

5. 참고.

.END.


ANDROID 프로그래밍/LISTVIEW