반응형

가장 빨리 만나는 코어 자바9




궁금했던 부분만 훑어 가며 읽어 보니 , 이것 저것 볼만한 내용이 많다.


시간날때 꼭 봐야 겠음.

반응형
반응형

열혈강의 Java Programming

김승현 저

FREELEC


아주 예전에 C++공부할때, "열혈강의 C++"이 꽤 좋았던 기억이 있어 펼쳐 든 책.


Java 관련 기본적인 내용들이 절반, 자바 GUI관련 내용이 절반 정도?


기본적인 내용들은  so so, GUI관련 내용은 skip.

반응형
반응형

RecyclerView #7 - ViewType 동적변경



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

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

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

RecyclerView #4 - 아이템 클릭 처리

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

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

RecyclerView #7 - ViewType 동적변경



아마도 RecyclerView에 대한 마지막 포스트.



#6까지 작성한 샘플을 기반으로 RecyclerView의 레이아웃을 동적으로 변경하는 방법을 알아보겠습니다.


마지막 샘플이니만큼 이번에는 구색을 좀 맞춰보겠습니다.


1. RecyclerView는 도서 목록을 표시합니다.

2. 각각의 아이템은 도서 제목과 저자, 출판사, 그리고 도서의 이미지를 포함합니다.

3. ItemView를 표시하는 방식은  normal과 detail 두가지 방식이 존재합니다

( 이미지 배치와 크기 정도만 다르게 처리하겠습니다.)

4. 구색을 맞추기 위해 자잘한 처리를 보완합니다.

( 아이템 테두리라든가 ... ItemView에 세팅되는 데이터의 class 처리라든가 )



구현 화면을 첨부하는 것이 나머지 내용을 이해하는데 도움이 될듯하여 화면을 첨부하고 시작합니다.


        




레이아웃 구성하기

normal viewType과 detail viewType을 표시하기 위한 레이아웃을 각각 구성합니다.

list_layout_normal.xml

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

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="7"
android:orientation="vertical"
android:padding="10dp">

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="18dp"
android:text="title" />

<TableLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textStyle="bold"
android:text="저자" />

<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="6"
android:text="" />
</TableRow>

<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textStyle="bold"
android:text="출판사" />

<TextView
android:id="@+id/publisher"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="6" />
</TableRow>
</TableLayout>
</LinearLayout>

<ImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:adjustViewBounds="true"
android:padding="5dp"
tools:srcCompat="@drawable/ic_image" />
</LinearLayout>




list_layout_detail.xml

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

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

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="18dp"
android:text="title" />

<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textStyle="bold"
android:text="저자" />

<TextView
android:id="@+id/author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="4"
android:text="" />
</TableRow>

<TableRow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textStyle="bold"
android:text="출판사" />

<TextView
android:id="@+id/publisher"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="4" />
</TableRow>
</TableLayout>
</LinearLayout>

<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:padding="5dp"
tools:srcCompat="@drawable/ic_image" />
</LinearLayout>




ItemView에 바인딩될 데이터 필드를 가지는 클래스를 작성합니다.

Book.java

import android.graphics.Bitmap;

public class Book {
private String title;
private String author;
private String publisher;
private Bitmap bitmap;

public Book(String title, String author, String publisher, Bitmap bitmap) {
this.title = title;
this.author = author;
this.publisher = publisher;
this.bitmap = bitmap;
}

public String getTitle() {
return title;
}

public String getAuthor() {
return author;
}

public String getPublisher() {
return publisher;
}

public Bitmap getBitmap() {
return bitmap;
}
}



하나의 ItemView Class로 두가지 레이아웃 처리하기


기존에 작성했던 ItemView Class는 아래와 같이 ItemView Class 내부에 Inflate할 Layout 리소스가 하드 코딩된 상태입니다.

public class ItemView extends LinearLayout {
TextView textView;
public ItemView(Context context) {
super(context);

LayoutInflater inflate = LayoutInflater.from(context);

View v = inflate.inflate(R.layout.list_item, this, true);

// bind widget
textView = v.findViewById(R.id.textView);

v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
}



이번 예제에서는 사용되는 두 가지 타입의 레이아웃은 위젯의 배치나 크기 외에는 다른 처리가 없으므로

Inflate할 Layout의 리소스를 ItemView의 생성자를 통해 전달하는 형태로 수정하고

동일한 ItemView Class를 통해 두가지 레이아웃을 공통으로 처리하는 방향으로 구현하겠습니다.

대략 아래와 같은 코드로 작성됩니다.

public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

ItemView view = null;
view = new ItemView(mContext, R.layout.list_layout_normal);

StdViewHolder vh = new StdViewHolder(view);

return vh;
}

public class ItemView extends LinearLayout {

public ItemView(Context context, @LayoutRes int resource) {
super(context);

LayoutInflater inflate = LayoutInflater.from(context);

View v = inflate.inflate(resource, this, true);
}

사족을 붙이자면,

ViewHolder에 Hold된(?) 레이아웃을 컨트롤하는 구조는 다음과 같은 방법들을 생각해 볼수 있을 것 같습니다.

1) Adapter에서 Inflate하고 Adapter에서 직접 처리하는 방법 (RecyclerView #1)

2) Layout별로 클래스를 구현하여 각각 처리하는 방법 ( RecyclerView #6 )

3) 클래스는 공통으로 사용하되 Layout만 별도로 받는 방법 ( RecyclerView #7)

4) Layout별 공통 처리를 위한 Base Class를 생성하고, 처리상의 차이가 있는 부분만 상속받아 추가하는 방법

등등.. 


어떤 방법이 '정석'이라고 할수는 없을 듯하고 서로 다른 두가지(혹은 이상의) 레이아웃의 동작을 

공통 코드로 처리할 수 있는지의 여부에 따라 결정하면 될것 같습니다.



이제 ViewType에 따라 레이아웃을 적용하는 코드를 구현해 보겠습니다.


우선 두가지 뷰타입에 대한 상수를 정의하고,

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

public static final int VIEWTYPE_NORMAL = 0;
public static final int VIEWTYPE_DETAIL = 1;
int mItemViewType;

외부에서 뷰타입을 설정할 수 있도록, setItemViewType 메소드를 작성합니다.

public void setItemViewType(int viewType){
mItemViewType = viewType;
}


그리고, 설정된 뷰타입에 따라 해당하는 레이아웃을 inflate한 ItemView를 생성하도록 CreateViewHolder를 변경합니다.

public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

ItemView view = null;

if ( mItemViewType == VIEWTYPE_NORMAL ){
view = new ItemView(mContext, R.layout.list_layout_normal);
Log.d("TEST", "normal view created");
} else {
view = new ItemView(mContext, R.layout.list_layout_detail);
Log.d("TEST", "detail view created");
}
StdViewHolder vh = new StdViewHolder(view);

return vh;
}

마지막으로 버튼을 두개 추가한 후, 각 버튼을 클릭했을때 뷰타입을 변경하도록 코드를 작성했습니다.

Button btnNormal = (Button) findViewById(R.id.btnNormal);
btnNormal.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAdapter.setItemViewType(StdRecyclerAdapter.VIEWTYPE_NORMAL);
}
});
Button btnDetail = (Button) findViewById(R.id.btnDetail);
btnDetail.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAdapter.setItemViewType(StdRecyclerAdapter.VIEWTYPE_DETAIL);
}
});


동작화면 입니다.

NORMAL VIEW Type 으로 초기 항목들이 표시된 상태에서

[DETAIL VIEW] 버튼을 눌러 detail view로 뷰타입 변경후,

아래로 스크롤 한 화면 입니다.




몇가지 문제점

첫째, VIEWTYPE_NORMAL로 생성된 기존의 뷰가 갱신되지 않는다.

둘째, RecyclerView를 스크롤시, 새로 생성되는 ViewHolder는 VIEWTYPE_DETAIL로 생성되지만

VIEWTYPE_NORMAL타입의 기존 ViewHolder가 여전히 재사용 된다.


해결하려면?

1) 기존에 VIEWTYPE_NORMAL로 생성된 ViewHolder를 제거(remove) 하거나 재사용되지 않도록 하고, 

2) 새로 생성되는 ViewHolder를 VIEWTYPE_DETAIL 로 재 생성하도록 해야 한다.

3) ViewType을 설정하는 즉시 화면에 표시된 아이템을 갱신해야 한다.



1)2) 기존에 생성된 ViewHolder를 제거하거나 재사용되지 않도록 처리하기

결론을 먼저 기술하자면, 다시 혹은 신규로 표시될 아이템의 ViewType이 기존의 ViewType과 다름을 알려줌으로써

기존의 ViewHolder를 재사용 하지 않도록 처리하면 됩니다.


뷰타입을 Adapter에게 알려주기

사실 RecyclerView.Adater는 뷰타입을 처리하는 방법을 이미 제공하고 있습니다.

public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)


그렇다면 RecyclerView.Adapter는 표시될 아이템의 뷰타입을 어디에서 얻어 오는 것일까요?

해답은 Adapter의 getItemViewType() 메소드 입니다.

@Override
public int getItemViewType(int position) {
return super.getItemViewType(position);
}

RecyclerView.Adapter의 getItemViewType() 메소드를 오버라이드 하게 되면 위와 같은 템플릿 코드가 작성됩니다.

코드에서 유추할 수 있듯, 각 아이템별 뷰타입을 요청하고(Adapter 입장에서) 리턴받는 형태로 되어 있습니다.

의도치 않은 동작의 예시로 제공한 위의 이미지에서 보았듯이, RecyclerView는 각기 다른 Layout을 가진 아이템을

표시할 수 있고, 이러한 처리를 위해서는 각 아이템별 뷰타입을 관리해야 할 필요성이 있다는 것입니다.


이번 예제에서는 아이템별이 아니라, 전체 아이템을 특정 뷰타입으로 변경하는 것이므로 

더 간단하게 다음과 같이 처리가 가능합니다.

    @Override
public int getItemViewType(int position) {
return mItemViewType;
}

그리고 onCreateViewHolder에서는 파라미터로 넘어온 뷰타입을 비교하도록 수정하면 되겠습니다.

    public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

ItemView view = null;

// if ( mItemViewType == VIEWTYPE_NORMAL ){
if ( viewType == VIEWTYPE_NORMAL ){
view = new ItemView(mContext, R.layout.list_layout_normal);

} else {
view = new ItemView(mContext, R.layout.list_layout_detail);
}
StdViewHolder vh = new StdViewHolder(view);

return vh;
}

여기서 조금 더 정확하게 이해해야 하는 것은 ViewHolder는 뷰타입별로 관리 되며,

뷰타입에 따라 onCreateViewHolder()가 호출될지 onBindViewHolder ()가 달라 질수 있다는 점입니다..

즉, getItemViewType()을 override 하여 기존과 다른 뷰타입을 리턴 함으로써, 

기존에 생성된 NORMAL 타입의 ViewHolder가 재사용 되지 않고

새로운 ViewHolder를 생성할 수 있게 된다는 의미 입니다..

(설명이 장황한 이유는 내가 헤깔렸던 부분이기 때문에 어쩔수 없음....)



3) ViewType을 설정하는 즉시 화면에 표시된 아이템을 갱신.

(별것 아닌데 설명하는 이유는 역시나 내가 방황했던 부분이기 때문이다..)

뷰타입을 변경후, 데이터 셋이 변경되었다고 Adapter에게 통지 해주면 됩니다.

#5 에서도 유사한 트릭으로 배경화면의 색상을 변경한 적이 있습니다.

public void setItemViewType(int viewType){
mItemViewType = viewType;
notifyDataSetChanged();
}


뷰타입 변경시, 아이템의 선택 상태 유지는 기존에 작업했던 코드를 통해 그대로 적용됩니다.

스크롤 위치 복원이라던가 하는 부분들이 추가 코드가 필요하겠습니다.


RecyclerView를 사용하면서 궁금했던 부분이 많았던 터라

하나 하나 찾아가며 습득한 내용들을 간단히 정리하고자 했는데,

어쩌다보니 RecyclerView 관련하여 7개의 포스트를 쓰게 되었네요.

(보는 사람은 없지만....)


생각보다 시간이 많이 들어가는 일입니다만,

포스트를 쓰다보면 "구현에 급급해 미쳐 알지 못했던 부분", 

"일단 동작하니 원리는 나중에 보자"하고 미루어놨던 부분들을

좀 더 찾아 보고 정리하게 되네요.


소스는 아래 링크에

https://github.com/thirteenrains/stdRecyclerView




반응형
반응형

android.view.InflateException: Binary XML file line #14: 

Binary XML file line #14: Error inflating class <unknown>



Case 1 - 관련된 리소스를 찾지 못하는 경우


상황

특정 Layout의 android:Background 속성으로 a_selector.xml을 적용.

실제 단말에서는 문제없이 동작하였으나, Emulator에서는 에러발생하며 종료됨.

android:background="@drawable/item_selector"


원인

a_selector.xml파일이 v24 폴더에 생성되어, Emulator에서 참조하는 폴더에는 해당 파일이 없음

* 예전에 스크롤바에 아래 속성을 설정할때도 동일한 문제가 있었는데 이제 보니 같은 원인

android:scrollbarThumbVertical="@drawable/scroll_bar"

조치

어차피 이 xml은 사이즈나 버전에 영향을 받지 않는 xml이므로

drawable 폴더로 xml 파일 이동

반응형
반응형

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


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

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

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

RecyclerView #4 - 아이템 클릭 처리

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

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

RecyclerView #7 - ViewType 동적변경



RecyclerView #5까지의 샘플을 보면 

Adapter의 onBindViewHolder() 메소드에서 ViewHolder가 Hold하고 있는 위젯에 데이터를 설정하는 형태로 작성되어 있다.

public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflate = LayoutInflater.from(mContext);

View view = inflate.inflate(R.layout.list_item, parent, true);
StdViewHolder vh = new StdViewHolder(view);

return vh;
}


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

}

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

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

}

}


샘플의 경우 아이템을 표시하는 위젯으로 TextView 하나만 추가된 형태이므로 큰 무리는 없겠지만

여러가지 위젯으로 복잡하게 구성된 경우, 처리하는 코드가 지저분하게 코딩될수 밖에 없다.


ViewHolder를 파라미터로 받는 함수를 작성하고 onBindViewHolder에서 호출함으로써 좀 정제화 할수는 있겠지만,

ItemView자체를 클래스화 해서 onBindViewHolder에서는 데이터만 던져주고 

클래스내부에서 나머지 처리를 하는 구조로 작성되면 좋을 것 같다.



대략적인 구현 방향은 다음과 같다.

1. ViewHolder가 Hold할 itemView로 LinearLayout 객체를 던져주고

2. 해당 LinearLayout에 우리가 작성한 Layout을 Inflate해서 붙여주자.


쉽게 말해 다음과 같은 코드로 LinearLayout 객체를 ViewHolder에 던져(?) 주면

RecyclerView는 아이템이 표시될 위치에 우리가 던져준 LinearLayout 을 배치하게 되고

우리는 해당 LinearLayout에 attach된 "Inflate된 View"를 통해 데이터를 갱신 할 수 있다.

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

LayoutInflater inflate = LayoutInflater.from(mContext);
View view = new LinearLayout(mContext);
View v = inflate.inflate(R.layout.list_item, view, true);
StdViewHolder vh = new StdViewHolder(view);

return vh;
}


그럼 위에서 정리된 내용대로 ItemView 클래스를 작성해 보자.


ItemView는 RecyclerView가 요구하는 View Class type이어야 할뿐 아니라, 우리가 inflate한 View를 attach할 수 있어야 하므로

일단은 제일 만만한 LinearLayout을 상속 받아 작성한다.


* Context를 파라미터로 갖는 기본적인 생성자는 구현이 필요하다.

public class ItemView extends LinearLayout {
TextView textView;
public ItemView(Context context) {
super(context);

LayoutInflater inflate = LayoutInflater.from(context);

// inflate itemLayout & attach to this LinearLayout
View v = inflate.inflate(R.layout.list_item, this, true);

// bind widget
textView = v.findViewById(R.id.textView);

v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}

void setContents(String contents) {
textView.setText(contents);
}
}


당연한 이야기 이지만, LinearLayout을 상속받은 이 클래스의 인스턴스와 

R.layout.list_item을 inflate한 View간의 관계가 맺어 지는 것은

inflate.inflate() 의 두번째 인자(root)와 세번째 인자(attachToRoot)에 의해서이다.



Adapter와 ViewHolder는 다음과 같이 변경될 수 있다.

public StdViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemView view = new ItemView(mContext);

StdViewHolder vh = new StdViewHolder(view);

return vh;
}

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

}


public class StdViewHolder extends RecyclerView.ViewHolder {
public ItemView mItemView;

public StdViewHolder(@NonNull View itemView) {
super(itemView);
this.mItemView = (ItemView)itemView;
}
}

이제 ItemView Class의 setContents() 역할을 하는 메소드를 변형해서

Class 자체를 넘겨주고 ItemView에서 내용을 뿌리면 된다.


알고 있겠지만, 이러한 구조를 적용하더라도 잊지 말아야 하는 것은 

이번에 작성한 ItemView Class의 인스턴스 또한 재활용 되는 인스턴스라는 점이다.


반응형
반응형

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

 

 

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

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

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

RecyclerView #4 - 아이템 클릭 처리

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

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

RecyclerView #7 - ViewType 동적변경

 

 

 

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

 

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

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

 

 

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

우선 생각해 볼수 있는 방법은 아이템이 클릭되었을때 onClick() 리스너에서 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는 화면에 표시되는 아이템의 개수에 기반하여 적정수( 한화면에 표시되는 아이템 개수 + 스크롤시 사용할 여분의 아이템 개수)의 View를 미리생성하여 ViewHolder를 통해 관리하며,

스크롤시 화면에서 벗어나는  View를 재사용하여

화면에 새로 나타나는 데이터를 화면에 표시하는 구조입니다.

(이러한 방식으로 전체 Item 갯수보다 훨씬 작은 View만으로 Item 전체를 표시 할 수 있습니다.)

 

클릭할 당시의 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>
 

 

반응형

+ Recent posts