RecyclerView #5 - 아이템 선택 처리하기



RecyclerView #1- 구조및 기본 사용법

RecyclerView #2 - 구분선 추가, 아이템간 간격 조절

RecyclerView #3 - 컨텍스트 메뉴 처리

RecyclerView #4 - 아이템 클릭 처리

RecyclerView #5 - 아이템 선택 처리하기

RecyclerView #6 - ItemView를 클래스화 하기

RecyclerView #7 - ViewType 동적변경




RecyclerView는 선택된 아이템의 선택상태나 선택된 아이템 리스트 관리등의 기능을 제공하지 않습니다.


아이템이 선택되었을때 배경색을 변경하거나, 

RecyclerView의 외부에서 선택 혹은 해제 처리를 하는 방법에 대해 정리해 보겠습니다.



특정 아이템을 클릭했을때 배경색상 변경하기

우선 생각해 볼수 있는 방법은 아이템이 클릭되었을때 onClick() 리스너r에서 itemView의 배경색을 변경하는 방법입니다.

ViewHolder가 보관하는 itemView에 onClick 리스너를 설정했으므로, 

onClick()리스너에서 넘어오는 View는 ViewHolder.itemView 객체입니다.

아래와 같이 배경색을 변경해 줄수 있습니다.

public StdViewHolder(@NonNull View itemView) {
super(itemView);
this.textView = itemView.findViewById(R.id.textView);

itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

v.setBackgroundColor(Color.BLUE);

Log.d("test", "position = "+ getAdapterPosition());
}
});

선택 해제 처리는?

onClick 이벤트 발생시, 클릭된 아이템의 position( ViewHolder.getAdapterPosition() )과 선택상태를 저장해 놓고 토글 시키면 될것 같습니다.

position별 선택상태를 저장하는 구조는 SparseBooleanArray를 사용하겠습니다.

public class StdRecyclerAdapter extends RecyclerView.Adapter<StdRecyclerAdapter.StdViewHolder> {

private SparseBooleanArray mSelectedItems = new SparseBooleanArray(0);

....


public class StdViewHolder extends RecyclerView.ViewHolder {
public TextView textView;

public StdViewHolder(@NonNull View itemView) {
super(itemView);
this.textView = itemView.findViewById(R.id.textView);

itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();

if ( mSelectedItems.get(position, false) ){
mSelectedItems.put(position, false);
v.setBackgroundColor(Color.WHITE);
} else {
mSelectedItems.put(position, true);
v.setBackgroundColor(Color.BLUE);
}

}
});


의도대로 잘 동작하는 것 같습니다만, 스크롤 해보면 이상한(혹은 당연한) 현상이 발생합니다.


 

"RecyclerView #1- 구조및 기본 사용법" 포스트에 첨부했던 이미지 인데요.

다시 한번만 정리하고 가겠습니다.


RecyclerView의 Adapter는 전체 아이템의 개수에 기반하여

적정수( 한화면에 표시되는 아이템 개수 + 스크롤시 사용할 여분의 아이템 개수)의 ViewHolder( View )를 미리 생성하고

화면상에 표시되지 않는 View를 재사용하는 구조입니다.


클릭할 당시의 View가 계속해서 동일한 데이터를 표시한다고 보장할 수 없다는 결론이죠.

즉 아이템의 선택상태는 position기반으로 관리하되, 실제 선택 상태를 표시하는 것은 ViewHolder에 데이터가

반영되는 시점에 처리 해주면 될것 같습니다.

* ViewHolder의 getAdapterPosition()은 ViewHolder의 재사용 여부와 관계 없이 아이템 배치순서상의 position( 0 base)을 리턴합니다.

DataSet내의 index값 이라고 생각해도 무방합니다.


그렇다면 

1. 아이템 클릭시 선택 상태 저장 및 선택 상태 표시

2. 아이템 바인딩 시에 선택 상태 표시

의 방법으로 구현해 보면 될것 같습니다.

itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();

if ( mSelectedItems.get(position, false) ){
mSelectedItems.put(position, false);
v.setBackgroundColor(Color.WHITE);
} else {
mSelectedItems.put(position, true);
v.setBackgroundColor(Color.BLUE);
}

}
});
public void onBindViewHolder(@NonNull StdViewHolder holder, int position) {
holder.textView.setText(mdata.get(position));

if ( mSelectedItems.get(position, false) ){
holder.itemView.setBackgroundColor(Color.BLUE);
} else {
holder.itemView.setBackgroundColor(Color.WHITE);
}
}


이제 의도한 대로 동작하는 것 같습니다.


하지만 RecyclerView 외부에서 특정 아이템을 선택하는 경우나,

선택된 아이템 전체를 해제(Clear)하는 것은 현재 구조로는 어려워 보이네요.

그리고 선택된 아이템의 배경색 변경도 한곳으로 몰고 싶어집니다.



아이템의 선택상태가 변경되는 시점( 아이템의 클릭이벤트가 발생하거나 아이템 선택/해제 Methord가 호출되었을때)에 선택 상태를 저장(갱신)하고

데이터가 View에 바인딩되는 시점인 onBindViewHolder() 에서 배경색을 변경하도록 처리하면 될것 같습니다.


다만 onBindViewHolder()가 호출되는 시점은 View에 Data를 바인드해야 하는 상황이므로,

Adapter에게 데이터를 다시 바인딩 해야 함을 알려주어야 합니다.

우리가 작성한 코드에서는 setData() 호출을 통해 데이터 셋을 설정하는 코드입니다.


public void setData(List<String> data) {
mdata = data;
notifyDataSetChanged();
}


RecyclerView Adapter는  표시되는 데이터를 다시 바인딩해야 하는 상황에서 

Adapter에게 알려줄수 있도록 notify~ 류의 메소드를 제공합니다.




데이터가 변경된건 아니지만, 강제로 데이터를 다시 그리도록 처리하는 방법입니다.

일종의 트릭이겠죠?


public class StdRecyclerAdapter extends RecyclerView.Adapter<StdRecyclerAdapter.StdViewHolder> {


@Override
public void onBindViewHolder(@NonNull StdViewHolder holder, int position) {
holder.textView.setText(mdata.get(position));

if (isItemSelected(position)) {
holder.itemView.setBackgroundColor(Color.BLUE);
} else {
holder.itemView.setBackgroundColor(Color.WHITE);
}
}



public class StdViewHolder extends RecyclerView.ViewHolder {
public TextView textView;

public StdViewHolder(@NonNull View itemView) {
super(itemView);
this.textView = itemView.findViewById(R.id.textView);

itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
toggleItemSelected(position);
}
});
}
}


아이템의 선택 상태 관리는 별도의 메소드로 분리했습니다.

private void toggleItemSelected(int position) {

if (mSelectedItems.get(position, false) == true) {
mSelectedItems.delete(position);
notifyItemChanged(position);
} else {
mSelectedItems.put(position, true);
notifyItemChanged(position);
}
}

private boolean isItemSelected(int position) {
return mSelectedItems.get(position, false);
}

public void clearSelectedItem() {
int position;

for (int i = 0; i < mSelectedItems.size(); i++) {
position = mSelectedItems.keyAt(i);
mSelectedItems.put(position, false);
notifyItemChanged(position);
}

mSelectedItems.clear();
}


전체 선택 해제 버튼도 하나 추가했구요.

의도한 대로 잘 동작하는 것 같습니다.




마지막으로 

BackgroundColor 를 변경하는 부분을 XML을 통해 처리 할 수 있습니다.

Xml selector에 select된 상태와 select되지 않은 상태에 적용할 색상 혹은 이미지를 지정하고,

대상 위젯의 BackGround속성으로 해당 selector를 할당 하는 방식입니다.


적용하는 방법은 아래와 같습니다.


"drawable" 폴더 하위에 선택 상태에 따라 표시할 색상을 지정하는 item_selector.xml을 작성합니다.

( 이때 xml 파일이 drawable-hdpi, drawable-hdpi, drawable-v24 등과 같은 사이즈 종속적인 폴더에 들어가지 않도록 주의 해야 합니다.

샘플 작업시, 해당 파일을 drawable-v24 폴더에 생성하는 바람에 테스트한 실제 장비에서는 정상적으로 동작하였지만,

Emulater에서 해당 selector를 적용한 Layout에 대해 아래와 같은 inflate 에러 메시지가 발생하여  앱이 종료된바 있습니다.)

android.view.InflateException: Binary XML file line #14: Binary XML file line #14: Error inflating class <unknown>


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="false">
<color android:color="@color/normal_item_bg" />
</item>
<item android:state_selected="true">
<color android:color="@color/selected_item_bg" />
</item>
</selector>


사용되는 배경색으로 사용되는 색상은 color.xml에 정의 되어 있어야 합니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>

<color name="normal_item_bg">#FFFFFFFF</color>
<color name="selected_item_bg">#FF0000FF</color>

</resources>



아이템을 표시할 Layout의 Root Layout에 Background를 다음과 같이 지정합니다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/item_selector">



onBindViewHolder의 코드를 다음과 같이 수정하면 선택 상태에 따라 자동으로 배경색이 변경되게 됩니다.

( onCreateViewHolder에서 Inflate된 View와 layout xml의 root LinearLayout이 같은 지는 모르겠으나,

적어도 Layout간에는 상위 Layout의 선택 상태가 하위 Layout에도 반영되는 것으로 보입니다.)

public void onBindViewHolder(@NonNull StdViewHolder holder, int position) {
holder.textView.setText(mdata.get(position));

holder.itemView.setSelected(isItemSelected(position));
}





롱클릭 이후 데이터를 선택하게 하거나, 단일 선택등의 처리는 조금 응용하면 가능할 것으로 생각됩니다.


<< 전체코드 >


StdRecyclerAdapter.java

public class StdRecyclerAdapter extends RecyclerView.Adapter<StdRecyclerAdapter.StdViewHolder> {

public interface OnListItemLongSelectedInterface {
void onItemLongSelected(View v, int position);
}

public interface OnListItemSelectedInterface {
void onItemSelected(View v, int position);
}

private OnListItemSelectedInterface mListener;
private OnListItemLongSelectedInterface mLongListener;


private SparseBooleanArray mSelectedItems = new SparseBooleanArray(0);

Context mContext;
List<String> mdata;
RecyclerView recyclerView;

public StdRecyclerAdapter(Context context
, RecyclerView recyclerView
, OnListItemSelectedInterface listener
, OnListItemLongSelectedInterface longListener) {
this.mContext = context;
this.mListener = listener;
this.mLongListener = longListener;
this.recyclerView = recyclerView;
}

public void setData(List<String> data) {
mdata = data;
notifyDataSetChanged();
}

@NonNull
@Override
public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflate = LayoutInflater.from(mContext);
View view = inflate.inflate(R.layout.list_item, parent, false);

StdViewHolder vh = new StdViewHolder(view);
return vh;
}

@Override
public void onBindViewHolder(@NonNull StdViewHolder holder, int position) {
holder.textView.setText(mdata.get(position));

holder.itemView.setSelected(isItemSelected(position));
}

@Override
public int getItemCount() {
return mdata.size();
}

public class StdViewHolder extends RecyclerView.ViewHolder {
public TextView textView;

public StdViewHolder(@NonNull View itemView) {
super(itemView);
this.textView = itemView.findViewById(R.id.textView);

itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = getAdapterPosition();
toggleItemSelected(position);

Log.d("test", "position = " + position);
}
});

itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
mLongListener.onItemLongSelected(v, getAdapterPosition());
return false;
}
});
}
}

private void toggleItemSelected(int position) {

if (mSelectedItems.get(position, false) == true) {
mSelectedItems.delete(position);
notifyItemChanged(position);
} else {
mSelectedItems.put(position, true);
notifyItemChanged(position);
}
}

private boolean isItemSelected(int position) {
return mSelectedItems.get(position, false);
}

public void clearSelectedItem() {
int position;

for (int i = 0; i < mSelectedItems.size(); i++) {
position = mSelectedItems.keyAt(i);
mSelectedItems.put(position, false);
notifyItemChanged(position);
}

mSelectedItems.clear();
}
}


MainActivity.java

public class MainActivity extends AppCompatActivity implements StdRecyclerAdapter.OnListItemLongSelectedInterface
, StdRecyclerAdapter.OnListItemSelectedInterface{

RecyclerView recyclerView;

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

init();
}

private void init(){
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);

List<String> dataSet = new ArrayList<String>();

int row = 1;
for ( int i = 0; i < 10 ; i++) {
dataSet.add("<" + row++ + ">" + "C/C++");
dataSet.add("<" + row++ + ">" + "Java");
dataSet.add("<" + row++ + ">" + "Kotlin");
dataSet.add("<" + row++ + ">" + "Python");
dataSet.add("<" + row++ + ">" + "Ruby");
}

final StdRecyclerAdapter mAdapter = new StdRecyclerAdapter(this, recyclerView, this,this);
recyclerView.setAdapter(mAdapter);
mAdapter.setData(dataSet);

// 기본 구분선 추가
DividerItemDecoration dividerItemDecoration =
new DividerItemDecoration(recyclerView.getContext(),new LinearLayoutManager(this).getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);

// 아이템간 공백 추가
RecyclerDecoration spaceDecoration = new RecyclerDecoration(20);
recyclerView.addItemDecoration(spaceDecoration);

Button btnClear = (Button) findViewById(R.id.btnClear);
btnClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAdapter.clearSelectedItem();
}
});
}

@Override
public void onItemSelected(View v, int position) {
StdRecyclerAdapter.StdViewHolder viewHolder = (StdRecyclerAdapter.StdViewHolder)recyclerView.findViewHolderForAdapterPosition(position);
Toast.makeText(this, viewHolder.textView.getText().toString(), Toast.LENGTH_SHORT).show();
// Toast.makeText(this, position + " clicked", Toast.LENGTH_SHORT).show();
}

@Override
public void onItemLongSelected(View v, int position) {
Toast.makeText(this, position + " long clicked", Toast.LENGTH_SHORT).show();
}

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:context=".MainActivity">

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

<Button
android:id="@+id/btnClear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Clear Selected Item" />

<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
</LinearLayout>

</LinearLayout>

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/item_selector">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_margin="8dp"
android:text="TextView" />
</LinearLayout>

item_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="false">
<color android:color="@color/normal_item_bg" />
</item>
<item android:state_selected="true">
<color android:color="@color/selected_item_bg" />
</item>
</selector>

color.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>

<color name="normal_item_bg">#FFFFFFFF</color>
<color name="selected_item_bg">#FF0000FF</color>

</resources>


  1. 감사합니다 2019.05.25 23:11

    ㅜㅜ정말 감사합니다 이틀동안 골머리썩던걸 해결해주셨어요ㅠㅠㅠㅠ 최고세요

+ Recent posts