treeView 를 사용해야 되는 상황이 있었는데요,
간단해 보이는 view 인데 생각보다 어려움을 겪었습니다.
기본적인 tree 구조에 대한 이해가 필요하고 소스를 참고하여 적용하기 쉽지 않더라구요..
그래서 따로 기본적인 예제를 남기고 혹시 또 필요한 상황에서 사용하려고 합니다.
혹시 저와 비슷한 상황이라면 아래 예제를 활용하시면 좋을 것 같습니다.
1. Tree 구조
많은 분들이 이미 알고 계신 내용이지만 간단하게 개념 정리 한번 했던게 이해에 도움이 됐습니다.
Tree View 에 사용되는 모든 Item 을 노드라고 부릅니다. 각각의 노드가 가진 속성에 따라 아래와 같이 분류할 수 있다.
- Root Node : 부모가 없는 노드 (최상위 단계)
- Leaf Node : 자식이 없는 노드 (최하위 단계)
- Parent Node : 자신의 상위 노드
- Child Node : 자신의 하위 노드
2. 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: