์๋ ํ์ธ์
๋๋ฌด ์ค๋๋ง์ ํฌ์คํ ์ ์ฌ๋ฆฌ๋ค์!!
ํ๋์ ํ์ฌ์์ ๋ฆฌํฉํ ๋ง ๋๋ฌธ์ ๋๋ฌด ์ผ์ด ๋ฐ๋นด๋ค์..
์ด์ ๋ค์ ๊พธ์คํ ํฌ์คํ ์ฌ๋ ค๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
์ค๋์
.
.
์ ๊ฐ ์งํํ๊ณ ์๋ ํ๋ก์ ํธ ํน์ฑ์ ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ผ ํ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ์!
๊ฒฐ๊ตญ, ํด๋น ๋ฆฌ์คํธ๋ค์ ๋์ผํ ๋์์ ์ถ๊ฐํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์๊ฒผ๊ณ ,
๋งค๋ฒ ๋์๋ค์ ์ถ๊ฐํ๋ค ๋ณด๋ ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ์ฝ๋๊ฐ ๋๋ฌด ๋ง์ด ๋ฐ์ํ์ฌ ๊ฐ Adapter์ BaseClass๋ฅผ ๋ง๋ค์๋๋ฐ
๊ด๋ จํด์ ๊ณต์ ํด๋ณด๋ ์๊ฐ์ ๊ฐ์ง๋ ค๊ณ ํฉ๋๋ค!

1. BaseViewHolder ๋ง๋ค๊ธฐ
abstract class BaseViewHolder<ITEM : Any, VB : ViewDataBinding>(binding: VB) :
RecyclerView.ViewHolder(binding.root) {
abstract val binding: VB
abstract fun bind(data: ITEM)
}
๋จผ์ , ViewDataBinding๊ณผ Item์ ๋ฐ์ ์ ์๋ ์ ๋ค๋ฆญ์ ์ ํด์ค ํ ViewHolder๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ Bind๋ฅผ ์ํํ๋ ์ถ์ ํจ์๋ฅผ ํ๋ ๋ง๋ค์ด์ค๋๋ค.
2. BaseAdapter ๋ง๋ค๊ธฐ
abstract class BaseAdapter<VB : ViewDataBinding, ITEM : Any>(val layoutId: Int)
: RecyclerView.Adapter<BaseViewHolder<ITEM, VB>>() {
private var itemList = emptyList<ITEM>()
abstract fun createViewHolder(parent: ViewGroup): VB
override fun onBindViewHolder(holder: BaseViewHolder<ITEM, VB>, position: Int) {
holder.bind(itemList[position])
}
override fun getItemCount() = itemList.size
fun setItemList(list: List<ITEM>) {
itemList = list
notifyItemRangeChanged(0 , itemCount -1)
}
}
๋ฑํ ์ด๋ ค์ด ๋ถ๋ถ์ ์๋๋ฐ์, ๊ธฐ๋ณธ์ ์ธ ItemList์ BindViewHolder๋ฅผ ๋ฏธ๋ฆฌ ์ ์ํ์ฌ ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ ์ค์์ต๋๋ค.
createViewHolder() ํจ์๋ฅผ ํตํด ์ ๋ค๋ฆญ์ผ๋ก ์ง์ ํ ViewDataBinding์ ๋ฐํํ๋๋ก ๊ตฌํํ์ฌ DataBinding์ ์์ฑํ ๋ ํ์ ์ ์ง์ ํด์ฃผ์ด์ผ ํ๋ ๋ฒ๊ฑฐ๋ก์์ ์ค์์ต๋๋ค.
3. Adapter์ ์ ์ฉํ๊ธฐ
class VideoListAdapter : BaseAdapter<ViewHolderVideoBinding, Video>(R.layout.view_holder_video) {
override fun createViewHolder(parent: ViewGroup): ViewHolderVideoBinding {
return DataBindingUtil.inflate(LayoutInflater.from(parent.context), layoutId, parent, false)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): BaseViewHolder<Video, ViewHolderVideoBinding> {
val binding = createViewHolder(parent)
return VideoViewHolder(binding)
}
}
class VideoViewHolder(
override val binding: ViewHolderVideoBinding,
) : BaseViewHolder<Video, ViewHolderVideoBinding>(binding) {
override fun bind(data: Video) {
with(binding) {
// binding ์ฒ๋ฆฌ
}
}
}
BaseAdapter๋ ์์์ ๋ณด์ด๋ ๊ฒ๊ณผ ๊ฐ์ด ๊ตฌํํด์ฃผ๋ฉด ๋๋๋ฐ์ ์ด๋ ๊ฒ ๋ณด๋ฉด ๊ตณ์ด ์ ์ด๋ ๊ฒ ๋ง๋ค์ด์ผ ํด?๋ผ๊ณ ์๊ฐ์ด ๋ค ์๋ ์๋๋ฐ์
์ด๊ฒ์ ๊ฐ๋ฐ์๋ค์ ์ทจํฅ์ฐจ์ด๊ฒ ์ง๋ง, ์ ๊ฐ์ ๊ฒฝ์ฐ๋ ์๋์ ๊ฐ์ ์ด์ ๋ก BaseAdapter๋ฅผ ๋ง๋ค์์ต๋๋ค.
- ์์์ ์ธ๊ธํ๋ฏ์ด ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ ์ค์ฌ์ ๊ฐ๋ฐ์ด ํจ์ฌ ํธ๋ฆฌํด์ง๋ค.
- ๋ฐ๋ณต๋๋ ์ฝ๋๊ฐ ์ค๋ฉด์ BaseAdapter๋ฅผ ๊ตฌํํ ๊ตฌํ์ฒด๋ค์ ์ฝ๋๋์ด ์ค์ด ์ฝ๋๋ฅผ ๋ณด๊ธฐ ํธํด์ง๊ณ ์ ์ง๋ณด์๊ฐ ํธ๋ฆฌํด์ง๋ค.
- ๋ง์ง๋ง์ผ๋ก, ์ ๊ฐ์ ๊ฒฝ์ฐ๋ ๋ชจ๋ Adapter์ ๊ณตํต ๋์(Click , Key ๋ฑ๋ฑ)์ ๋ํด์ ๋ฏธ๋ฆฌ ์ ์ํด๋๊ณ ์ฌ์ฉํ๊ธฐ ์ํด BaseAdapter๋ฅผ ์ ์ํด ๋์์ต๋๋ค. (์๋๋ ์ ๊ฐ ํ๋ก์ ํธ์ ์ ์ฉํ ์์์ ๋๋ค.)
abstract class BaseAdapter<VB : ViewDataBinding, ITEM : Any>(
private val tag: String,
val layoutId: Int,
private val recyclerView: RecyclerView,
) : RecyclerView.Adapter<BaseViewHolder<ITEM, VB>>() {
private var itemList = emptyList<ITEM>()
private var focusedPosition = 0
private var itemKeyListener: OnItemKeyListener? = null
abstract fun createViewHolder(parent: ViewGroup): VB
override fun onBindViewHolder(holder: BaseViewHolder<ITEM, VB>, position: Int) {
holder.bind(itemList[position])
if (position == focusedPosition) {
holder.itemView.requestFocus()
}
}
override fun getItemCount() = itemList.size
fun setOnItemKeyListener(listener: OnItemKeyListener) {
itemKeyListener = listener
}
fun setItemList(list: List<ITEM>) {
itemList = list
notifyItemRangeChanged(0 , itemCount -1)
}
fun requestFocusItem() {
notifyItemChanged(focusedPosition)
val layoutManager =
recyclerView.layoutManager as WrapLinearLayoutManager
notifyItemChanged(focusedPosition)
layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
}
fun setOnKeyListener(binding: VB) {
binding.root.setOnKeyListener keyListener@{ _, _, keyEvent ->
itemKeyListener?.onKeyClick(
keyEvent.keyCode,
itemList[focusedPosition],
focusedPosition,
)
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (focusedPosition > 0) {
focusedPosition--
val layoutManager =
recyclerView.layoutManager as WrapLinearLayoutManager
notifyItemChanged(focusedPosition)
layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
}
Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
return@keyListener true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (focusedPosition < itemCount - 1) {
focusedPosition++
val layoutManager =
recyclerView.layoutManager as WrapLinearLayoutManager
notifyItemChanged(focusedPosition)
layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
}
Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
return@keyListener true
}
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_CENTER,
-> {
return@keyListener true
}
}
false
}
}
}
์ ์ฝ๋๋ฅผ ์ฒ์ฒํ ๋ถ์ํด ๋ณด๊ฒ ์ต๋๋ค!
์ฐ์ , ํด๋น ๋์์ ์ผ๋ฐ์ ์ธ Android ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ๋ฅผ ๊ณ ๋ คํ ์ฝ๋๊ฐ ์๋๊ณ Android TV ๋์์ ๋ํ ๋ถ๋ถ์ด๋๊ฑธ ์ฐธ๊ณ ๋ถํ๋๋ฆฝ๋๋ค!
setOnItemKeyListener()
RecyclerView์์ Key๊ฐ ๊ฐ์ง๋์์ ๊ฒฝ์ฐ ํธ์ถํ Listener๋ฅผ ์ค์ ํ๋ ํจ์์ ๋๋ค.
fun setOnItemKeyListener(listener: OnItemKeyListener) {
itemKeyListener = listener
}
interface OnItemKeyListener {
fun onKeyClick(keyCode: Int, data: Any, index: Int)
}
โป ์ฌ์ฉ์์
videoAdapter = VideoListAdapter(rvVideo).apply {
setOnItemKeyListener(object : OnItemKeyListener {
override fun onKeyClick(keyCode: Int, data: Any, index: Int) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_DOWN -> {
chattingAdapter.requestFocusItem() // ์ฑํ
์ผ๋ก ํฌ์ปค์ค ์ด๋
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
if (data is Video) {
mainViewModel.startNewVideo(data) // ๋น๋์ค ์ฌ์
}
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (index == 0) {
mainViewModel.showNavigation() // ๋ค๋น๊ฒ์ด์
View ํ์
}
}
}
}
})
}
requestFocusItem()
์ด์ ์ ์ ์ฅ๋ focusedPosition์ผ๋ก Focus๋ฅผ ์ด๋์ํค๋ ํจ์
fun requestFocusItem() {
notifyItemChanged(focusedPosition)
val layoutManager =
recyclerView.layoutManager as WrapLinearLayoutManager
notifyItemChanged(focusedPosition)
layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
}
setOnKeyListener()
๋ชจ๋ Adapter์ ๊ณตํต์ ์ผ๋ก ์ ์๋ KeyListener๋ฅผ ๊ตฌํํ ํจ์
(BaseAdapter๋ฅผ ์์๋ฐ์ ์ฌ์ฉํ ๋ ํด๋น ์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํ๊ธฐ๋ฅผ ์์น ์๋๋ค๋ฉด onCreateViewHolder์์ ํจ์๋ฅผ ํธ์ถํ์ง ์์ผ๋ฉด ๋จ)
fun setOnKeyListener(binding: VB) {
binding.root.setOnKeyListener keyListener@{ _, _, keyEvent ->
// ๋ชจ๋ Key๋ ์ฐ์ Listener๋ก ์ ๋ฌ๋จ
// ๊ฐ parent์์ ์ฒ๋ฆฌํ ํค๋ง ์ ์ํ์ฌ ์ฌ์ฉ
itemKeyListener?.onKeyClick(
keyEvent.keyCode,
itemList[focusedPosition],
focusedPosition,
)
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> { // ๋ฆฌ์คํธ ์ผ์ชฝ์ผ๋ก ํฌ์ปค์ค ์ด๋
if (focusedPosition > 0) {
focusedPosition--
val layoutManager =
recyclerView.layoutManager as WrapLinearLayoutManager
notifyItemChanged(focusedPosition)
layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
}
Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
return@keyListener true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> { // ๋ฆฌ์คํธ ์ค๋ฅธ์ชฝ์ผ๋ก ํฌ์ปค์ค ์ด๋
if (focusedPosition < itemCount - 1) {
focusedPosition++
val layoutManager =
recyclerView.layoutManager as WrapLinearLayoutManager
notifyItemChanged(focusedPosition)
layoutManager.scrollToPositionWithOffset(focusedPosition, 0)
}
Log.d(tag, "onCreateViewHolder: focusedPosition = $focusedPosition")
return@keyListener true
}
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_CENTER,
-> { // UP , DOWN , CENTER๋ Parent์์ ์ฌ์ ์
return@keyListener true
}
}
false
}
}
์ด๋ ๊ฒ BaseAdapter๋ฅผ ๊ตฌํํ ๋ด์ฉ์ ๋ํด์ ํฌ์คํ ํด๋ดค๋๋ฐ์!
๋ค์ ๋ง์๋๋ฆฌ์ง๋ง ์ด๊ฑด ๊ฐ๋ฐ์์ ์ทจํฅ์ด๊ธฐ ๋๋ฌธ์ ์ ๋ต์ ์๋ค๋ ์ ์์์ฃผ์ธ์ ๐
์ด ํฌ์คํ ์ด ์ฌ๋ฌ๋ถ์๊ฒ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค!
์ค๋๋ ์ฆ์ฝํ์ธ์ :)