Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- hilt
- kotlin
- room
- notification
- Algorithm
- recyclerview
- ViewModel
- Behavior
- View
- lifecycle
- CoordinatorLayout
- AppBarLayout
- Navigation
- 코틀린
- 백준
- onLayout
- BOJ
- 안드로이드
- onMeasure
- LiveData
- HTTP
- sqlite
- Android
- 알고리즘
- CustomView
- Coroutine
- activity
- 알림
- DataBinding
- CollapsingToolbarLayout
Archives
- Today
- Total
개발일지
Android in A..Z - RecyclerView (Selection Tracker) 본문
Android (안드로이드)/RecyclerView
Android in A..Z - RecyclerView (Selection Tracker)
강태종 2021. 1. 15. 04:19
Selection Tracker
RecyclerView에서 Item을 선택하는 기능을 제공한다.
ex) 갤러리에서 사진 여러개 선택
Dependency
dependencies {
implementation "androidx.recyclerview:recyclerview:1.1.0"
// For control over item selection of both touch and mouse driven selection
implementation "androidx.recyclerview:recyclerview-selection:1.1.0-rc03"
}
기본구조
RecyclerView에서 Select이 발생하면 Select된 ViewHolder의 Id를 Selection에 기록하고, onBindViewHolder를 호출하여 업데이트하는 구조이다.
=> ViewHolder의 Id를 가져오는 코드가 필요함 -> ItemKeyProvider
=> Select된 ViewHolder의 정보를 가져오는 코드가 필요함 -> ItemDetailsLookup
- SelectionTracker : RecyclerView를 감시하여 Select를 감지하고 Select된 ViewHolder의 Id를 저장한다.
- ItemKeyProvider : Select된 ViewHolder의 Id값을 가져온다.
- ItemDetailsLookup : Select된 ViewHolder의 정보를 가져온다.
- StorageStrategy : Id의 Save, Restore등을 담당한다. => 생명주기의 변화같은 Data의 저장 복원이 필요한 경우
Key (Id) 정하기
Selection 라이브러리는 3가지의 Key Type을 제공한다.
- Long
- String
- Parcelable
Adapter 설정
class SelectionAdapter : BaseAdapter<Selection>(SelectionItemCallback()) {
var tracker: SelectionTracker<Long>? = null
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseHolder<out ViewDataBinding, Selection> {
return SelectionHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.context), viewType, parent, false))
}
override fun getItemId(position: Int): Long {
return getItem(position).id
}
override fun getItemViewType(position: Int): Int {
return R.layout.holder_selection
}
inner class SelectionHolder(binding: HolderSelectionBinding) : BaseHolder<HolderSelectionBinding, Selection>(binding) {
override fun bind(element: Selection) {
super.bind(element)
binding.selection = element
itemView.isActivated = tracker?.isSelected(itemId) ?: false
}
}
class SelectionKeyProvider(private val recyclerView: RecyclerView) : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int): Long {
val holder = recyclerView.findViewHolderForAdapterPosition(position)
return holder?.itemId ?: throw IllegalStateException("No Holder")
}
override fun getPosition(key: Long): Int {
val holder = recyclerView.findViewHolderForItemId(key)
return if (holder is SelectionAdapter.SelectionHolder) {
holder.adapterPosition
} else {
RecyclerView.NO_POSITION
}
}
}
class SelectionDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
val view = recyclerView.findChildViewUnder(e.x, e.y) ?: return null
val holder = recyclerView.getChildViewHolder(view)
return if (holder is SelectionHolder) {
object : ItemDetails<Long>() {
override fun getPosition(): Int {
return holder.adapterPosition
}
override fun getSelectionKey(): Long {
return holder.itemId
}
}
} else {
null
}
}
}
class SelectionPredicate(private val recyclerView: RecyclerView) : SelectionTracker.SelectionPredicate<Long>() {
override fun canSetStateForKey(key: Long, nextState: Boolean): Boolean {
val holder = recyclerView.findViewHolderForItemId(key)
return if (holder is SelectionAdapter.SelectionHolder) {
holder.element.text == "YES"
} else {
false
}
}
override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean {
return true
}
override fun canSelectMultiple(): Boolean {
return true
}
}
class SelectionItemCallback : DiffUtil.ItemCallback<Selection>() {
override fun areItemsTheSame(oldItem: Selection, newItem: Selection): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Selection, newItem: Selection): Boolean {
return oldItem.text == newItem.text
}
}
}
- setHasStableIds(true) : Adapter에 고유한 ID값을 가진다고 설정한다.
- getItemId(position) : 각 Item의 Id값을 설정한다.
ItemKeyProvider
SelectionTracker에서 기본적으로 제공하는 StableIdKeyProvider을 사용하거나 ItemKeyProvider를 상속받아서 구현한다.
class SelectionKeyProvider(private val recyclerView: RecyclerView) : ItemKeyProvider<Long>(SCOPE_MAPPED) {
override fun getKey(position: Int): Long {
val holder = recyclerView.findViewHolderForAdapterPosition(position)
return holder?.itemId ?: throw IllegalStateException("No Holder")
}
override fun getPosition(key: Long): Int {
val holder = recyclerView.findViewHolderForItemId(key)
return if (holder is SelectionAdapter.SelectionHolder) {
holder.adapterPosition
} else {
RecyclerView.NO_POSITION
}
}
}
ItemDetailsLookup
ItemDetailsLookup을 상속받아서 구현한다.
class SelectionDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
val view = recyclerView.findChildViewUnder(e.x, e.y) ?: return null
val holder = recyclerView.getChildViewHolder(view)
return if (holder is SelectionHolder) {
object : ItemDetails<Long>() {
override fun getPosition(): Int {
return holder.adapterPosition
}
override fun getSelectionKey(): Long {
return holder.itemId
}
}
} else {
null
}
}
}
StorageStrategy
StorageStrategy에서 기본적으로 제공하는 함수를 사용한다.
- StorageStrategy.createLongStorage() : Key가 Long인 경우
- StorageStrategy.createStringStorage() : Key가 String인 경우
- StorageStrategy.createParcelableStorage() : Key가 Parcelable인 경우
Selection Tracker
SelectionTracker.Builder로 생성한다.
private val tracker by lazy {
with(binding) {
SelectionTracker.Builder(
"Selection",
recyclerView,
//StableIdKeyProvider()
SelectionAdapter.SelectionKeyProvider(recyclerView),
SelectionAdapter.SelectionDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionAdapter.SelectionPredicate(recyclerView)
).build().apply {
addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
val tracker = this@apply
if (tracker.hasSelection() && menu.findItem(MENU_DELETE) == null) {
menu.add(Menu.NONE, MENU_DELETE, Menu.NONE, "Delete")
.setIcon(R.drawable.ic_delete)
.setOnMenuItemClickListener {
tracker.selection.forEach {
val holder = recyclerView.findViewHolderForItemId(it)
if (holder is SelectionAdapter.SelectionHolder) {
list.remove(holder.element)
}
}
tracker.clearSelection()
adapter.notifyDataSetChanged()
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else if (!tracker.hasSelection() && menu.findItem(MENU_DELETE) != null){
menu.removeItem(MENU_DELETE)
}
}
})
}
}
}
SelectionHolder
inner class SelectionHolder(binding: HolderSelectionBinding) : BaseHolder<HolderSelectionBinding, Selection>(binding) {
override fun bind(element: Selection) {
super.bind(element)
binding.selection = element
itemView.isActivated = tracker?.isSelected(itemId) ?: false
}
}
holder_selection
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="selection"
type="com.taetae98.recyclerview.data.Selection" />
</data>
<LinearLayout
android:background="@drawable/selection_background"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="100dp"
android:text="@{selection.text}"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</layout>
selection_background
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_activated="true"
android:drawable="@android:color/holo_blue_light" />
<item
android:drawable="@color/white"/>
</selector>
SelectionFragment
class SelectionFragment : BaseFragment<FragmentSelectionBinding>(R.layout.fragment_selection) {
lateinit var menu: Menu
init {
setHasOptionsMenu(true)
}
companion object {
const val MENU_DELETE = 0
}
private val adapter by lazy { SelectionAdapter().apply { submitList(list) }}
private val list by lazy {
mutableListOf(
Selection(0, "YES"), Selection(1, "NO"), Selection(2, "YES"),
Selection(3, "NO"), Selection(4, "YES"), Selection(5, "YES"),
Selection(6, "YES"), Selection(7, "YES"), Selection(8, "YES")
) }
private val tracker by lazy {
with(binding) {
SelectionTracker.Builder(
"Selection",
recyclerView,
//StableIdKeyProvider()
SelectionAdapter.SelectionKeyProvider(recyclerView),
SelectionAdapter.SelectionDetailsLookup(recyclerView),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionAdapter.SelectionPredicate(recyclerView)
).build().apply {
addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
val tracker = this@apply
if (tracker.hasSelection() && menu.findItem(MENU_DELETE) == null) {
menu.add(Menu.NONE, MENU_DELETE, Menu.NONE, "Delete")
.setIcon(R.drawable.ic_delete)
.setOnMenuItemClickListener {
tracker.selection.forEach {
val holder = recyclerView.findViewHolderForItemId(it)
if (holder is SelectionAdapter.SelectionHolder) {
list.remove(holder.element)
}
}
tracker.clearSelection()
adapter.notifyDataSetChanged()
true
}
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else if (!tracker.hasSelection() && menu.findItem(MENU_DELETE) != null){
menu.removeItem(MENU_DELETE)
}
}
})
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
this.menu = menu
}
override fun init() {
super.init()
initRecyclerView()
initTracker()
}
private fun initRecyclerView() {
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(3, 10.toDp()))
binding.recyclerView.adapter = adapter
}
private fun initTracker() {
adapter.tracker = tracker
}
}
주의할 점
SelectionTracker에서 Select를 감지한 경우 onBindViewHolder를 호출하기 때문에 onBindViewHolder에서 Select에 대한 업데이트를 처리한다.
SelectionTracker를 구현하기 전에 Adapter가 RecyclerView에 attach되어있어야 한다.
Git (예제코드)
github.com/KangTaeJong98/Example/tree/main/Android/RecyclerView
'Android (안드로이드) > RecyclerView' 카테고리의 다른 글
Android in A..Z - RecyclerView (Selection - Observer) (0) | 2021.01.15 |
---|---|
Android in A..Z - RecyclerView (Selection - Predicate) (0) | 2021.01.15 |
Android in A..Z - RecyclerView (payload) (0) | 2021.01.10 |
Android in A..Z - RecyclerView (setHasFixedSize) (1) | 2021.01.10 |
Android in A..Z - RecyclerView (setHasStableIds) (0) | 2021.01.10 |
Comments