treeView 를 사용해야 되는 상황이 있었는데요, 간단해 보이는 view 인데 생각보다 어려움을 겪었습니다. 기본적인 tree 구조에 대한 이해가 필요하고 소스를 참고하여 적용하기 쉽지 않더라구요.. 그래서 따로 기본적인 예제를 남기고 혹시...

[안드로이드] RecyclerView 를 활용한 TreeView 예제

 

 
treeView 를 사용해야 되는 상황이 있었는데요,
간단해 보이는 view 인데 생각보다 어려움을 겪었습니다.
기본적인 tree 구조에 대한 이해가 필요하고 소스를 참고하여 적용하기 쉽지 않더라구요..
그래서 따로 기본적인 예제를 남기고 혹시 또 필요한 상황에서 사용하려고 합니다.
혹시 저와 비슷한 상황이라면 아래 예제를 활용하시면 좋을 것 같습니다.




1. Tree 구조

많은 분들이 이미 알고 계신 내용이지만 간단하게 개념 정리 한번 했던게 이해에 도움이 됐습니다.

Tree View 에 사용되는 모든 Item 을 노드라고 부릅니다. 각각의 노드가 가진 속성에 따라 아래와 같이 분류할 수 있다.

 - Root Node : 부모가 없는 노드 (최상위 단계)

 - Leaf Node : 자식이 없는 노드 (최하위 단계)

 - Parent Node : 자신의 상위 노드

 - Child Node : 자신의 하위 노드




2. TreeView 예제 다운로드

TreeView 예제 다운로드




3. TreeView 예제 주요 클래스

TreeView 를 사용하기 위한 주요 클래스는 아래와 같습니다.

 - TreeNode : TreeView Item 클래스

 - TreeNodeAdapter : view 에 연결시켜주는 adapter 클래스 (with recyclerview)


TreeNode.kt

class TreeNode(private val name: String) {

var parent: TreeNode? = null
var children = ArrayList<TreeNode>()

private var isCheck = false
private var isExpand = false
private var depth = -1

fun getName(): String {
return name
}

fun isRoot(): Boolean {
return parent == null
}

fun isLeaf(): Boolean {
return children.isEmpty()
}

fun isExpand(): Boolean {
return isExpand
}

fun isCheck(): Boolean {
return isCheck
}

fun setExpand(isExpanded: Boolean) {
isExpand = isExpanded
}

fun setCheck(isChecked: Boolean) {
isCheck = isChecked
}

fun getDepth(): Int {
if (isRoot()) depth = 0
else if (depth == -1) depth = parent!!.depth + 1

return depth
}

fun addChild(child: TreeNode): TreeNode {
children.add(child)
child.parent = this

return this
}

override fun toString(): String {
return "Node : Name(${name}), isRoot(${isRoot()}), isLeaf(${isLeaf()}), isExpand(${isExpand()}), isCheck(${isCheck()}), Child size(${children.size})"
}
}


TreeNodeAdapter.kt

class TreeNodeAdapter(private var itemList: ArrayList<TreeNode>) : RecyclerView.Adapter<TreeNodeAdapter.RecyclerViewHolder?>() {
private lateinit var binding: ItemNodeBinding

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
binding = ItemNodeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RecyclerViewHolder(binding)
}

override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
val treeNode = itemList[position]
holder.updateViewHolder(treeNode)
}

override fun getItemCount(): Int {
return itemList.size
}

fun expand(treeNode: TreeNode) {
if (treeNode.isLeaf()) return
if (treeNode.isExpand()) return

val startPosition = itemList.indexOf(treeNode) + 1
val itemCount = addChildNodes(treeNode, startPosition)
notifyItemRangeInserted(startPosition, itemCount)

val position = itemList.indexOf(treeNode)
notifyItemChanged(position)
}

fun expandAll() {
val tempList = ArrayList<TreeNode>()
tempList.addAll(itemList)

for (treeNode in tempList) {
if (treeNode.isLeaf()) continue
if (treeNode.isExpand()) continue

val startPosition = itemList.indexOf(treeNode) + 1
val itemCount = addAllChildNodes(treeNode, startPosition)
notifyItemRangeInserted(startPosition, itemCount)

val position = itemList.indexOf(treeNode)
notifyItemChanged(position)
}
}

fun collapse(treeNode: TreeNode) {
if (treeNode.isLeaf()) return
if (!treeNode.isExpand()) return

val startPosition = itemList.indexOf(treeNode) + 1
val itemCount = removeChildNodes(treeNode, true)
notifyItemRangeRemoved(startPosition, itemCount)

val position = itemList.indexOf(treeNode)
notifyItemChanged(position)
}

fun collapseAll() {
val tempList = ArrayList<TreeNode>()
tempList.addAll(itemList)

for (treeNode in tempList) {
if (treeNode.isLeaf()) continue
if (!treeNode.isExpand()) continue

val startPosition = itemList.indexOf(treeNode) + 1
val itemCount = removeAllChildNodes(treeNode)
notifyItemRangeRemoved(startPosition, itemCount)

val position = itemList.indexOf(treeNode)
notifyItemChanged(position)
}
}

fun setSelect(treeNode: TreeNode, isChecked: Boolean) {

treeNode.setCheck(isChecked)

val startPosition = itemList.indexOf(treeNode) + 1
val itemCount = selectChildNodes(treeNode, isChecked)
notifyItemRangeChanged(startPosition, itemCount)

if (!treeNode.isRoot()) {
selectParentNodes(treeNode)
}
}

fun selectAll() {
for ((index, treeNode) in itemList.withIndex()) {
if (treeNode.isRoot()) {
setSelect(treeNode, true)
notifyItemChanged(index)
}
}
}

fun unselectAll() {
for ((index, treeNode) in itemList.withIndex()) {
if (treeNode.isRoot()) {
setSelect(treeNode, false)
notifyItemChanged(index)
}
}
}

fun getSelectedNodes(): ArrayList<TreeNode> {
val selectedNodes = ArrayList<TreeNode>()

itemList.forEach { treeNode ->
if (treeNode.isRoot()) {
if (treeNode.isCheck()) {
selectedNodes.add(treeNode)
}
selectedNodes.addAll(getSelectChildNodes(treeNode))
}
}

return selectedNodes
}

private fun addChildNodes(parent: TreeNode, startPosition: Int): Int {

var addChildCount = 0
parent.children.forEach { child ->

itemList.add(startPosition + addChildCount, child)
addChildCount++

if (!child.isLeaf() && child.isExpand()) {
addChildCount += addChildNodes(child, startPosition + addChildCount)
}
}

parent.setExpand(true)

return addChildCount
}

private fun addAllChildNodes(parent: TreeNode, startPosition: Int): Int {

var addChildCount = 0
parent.children.forEach { child ->

itemList.add(startPosition + addChildCount, child)
addChildCount++

if (!child.isLeaf()) {
addChildCount += addAllChildNodes(child, startPosition + addChildCount)
}
}

parent.setExpand(true)

return addChildCount
}

private fun removeChildNodes(parent: TreeNode, isChangedExpand: Boolean): Int {

var removeChildCount = 0
parent.children.forEach { child ->

itemList.remove(child)
removeChildCount++

if (!child.isLeaf() && child.isExpand()) {
removeChildCount += removeChildNodes(child, false)
}
}

if (isChangedExpand) parent.setExpand(false)

return removeChildCount
}

private fun removeAllChildNodes(parent: TreeNode): Int {

var removeChildCount = 0
parent.children.forEach { child ->

itemList.remove(child)
removeChildCount++

if (!child.isLeaf() && child.isExpand()) {
removeChildCount += removeAllChildNodes(child)
}
}

parent.setExpand(false)

return removeChildCount
}

private fun selectChildNodes(parent: TreeNode, isChecked: Boolean): Int {

var selectChildCount = 0
parent.children.forEach { child ->

child.setCheck(isChecked)

if (parent.isExpand()) {
selectChildCount++
}

if (!child.isLeaf()) {
selectChildCount += selectChildNodes(child, isChecked)
}
}

return selectChildCount
}

private fun selectParentNodes(treeNode: TreeNode) {

var isUnCheckAll = true
treeNode.parent?.let { parent ->
for (child in parent.children) {
if (child.isCheck()) {
isUnCheckAll = false
break
}
}

if (isUnCheckAll) {
parent.setCheck(false)
} else {
parent.setCheck(true)
}

val position = itemList.indexOf(parent)
notifyItemChanged(position)

selectParentNodes(parent)
}
}

private fun getSelectChildNodes(parent: TreeNode): ArrayList<TreeNode> {

val selectedNodes = ArrayList<TreeNode>()

parent.children.forEach { child ->

if (child.isCheck()) {
selectedNodes.add(child)
}

if (!child.isLeaf()) {
selectedNodes.addAll(getSelectChildNodes(child))
}
}

return selectedNodes
}

inner class RecyclerViewHolder(private val binding: ItemNodeBinding) : RecyclerView.ViewHolder(binding.root) {
fun updateViewHolder(treeNode: TreeNode) {

setPaddingStart(treeNode)

if (treeNode.isExpand()) {
binding.ivExpand.setImageResource(R.drawable.ic_minus)
} else {
binding.ivExpand.setImageResource(R.drawable.ic_plus)
}

if (treeNode.isLeaf()) {
binding.ivExpand.visibility = View.INVISIBLE
} else {
binding.ivExpand.visibility = View.VISIBLE
binding.ivExpand.setOnClickListener {

if (treeNode.isExpand()) {
collapse(treeNode)
} else {
expand(treeNode)
}
}
}

binding.cbName.text = treeNode.getName()
binding.cbName.isChecked = treeNode.isCheck()
binding.cbName.setOnClickListener {

setSelect(treeNode, binding.cbName.isChecked)
}
}

private fun setPaddingStart(node: TreeNode) {
val depth = node.getDepth()
itemView.run {
setPadding(DEPTH_PADDING * depth, paddingTop, paddingRight, paddingBottom)
}
}
}

companion object {
private const val DEPTH_PADDING = 60
}
}




[참고자료]

https://leveloper.tistory.com/203




0 comments: