앞의 두 포스팅을 봐주세요
https://gangglion.tistory.com/8
https://gangglion.tistory.com/9
이전에 만들었던 리사이클러뷰에 이어서, 선택한 항목에 대해 선택됨을 나타내 볼 것이다.
사실 이부분은 어렵지 않은 부분이다. 선택한 항목에 대해 색상을 바꿔주던지, 체크표시를 해주던지 하면 되니까
바로 코드를 보자. 나머지는 동일하고 onBindViewHolder 에서 content를 클릭하면 색을 변경하게 하였다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = context.getString(R.string.temp_title).format(itemList[position])
holder.content.text = context.getString(R.string.temp_content).format(itemList[position])
holder.content.setOnClickListener {
holder.content.setBackgroundColor(context.getColor(R.color.line_color))
mListener.onClick(position)
}
}
결과는?
클릭한 항목이 색칠이 되는걸 볼 수 있다. 하지만 두번째 사진을 보면 클릭하지 않은 항목도 색칠되어있는 것을 볼 수 있다.
이는 리사이클러뷰의 특징 때문이다. 우리는 리사이클러뷰는 뷰를 재활용한다는 것을 알고 있다.
위에서 배경색을 바꿔준 뷰가 재활용되어 데이터만 바뀐 채로 다시 등장하여 클릭하지 않은 항목인데도 배경색이 바뀌어있는 것이다.
이를 해결하기 위한 방법은 여러가지 있겠지만, 내가 택한 방법은 리스트마다 클릭 여부를 저장하는 Boolean 값을 지정해주는 것이다.
다음과 같이 해결하였다.
우선 데이터리스트의 타입을 아래 데이터클래스로 변경하였다
data class ExampleData(
var value: Int,
var isChecked: Boolean
)
값을 저장하는 Int 형 변수 value와 클릭 여부를 저장할 수 있는 Boolean 형 변수 isChecked이다.
간단하게 for문을 사용하여 ExampleData 타입의 ArrayList를 100개 생성하고, 클릭여부는 false로 지정하였다.
private lateinit var mBasicItemList: ArrayList<ExampleData>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view)
mContext = this
mRecyclerViewBasic = findViewById(R.id.rc_basic)
// 리사이클러뷰 준비
mRecyclerViewBasic.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
mBasicItemList = ArrayList()
for(i: Int in 1..100){
mBasicItemList.add(ExampleData(i, false))
}
}
그 뒤 onBindViewHolder에서 isChecked가 true면 색칠되게, false면 색칠되지 않게 변경하였다. 또한 클릭하면 isClicked를 true로 지정해준다.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = context.getString(R.string.temp_title).format(itemList[position].value)
holder.content.text = context.getString(R.string.temp_content).format(itemList[position].value)
if(itemList[position].isChecked){
holder.content.setBackgroundColor(context.getColor(R.color.line_color))
} else{
holder.content.background = null
}
holder.content.setOnClickListener {
holder.content.setBackgroundColor(context.getColor(R.color.line_color))
itemList[position].isChecked = true
mListener.onClick(position)
}
}
한 단계 더 나아가서, 중복 선택을 방지하려면 어떻게 해야할까?
(부끄러운 이야기지만, 실제 이부분에서 꽤나 애먹었다. 얼마나 기초가 부족했는지, 구글링 코드 복붙에 의존해왔는지 알 수 있었던 부분)
마찬가지로 아래 나오는 내용이 무조건적인 정답은 아니며, 더 좋은 방법이 있을 수도 있다.
두 가지 방법으로 구현하였다.
첫번째 방법이다.
ViewHolder에서 setColor라는 함수를 만든다. 클릭된 상태면 true, 아니면 false를 받아 처리해주게끔 한다.
또한 클릭된 포지션을 저장하기 위해 clickPosition 변수를 생성해 초기값을 -1로 지정한다.
onCreateViewHolder 에서 ViewHolder 객체를 생성하여 모양을 잡아주고, onBindViewHolder가 호출될 때 clickPosition의 값과 현재 position을 비교하여 같으면(현재 그려야할 아이템의 포지션이 클릭한 포지션이라면) setColor(true)를, 다르면 setColor(false)를 호출해준다.
그리고 onBindViewHolder 의 content 클릭 이벤트에서 clickPosition 값에 클릭한 position을 저장하고 notifyDataSetChanged를 사용하여 RecyclerView를 갱신시켜준다.
(notifyDataSetChanged는 데이터와 레이아웃 구조가 변경됨을 알리고, onCreateViewHolder 부터 다시 실행한다. 아래에서 자세히 다룬다.)
코드는 다음과 같다.
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val title: AppCompatTextView = itemView.findViewById(R.id.tv_title)
val content: AppCompatTextView = itemView.findViewById(R.id.tv_content)
fun setColor(isClick: Boolean){
if(isClick){
itemList[adapterPosition].isChecked = true
content.setBackgroundColor(context.getColor(R.color.line_color))
} else{
itemList[adapterPosition].isChecked = false
content.background = null
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = context.getString(R.string.temp_title).format(itemList[position].value)
holder.content.text = context.getString(R.string.temp_content).format(itemList[position].value)
if(clickPosition == position){
holder.setColor(true)
} else{
holder.setColor(false)
}
holder.content.setOnClickListener {
clickPosition = holder.adapterPosition
notifyDataSetChanged()
// mListener.onClick(position)
}
}
사실 이방법으로 하면 ArrayList의 타입을 데이터 클래스로 해 줄 필요가 없다.
배경색을 바꿔준 뷰가 재활용되어 데이터만 바뀐 채로 다시 등장하여 클릭하지 않은 항목인데도 배경색이 바뀌어있는 문제를 해결하기 위해 데이터 클래스로 변경하였지만, 여기서는 notifyDataSetChanged를 사용하여 View를 다시 갱신시기도 하고, clickPosition으로 비교하여 아니라면 배경색을 없애주기 때문이다.
코드 전체
class BasicRecyclerViewAdapter(private val context: Context, private val itemList: ArrayList<ExampleData>, private val mListener: OnItemClick) : RecyclerView.Adapter<BasicRecyclerViewAdapter.ViewHolder>(){
private var clickPosition = -1
interface OnItemClick{
fun onClick(pos: Int)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val title: AppCompatTextView = itemView.findViewById(R.id.tv_title)
val content: AppCompatTextView = itemView.findViewById(R.id.tv_content)
fun setColor(isClick: Boolean){
if(isClick){
itemList[adapterPosition].isChecked = true
content.setBackgroundColor(context.getColor(R.color.line_color))
} else{
itemList[adapterPosition].isChecked = false
content.background = null
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d("shhan", "onCreateViewHolder")
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_temp_recycler, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return itemList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = context.getString(R.string.temp_title).format(itemList[position].value)
holder.content.text = context.getString(R.string.temp_content).format(itemList[position].value)
if(clickPosition == position){
holder.setColor(true)
} else{
holder.setColor(false)
}
holder.content.setOnClickListener {
clickPosition = holder.adapterPosition
notifyDataSetChanged()
// mListener.onClick(position)
}
}
}
두번째 방법이다.
ViewHolder의 setColor 함수에 pos 값을 받는다.
itemList의 pos 위치의 isClicked가 true라면 색을 바꿔주고, 아니라면 배경색을 없애준다.
그리고 클릭한 아이템의 포지션을 기억할 oldPosition 변수를 하나 생성하고 초기값을 -1로 지정한다(포지션이 0부터 시작하기 때문)
클릭 이벤트가 일어날 때, onBindViewHolder에서 oldPosition과 클릭이 일어난 position 을 비교해서, 다르면 다른 항목을 클릭했다는 뜻이 되므로 oldPosition에 해당하는 isClicked를 false로, position에 해당하는 isClicked를 true로 준다.
onBindViewHolder에서는 이미 생성된 ViewHolder에 데이터를 넣어주는 역할을 하므로, 색상을 바꾸는 레이아웃 코드는 ViewHolder에서 처리할 수 있도록 ViewHolder 에서 함수를 생성하여 onBindViewHolder에서 호출해준다.
그 뒤 notifyDataSetChanged() 를 사용하여 리스트를 갱신해준다.
코드는 다음과 같다.
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val title: AppCompatTextView = itemView.findViewById(R.id.tv_title)
val content: AppCompatTextView = itemView.findViewById(R.id.tv_content)
fun setColor(pos: Int){
if(itemList[pos].isChecked){
content.setBackgroundColor(context.getColor(R.color.line_color))
} else{
content.background = null
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = context.getString(R.string.temp_title).format(itemList[position].value)
holder.content.text = context.getString(R.string.temp_content).format(itemList[position].value)
holder.setColor(position)
holder.content.setOnClickListener {
itemList[position].isChecked = true
if(oldPosition != -1){ // oldPosition 초기값 -1일때 에러 방지
if(position != oldPosition){ // 다른거 클릭했을때
itemList[oldPosition].isChecked = false // 이전 위치의 클릭 해제
}
}
oldPosition = holder.adapterPosition // 이전 클릭위치에 현재 클릭 위치 넣어줌
notifyDataSetChanged() // 변경을 알리고 RecyclerView 를 다시 그림
}
}
코드 전체
class BasicRecyclerViewAdapter(private val context: Context, private val itemList: ArrayList<ExampleData>, private val mListener: OnItemClick) : RecyclerView.Adapter<BasicRecyclerViewAdapter.ViewHolder>(){
private var oldPosition = -1
interface OnItemClick{
fun onClick(pos: Int)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val title: AppCompatTextView = itemView.findViewById(R.id.tv_title)
val content: AppCompatTextView = itemView.findViewById(R.id.tv_content)
fun setColor(pos: Int){
if(itemList[pos].isChecked){
content.setBackgroundColor(context.getColor(R.color.line_color))
} else{
content.background = null
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Log.d("shhan", "onCreateViewHolder")
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_temp_recycler, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return itemList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = context.getString(R.string.temp_title).format(itemList[position].value)
holder.content.text = context.getString(R.string.temp_content).format(itemList[position].value)
holder.setColor(position)
holder.content.setOnClickListener {
itemList[position].isChecked = true
if(oldPosition != -1){ // oldPosition 초기값 -1일때 에러 방지
if(position != oldPosition){ // 다른거 클릭했을때
itemList[oldPosition].isChecked = false // 이전 위치의 클릭 해제
}
}
oldPosition = holder.adapterPosition // 이전 클릭위치에 현재 클릭 위치 넣어줌
notifyDataSetChanged() // 변경을 알리고 RecyclerView 를 다시 그림
}
}
}
결과를 확인해 보면, 다른 항목을 클릭하면 이전에 클릭했던 항목의 배경색은 사라지고 지금 클릭한 항목의 배경색이 칠해지는것을 볼 수가 있다.
그런데 안드로이드 스튜디오를 사용한다는 가정 하에, notifyDataSetChanged() 부분이 이렇게 되어있지 않은가?
마우스를 올려보면, 이렇게 나온다.
It will always be more efficient to use more specific change events if you can. Rely on notifyDataSetChanged as a last resort
해석
가능하다면 항상 보다 구체적인 변경 이벤트를 사용하는 것이 더 효율적일 것입니다. 마지막 수단으로 NotifyDataSetChanged 를 사용합니다
이걸 이해하려면 notifyDataSetChanged() 에 대해 자세히 알아볼 필요가 있다.
notifyDataSetChanged() 는 리사이클러뷰 리스트를 업데이트하는 5가지 방법중에 한가지이다.
리스트의 크기와 아이템이 모두 변경되는 경우에 새로 그리라고 알려주는 메소드인데, 사실상 우리가 변경하고 싶은건
이전에 클릭한 아이템, 지금 클릭한 아이템 이 두가지인데 굳이 새로 그려야 할 필요가 있을까?
리스트를 업데이트하는 5가지 방법은 다음과 같다.
1. notifyDataSetChanged() : 전체 변경. 리스트의 처음부터 onCreateViewHolder, onBindViewHolder 다시 실행됨
2-1. notifyItemChanged(position: Int) : 해당 아이템만 변경. 해당 포지션의 onCreateViewHolder, onBindViewHolder만 실행됨
2-2. notifyItemRangedChaged(positionStart: Int, itemCount: Int) : 변경된 아이템이 연속된 여러개의 아이템일때 해당 부분만 다시 그림
3-1. notifyItemInserted(position: Int) : 특정 position에 아이템이 새로 삽입되었을때 사용함. 새로 삽입된 부분만 다시 그림
3-2. notifyItemRangedInserted(positionStart: Int, itemCount: Int) : 연속된 여러개의 아이템이 삽입되었을때
4-1. notifyItemRemoved(position: Int) : notifyItemInserted와 동일. 특정 아이템 1개를 삭제할때
4-2. notifyItemRangedRemoved(position: Int, itemCount: Int) : notifyRangedItemInserted와 동일. 연속된 여러개의 아이템이 삭제될때
5. notifyItemMoved(fromPosition: Int, toPosition: Int) : 아이템의 순서가 변경되었을 때
자세한 설명은 https://todaycode.tistory.com/55 여기
우린 2개의 아이템만 새로 그리면 되니까 notifyItemChanged(oldPosition), notifyItemChanged(position) 이렇게 두개만 호출해도 되지 않을까??
첫번째 방법, 두번째 방법 모두 notifyItemChanged() 를 사용하여도 문제없이 작동하는걸 확인 할 수 있다.
되도록이면 notifyDataSetChanged 보단 상황에 맞게 리스트를 업데이트 해주자.