diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb4bf323b..563d7dcb9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -69,6 +69,7 @@ android:name=".activity.dragswipe.DefaultDragAndSwipeActivity" android:exported="false" /> + \ No newline at end of file diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/home/HomeActivity.kt b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/home/HomeActivity.kt index d83f6531d..165c41efe 100644 --- a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/home/HomeActivity.kt +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/home/HomeActivity.kt @@ -17,6 +17,7 @@ import com.chad.baserecyclerviewadapterhelper.activity.itemclick.ItemClickActivi import com.chad.baserecyclerviewadapterhelper.activity.loadmore.AutoLoadMoreRefreshUseActivity import com.chad.baserecyclerviewadapterhelper.activity.loadmore.NoAutoAutoLoadMoreRefreshUseActivity import com.chad.baserecyclerviewadapterhelper.activity.scene.GroupDemoActivity +import com.chad.baserecyclerviewadapterhelper.activity.treenode.TreeNodeActivity import com.chad.baserecyclerviewadapterhelper.activity.upfetch.UpFetchUseActivity import com.chad.baserecyclerviewadapterhelper.databinding.ActivityHomeBinding import com.chad.baserecyclerviewadapterhelper.entity.HomeEntity @@ -80,5 +81,6 @@ class HomeActivity : AppCompatActivity() { HomeEntity(sectionTitle = "场景演示"), HomeEntity("Group(ConcatAdapter)", GroupDemoActivity::class.java, R.mipmap.gv_animation), + HomeEntity("Tree Node", TreeNodeActivity::class.java, R.mipmap.gv_expandable), ) } \ No newline at end of file diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/treenode/TreeNodeActivity.kt b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/treenode/TreeNodeActivity.kt new file mode 100644 index 000000000..de13196ec --- /dev/null +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/treenode/TreeNodeActivity.kt @@ -0,0 +1,184 @@ +package com.chad.baserecyclerviewadapterhelper.activity.treenode + +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import com.chad.baserecyclerviewadapterhelper.activity.home.adapter.HomeTopHeaderAdapter +import com.chad.baserecyclerviewadapterhelper.activity.treenode.adapter.TreeNodeAdapter +import com.chad.baserecyclerviewadapterhelper.base.BaseViewBindingActivity +import com.chad.baserecyclerviewadapterhelper.databinding.ActivityUniversalRecyclerBinding +import com.chad.baserecyclerviewadapterhelper.entity.MyNodeEntity +import com.chad.baserecyclerviewadapterhelper.entity.MyNodeLoadingEntity +import com.chad.library.adapter.base.BaseNode +import com.chad.library.adapter.base.QuickAdapterHelper +import com.chad.library.adapter.base.dragswipe.QuickDragAndSwipe + +/** + * 树形结构演示Adapter + * + * @author Dboy233 + */ +class TreeNodeActivity : BaseViewBindingActivity() { + +// val TAG by lazy { localClassName } + + private lateinit var helper: QuickAdapterHelper + + val mAdapter = TreeNodeAdapter() + + val loadingNode = MyNodeLoadingEntity() + + private val quickDragAndSwipe = QuickDragAndSwipe() + .setSwipeMoveFlags(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) + + override fun initBinding(): ActivityUniversalRecyclerBinding { + return ActivityUniversalRecyclerBinding.inflate( + layoutInflater + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewBinding.titleBar.title = "Tree Node" + viewBinding.titleBar.setOnBackListener { v: View? -> finish() } + + helper = QuickAdapterHelper.Builder(mAdapter) + .build() + .addBeforeAdapter(HomeTopHeaderAdapter()) + + viewBinding.rv.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + + viewBinding.rv.adapter = helper.adapter + + mAdapter.setOnItemClickListener { adapter, view, position -> + val item = mAdapter.getItem(position) + if (item?.childNodes != null && item.childNodes!!.isEmpty()) { + addLoadingNode(item) + } + //一层层展开 + mAdapter.switchState(position, 1) + //全部子节点展开 + //mAdapter.switchState(position) + } + + mAdapter.setOnItemLongClickListener { adapter, view, position -> + val item = mAdapter.getItem(position) ?: return@setOnItemLongClickListener false + mAdapter.addNode(item.parentNode!!, getChildNode(item), getChildNodeIndex(item), 1) + true + } + + quickDragAndSwipe.attachToRecyclerView(viewBinding.rv) + .setDataCallback(mAdapter) + + mAdapter.addNodes(getList(), 0) + + Toast.makeText(this, "长按添加Item,Long press to add Item", Toast.LENGTH_SHORT).show() + } + + /** + * 添加更多节点 + */ + private fun addLoadingNode(parentNode: BaseNode) { + ///添加loading节点 + mAdapter.addNode(parentNode, loadingNode) + ///模拟异步操作 + viewBinding.root.postDelayed({ + ///移除Loading节点 + mAdapter.remove(loadingNode) + ///添加新的节点 + mAdapter.addNodes(parentNode,moreNode()) + }, 2000) + } + + ///获取更多节点 + private fun moreNode(): List { + return mutableListOf( + MyNodeEntity("new node"), + MyNodeEntity("load more node", mutableListOf()), + MyNodeEntity("new node") + ) + } + + + private fun getChildNodeIndex(item: BaseNode): Int { + return mAdapter.getChildNodeIndex(item) + 1 + } + + private fun getChildNode(item: BaseNode): BaseNode { + return when (item.nodeType) { + TreeNodeAdapter.TYPE_FILE -> { + MyNodeEntity( + "new file node" + ) + } + + else -> { + MyNodeEntity( + "new folder node", + mutableListOf( + MyNodeEntity( + "new new folder", mutableListOf( + MyNodeEntity( + "new file node" + ) + ) + ) + ) + ) + } + } + } + + /** + * 所有节点用同一个实体类 + */ + private fun getList(): List { + return listOf( + MyNodeEntity( + "My Folders", mutableListOf( + MyNodeEntity( + "My Pictures folder", mutableListOf( + MyNodeEntity("Belle.png"), + MyNodeEntity("Handsome_guy.png"), + MyNodeEntity( + "Private picture folders", mutableListOf( + MyNodeEntity("凤姐.png") + ) + ) + ) + ), + MyNodeEntity( + "Work Folders", mutableListOf( + MyNodeEntity("life_summary.txt") + ) + ) + ) + ), + MyNodeEntity( + "Her folder", mutableListOf( + MyNodeEntity( + "Her picture folder", mutableListOf( + MyNodeEntity( + "Private picture folders", mutableListOf( + MyNodeEntity("Swimsuit.png") + ) + ), + MyNodeEntity("Mountaintop.png"), + MyNodeEntity("Park.png"), + ) + ), + MyNodeEntity( + "Her working folder", mutableListOf( + MyNodeEntity("how_to_work_easily.txt") + ) + ) + ) + ), + MyNodeEntity("load more node", mutableListOf()) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/treenode/adapter/TreeNodeAdapter.java b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/treenode/adapter/TreeNodeAdapter.java new file mode 100644 index 000000000..6dacd51e1 --- /dev/null +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/activity/treenode/adapter/TreeNodeAdapter.java @@ -0,0 +1,97 @@ +package com.chad.baserecyclerviewadapterhelper.activity.treenode.adapter; + +import android.content.Context; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.chad.baserecyclerviewadapterhelper.R; +import com.chad.baserecyclerviewadapterhelper.entity.MyNodeEntity; +import com.chad.library.adapter.base.BaseNode; +import com.chad.library.adapter.base.BaseTreeNodeAdapter; +import com.chad.library.adapter.base.dragswipe.listener.DragAndSwipeDataCallback; +import com.chad.library.adapter.base.viewholder.QuickViewHolder; + +/** + * @author Dboy233 + */ +public class TreeNodeAdapter extends BaseTreeNodeAdapter implements DragAndSwipeDataCallback { + + public final static int TYPE_FOLDER = 1; + + public final static int TYPE_FILE = 2; + + public final static int TYPE_LOAD_MORE = 3; + + + public TreeNodeAdapter() { + addItemType(TYPE_FILE, new OnMultiItemAdapterListener() { + @NonNull + @Override + public QuickViewHolder onCreate(@NonNull Context context, @NonNull ViewGroup parent, + int viewType) { + return new QuickViewHolder(R.layout.item_file, parent); + } + + @Override + public void onBind(@NonNull QuickViewHolder holder, int position, + @Nullable BaseNode item) { + if (item instanceof MyNodeEntity) { + holder.setText(R.id.tv, ((MyNodeEntity) item).getName()) + .setText(R.id.sub_tv, ((MyNodeEntity) item).getTime().toString()); + } + } + }).addItemType(TYPE_FOLDER, new OnMultiItemAdapterListener() { + @NonNull + @Override + public QuickViewHolder onCreate(@NonNull Context context, @NonNull ViewGroup parent, + int viewType) { + return new QuickViewHolder(R.layout.item_folder, parent); + } + + @Override + public void onBind(@NonNull QuickViewHolder holder, int position, + @Nullable BaseNode item) { + if (item==null) { + return; + } + if (item instanceof MyNodeEntity) { + holder.setText(R.id.tv, ((MyNodeEntity) item).getName()); + } + + int angle = item.isExpand() ? 0 : -90; + holder.getView(R.id.iv).setRotation(angle); + + int nodeDeep = getNodeDepth(item); + switch (nodeDeep) { + case 2 -> holder.setImageResource(R.id.iv_head, R.mipmap.head_img1); + case 3 -> holder.setImageResource(R.id.iv_head, R.mipmap.head_img2); + default -> holder.setImageResource(R.id.iv_head, R.mipmap.head_img0); + } + } + }); + + //加载更多 + addItemType(TYPE_LOAD_MORE, new OnMultiItemAdapterListener() { + @Override + public void onBind(@NonNull QuickViewHolder holder, int position, + @Nullable BaseNode item) { + } + @NonNull + @Override + public QuickViewHolder onCreate(@NonNull Context context, @NonNull ViewGroup parent, + int viewType) { + return new QuickViewHolder(R.layout.view_load_more,parent); + } + }); + } + + @Override + public void dataSwap(int fromPosition, int toPosition) { + swap(fromPosition, toPosition); + } + + @Override + public void dataRemoveAt(int position) { + removeAt(position); + } +} diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/FileNodeEntity.java b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/FileNodeEntity.java new file mode 100644 index 000000000..c7b62c26a --- /dev/null +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/FileNodeEntity.java @@ -0,0 +1,50 @@ +package com.chad.baserecyclerviewadapterhelper.entity; + +import androidx.annotation.Nullable; + +import com.chad.baserecyclerviewadapterhelper.activity.treenode.adapter.TreeNodeAdapter; +import com.chad.library.adapter.base.BaseNode; + +import java.util.Date; +import java.util.List; + +/** + * @author Dboy233 + */ +public class FileNodeEntity extends BaseNode { + + private String name; + + private Date time = new Date(); + + public FileNodeEntity(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getTime() { + return time; + } + + public void setTime(Date time) { + this.time = time; + } + + @Override + public int getNodeType() { + return TreeNodeAdapter.TYPE_FILE; + } + + @Nullable + @Override + public List getChildNodes() { + return null; + } +} diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/FolderNodeEntity.java b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/FolderNodeEntity.java new file mode 100644 index 000000000..e43f7a181 --- /dev/null +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/FolderNodeEntity.java @@ -0,0 +1,42 @@ +package com.chad.baserecyclerviewadapterhelper.entity; + +import androidx.annotation.Nullable; + +import com.chad.baserecyclerviewadapterhelper.activity.treenode.adapter.TreeNodeAdapter; +import com.chad.library.adapter.base.BaseNode; + +import java.util.List; + +/** + * @author Dboy233 + */ +public class FolderNodeEntity extends BaseNode { + + private String name; + + private List childs; + + public FolderNodeEntity(String name, List childs) { + this.name = name; + this.childs = childs; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int getNodeType() { + return TreeNodeAdapter.TYPE_FOLDER; + } + + @Nullable + @Override + public List getChildNodes() { + return childs; + } +} diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/MyNodeEntity.kt b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/MyNodeEntity.kt new file mode 100644 index 000000000..661f19002 --- /dev/null +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/MyNodeEntity.kt @@ -0,0 +1,34 @@ +package com.chad.baserecyclerviewadapterhelper.entity + +import com.chad.baserecyclerviewadapterhelper.activity.treenode.adapter.TreeNodeAdapter +import com.chad.library.adapter.base.BaseNode +import java.util.* + +/** + * 可以使用同一个数据类,也可以分开使用例如[FileNodeEntity]和[FolderNodeEntity]。分开会比较麻烦。 + * + * 可以将子节点数据和父节点数据合并为一个节点 + * + * @author Dboy233 + */ +class MyNodeEntity( + val name: String? = null, + //一定要使用可变集合 + val child: MutableList? = null, + val time: Date = Date() +) : + BaseNode() { + + + override val nodeType: Int + get() = if (child == null) { + TreeNodeAdapter.TYPE_FILE + } else { + TreeNodeAdapter.TYPE_FOLDER + } + + override val childNodes: MutableList? + get() = if (child == null) null else child as MutableList + + +} \ No newline at end of file diff --git a/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/MyNodeLoadingEntity.java b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/MyNodeLoadingEntity.java new file mode 100644 index 000000000..efa7cd65d --- /dev/null +++ b/app/src/main/java/com/chad/baserecyclerviewadapterhelper/entity/MyNodeLoadingEntity.java @@ -0,0 +1,23 @@ +package com.chad.baserecyclerviewadapterhelper.entity; + +import androidx.annotation.Nullable; +import com.chad.baserecyclerviewadapterhelper.activity.treenode.adapter.TreeNodeAdapter; +import com.chad.library.adapter.base.BaseNode; +import java.util.List; + +/** + * 加载更多节点 + */ +public class MyNodeLoadingEntity extends BaseNode { + + @Override + public int getNodeType() { + return TreeNodeAdapter.TYPE_LOAD_MORE; + } + + @Nullable + @Override + public List getChildNodes() { + return null; + } +} diff --git a/app/src/main/res/layout/item_file.xml b/app/src/main/res/layout/item_file.xml new file mode 100644 index 000000000..def668ad0 --- /dev/null +++ b/app/src/main/res/layout/item_file.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_folder.xml b/app/src/main/res/layout/item_folder.xml new file mode 100644 index 000000000..be489efd3 --- /dev/null +++ b/app/src/main/res/layout/item_folder.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + diff --git a/library/src/main/java/com/chad/library/adapter/base/BaseNode.kt b/library/src/main/java/com/chad/library/adapter/base/BaseNode.kt new file mode 100644 index 000000000..28b4f76ba --- /dev/null +++ b/library/src/main/java/com/chad/library/adapter/base/BaseNode.kt @@ -0,0 +1,34 @@ +package com.chad.library.adapter.base + +/** + * 继承后只实现 childNode , 返回的集合类型必须是可变集合。 + * + * + * @author Dboy233 + */ +abstract class BaseNode { + /** + * 节点类型 + */ + abstract val nodeType: Int + + /** + * 展开,折叠状态。外部只允许Get。其真实状态由adapter设置 + */ + var isExpand = true + internal set + + /** + * 父节点,外部只允许Get。其真实对象由adapter设置。 + * 虽然其类型为可空,但是adapter在进行数据编排的时候会为每一个item都设置一个parent。 + * 即便是第一个节点也都有一个[BaseTreeNodeAdapter.RootNode]。 + */ + var parentNode: BaseNode? = null + internal set + + /** + * 子节点列表.必须是可变集合 + */ + abstract val childNodes: MutableList? + +} \ No newline at end of file diff --git a/library/src/main/java/com/chad/library/adapter/base/BaseQuickAdapter.kt b/library/src/main/java/com/chad/library/adapter/base/BaseQuickAdapter.kt index 581a852a4..347b09579 100644 --- a/library/src/main/java/com/chad/library/adapter/base/BaseQuickAdapter.kt +++ b/library/src/main/java/com/chad/library/adapter/base/BaseQuickAdapter.kt @@ -684,7 +684,7 @@ abstract class BaseQuickAdapter( /** * items 转化为 MutableList */ - private val mutableItems: MutableList + internal val mutableItems: MutableList get() { return when (items) { is java.util.ArrayList -> { diff --git a/library/src/main/java/com/chad/library/adapter/base/BaseTreeNodeAdapter.kt b/library/src/main/java/com/chad/library/adapter/base/BaseTreeNodeAdapter.kt new file mode 100644 index 000000000..fded7445c --- /dev/null +++ b/library/src/main/java/com/chad/library/adapter/base/BaseTreeNodeAdapter.kt @@ -0,0 +1,510 @@ +package com.chad.library.adapter.base + + +/** + * 树形结构数据适配器。 + * 功能实现: + * 1. 展开[expand] 可指定展开列表的深度 + * 2. 折叠[collapse] 折叠对应位置的ChildNode + * 3. 自动展开和折叠[switchState] + * 4. 添加[addNode],[addNodes] [add**]都可使用 + * 5. 删除[remove],[removeAt] 如果删除的node有child,它的child也将被移除。两种remove中默认实现逻辑 + * 6. 获取节点所在的层级[getNodeDepth] + * 7. 获取节点所在的父节点的的相对位置[getChildNodeIndex] + * + * 使用方式和[BaseMultiItemAdapter]一致。 + * + * @author Dboy233 + */ +abstract class BaseTreeNodeAdapter : BaseMultiItemAdapter() { + + /** + * 真实的根节点数据 + */ + private data class RootNode( + override val nodeType: Int = -1, + override val childNodes: MutableList = mutableListOf() + ) : BaseNode() + + /** + * 根节点。数据必须有始有终。这里就是始。这个对象不会添加到[items]集合中,也不会展示。 + */ + private val rootNode: BaseNode = RootNode() + + + init { + //已经实现此方法 + onItemViewType { position, list -> + if(position==-1)return@onItemViewType -1 + list[position].nodeType + } + } + + + /** + * 添加节点 + * [node] 要添加的节点 + * [expandDepth] 如果你新添加的节点存在多个嵌套的子节点,设置展开节点的深度, <= 0 全部不展开 + */ + @JvmOverloads + fun addNode(node: BaseNode, expandDepth: Int = Int.MAX_VALUE) { + val newNodes = depthNodes(node, expandDepth, object : DepthFindCallBack { + override fun accept(deep: Int, node: BaseNode) { + //当前的深度必须大于0。因为等于0属于最后一层。最后一层没有展开的资格。 + //当上一个条件成立, + //那么他必须有子节点才能展开。否则不允许。 + node.isExpand = deep > 0 && !node.childNodes.isNullOrEmpty() + } + }) + + rootNode.childNodes?.add(node) + node.parentNode = rootNode + + super.addAll(newNodes) + } + + @JvmOverloads + fun addNodes( + parentNode: BaseNode, + childNodes: List, + childIndex: Int = Int.MAX_VALUE, + expandDepth: Int = Int.MAX_VALUE + ){ + for (childNode in childNodes) { + addNode(parentNode,childNode,childIndex,expandDepth) + } + } + + /** + * 添加节点 + * [parentNode] 添加node的父node。 + * [childNode] 要添加的节点 + * [childIndex] 添加的node所在的位置,默认最后位置添加 + * [expandDepth] 如果你新添加的节点存在多个嵌套的子节点,设置展开节点的深度, <= 0 全部不展开 + */ + @JvmOverloads + fun addNode( + parentNode: BaseNode, + childNode: BaseNode, + childIndex: Int = Int.MAX_VALUE, + expandDepth: Int = Int.MAX_VALUE + ) { + childNode.parentNode = parentNode + parentNode.childNodes?.let { + //如果下标大于列表个数,就末尾添加 + val index = if (childIndex > it.size) it.size else childIndex + it.add(index, childNode) + //计算node在adapter中的位置 + val parentIndex = items.indexOf(parentNode) + + //修正node在adapter中的位置 + //加入插入位置之前有已经展开的Node需要加上所有展开的node,如果没有展开的Node,那么这个返回的fixSize其实等于childIndex的值。 + //其实这个nodeAlreadyExpandSize就是查找在adapter中相对parent的位置 + val fixSize = nodeAlreadyExpandSize(parentNode, childNode) + + val newIndex = parentIndex + 1 + fixSize + + val depthNodes = + depthNodes(childNode, expandDepth, onFind = object : DepthFindCallBack { + override fun accept(deep: Int, node: BaseNode) { + node.isExpand = deep > 0 && !node.childNodes.isNullOrEmpty() + } + }) + + //只有当父节点是展开状态的时候添加新的节点 + if (parentNode.isExpand) { + depthNodes.reversed().forEach { newNode -> + super.add(newIndex, newNode) + } + } + } + } + + /** + * 添加节点列表 + * - [data] 需要添加的所有子节点。初始化数据的时候可以使用 + * - [expandDepth] 如果你新添加的节点存在多个嵌套的子节点,设置展开节点的深度, <= 0 全部不展开 + */ + fun addNodes(data: List, expandDepth: Int = Int.MAX_VALUE) { + val newData = mutableListOf() + for (itemEntry in data) { + //将新节点添加到根节点 + rootNode.childNodes?.add(itemEntry) + itemEntry.parentNode = rootNode + + newData.addAll(depthNodes(itemEntry, expandDepth, onFind = object : DepthFindCallBack { + override fun accept(deep: Int, node: BaseNode) { + //当前的深度必须大于0。因为等于0属于最后一层。最后一层没有展开的资格。 + //当上一个条件成立, + //那么他必须有子节点才能展开。否则不允许。 + node.isExpand = deep > 0 && !node.childNodes.isNullOrEmpty() + } + })) + } + //添加新的数据 + super.addAll(newData) + } + + /** + * 设置新的集合 + */ + override fun submitList(list: List?) { + rootNode.childNodes?.clear() + addNodes(list ?: mutableListOf()) + } + + /** + * 在[rootNode]的子节点末尾添加 + */ + override fun add(data: BaseNode) { + addNode(data) + } + + /** + * 在[rootNode]的子节点末尾添加多个 + */ + override fun addAll(newCollection: Collection) { + addNodes(newCollection.toMutableList()) + } + + /** + * 在[rootNode]指定位置根节点下添加 + */ + override fun add(position: Int, data: BaseNode) { + val childSize = rootNode.childNodes?.size ?: 0 + if (position > childSize) throw IndexOutOfBoundsException("root node child size ${childSize}, position $position") + addNode(rootNode, data, position) + } + + /** + * 在[rootNode]指定位置根节点下添加多个 + */ + override fun addAll(position: Int, newCollection: Collection) { + val childSize = rootNode.childNodes?.size ?: 0 + if (position > childSize) throw IndexOutOfBoundsException("root node child size ${childSize}, position $position") + + newCollection.forEachIndexed { index, newNode -> + addNode(rootNode, newNode, position + index) + } + + } + + /** + * 如果对于数据的删除有自己的处理逻辑,重写此方法. + * 查看[removeNodeFromParentNode],[removeNodeChildren]的使用说明 + */ + override fun remove(data: BaseNode) { + //将节点和它的父节点分离 + removeNodeFromParentNode(data) + //如果要移除的Node已经展开并且有子节点,将子节点一并移除 + removeNodeChildren(data) + //从adapter items 中移除 + super.remove(data) + } + + + override fun removeAt(position: Int) { + //将节点和它的父节点分离 + val node = removeNodeFromParentNode(position) ?: return + //如果要移除的Node已经展开并且有子节点,将子节点一并移除 + removeNodeChildren(node) + //从adapter items 中移除 + super.removeAt(position) + } + + + /** + * 获取当前节点的深度。 + * @return 1 = 为第一层 此节点位于[RootNode]下 + */ + fun getNodeDepth(node: BaseNode): Int { + return internalGetNodeDepth(node) + } + + /** + * 获取node所在父节点下的位置 + * - [node] 需要获取位置的节点 + */ + fun getChildNodeIndex(node: BaseNode): Int { + return node.parentNode?.childNodes?.indexOfFirst { + it == node + } ?: -1 + } + + /** + * 切换 + */ + @JvmOverloads + fun switchState(position: Int, depth: Int = Int.MAX_VALUE) { + val item = getItem(position) ?: return + if (item.isExpand) { + collapse(position) + } else { + expand(position, depth) + } + } + + + /** + * 展开某个位置 + * - [position] 展开的位置,当前item包含多个子节点 + * - [depth] 展开是否包含子节点一起展开。true所有子节点都将展开,false只展开最近子节点 + */ + @JvmOverloads + fun expand(position: Int, depth: Int = Int.MAX_VALUE) { + val item = getItem(position) ?: return + if (item.childNodes.isNullOrEmpty()) { + return + } + if (item.isExpand) return + //修改展开状态 + item.isExpand = true + + if (depth > 1) { + item.childNodes?.reversed()?.forEachIndexed { _, node -> + //对节点的父节点赋值。每次都要确保其不为null才行 + node.parentNode = item + //查找子节点的深层节点并反转,因为添加位置是从当前position开始。 + //这里的depth-1是因为此时的node已经身处第一层了,向下查找层级就要排除自己这一层 + val needExpandNode = depthNodes(node, depth - 1, object : DepthFindCallBack { + override fun accept(deep: Int, node: BaseNode) { + //当前的深度必须大于0。因为等于0属于最后一层。最后一层没有展开的资格。 + //当上一个条件成立, + //那么他必须有子节点才能展开。否则不允许。 + node.isExpand = deep > 0 && !node.childNodes.isNullOrEmpty() + } + }).reversed() + + if (needExpandNode.isNotEmpty()) { + needExpandNode.forEach { deepNode -> + //将当前需要展开的节点位置添加 + mutableItems.add(position+1,deepNode) + } + //所有位置一起刷新 + notifyItemRangeInserted(position+1,needExpandNode.size) + } + } + } else { + //遍历最近子节点 + val needExpandNode = item.childNodes + if (!needExpandNode.isNullOrEmpty()) { + needExpandNode.forEachIndexed { _, node -> + //对节点的父节点赋值。每次都要确保其不为null才行 + node.parentNode = item + //不展开子节点,子节点的展开状态为false + node.isExpand = false + //将当前节点添加 + mutableItems.add(position+1,node) + } + //所有位置一起刷新 + notifyItemRangeInserted(position+1,needExpandNode.size) + } + } + //当然自己也要刷新一下 + notifyItemChanged(position) + } + + /** + * 折叠某个文件夹 + * [position] 折叠的Node位置,这个位置是Adapter中的位置 + */ + fun collapse(position: Int) { + val item = getItem(position) ?: return + if (!item.isExpand) return + item.isExpand = false + if (item.childNodes.isNullOrEmpty()) { + notifyItemChanged(position) + return + } + //需要删除的item记录 + val needRemoveItem = mutableListOf() + //检查从当前位置折叠需要折叠几个item + internalDetachedInspect(item, needRemoveItem) + //倒着移除每一个item + for (i in position + needRemoveItem.size downTo position + 1) { + mutableItems.removeAt(i) + } + //因为不涉及空布局的相关内容,所以当所有item全部折叠之后,一起刷新。 + notifyItemRangeRemoved(position+1,needRemoveItem.size) + notifyItemChanged(position) + } + + /** + * 从父节点中移除节点 + * - [childNode]要移除的节点 + * - [isolated] 是否将移除的节点孤立,true 它的parentNode将被设置为null.从此成为孤儿,false 依旧保持联系,可以随时回到parent的怀抱 + */ + protected fun removeNodeFromParentNode(childNode: BaseNode, isolated: Boolean = false) { + childNode.parentNode?.childNodes?.remove(childNode) + if (isolated) { + childNode.parentNode = null + } + } + + /** + * 从父节点移除下标位置的节点 + * - [position] 列表数据的下表位置 + * - [isolated] 是否将移除的节点孤立,true 它的parentNode将被设置为null.从此成为孤儿,false 依旧保持联系,可以随时回到parent的怀抱 + */ + protected fun removeNodeFromParentNode(position: Int, isolated: Boolean = false): BaseNode? { + if (position >= items.size) { + throw IndexOutOfBoundsException("position: ${position}. size:${items.size}") + } + val node = getItem(position) ?: return null + node.parentNode?.childNodes?.remove(node) + if (isolated) { + node.parentNode = null + } + return node + } + + /** + * 移除节点和节点包含的所有子节点 + * - [node] 要移除的节点 + * - [isolated] 移除它的子节点的时候是否将子节点也完全孤立,断开所有子节点和parentNode的联系。 + * + * [isolated] = false 其所有子节点都不是真正的被移除只是在[items]中移除, + * 如果你需要[node]的所有子节点不再展示,需要在外部调用`BaseNode.childNodes?.clear()`。 + * [isolated] = true 其所有树上的节点都将分离。 + */ + protected fun removeNodeChildren(node: BaseNode, isolated: Boolean = false) { + if (!node.isExpand) { + return + } + if (!node.childNodes.isNullOrEmpty()) { + val needRemove = mutableListOf() + internalDetachedInspect(node, needRemove) + needRemove.reversed().forEach { childNode -> + super.remove(childNode) + } + } + if (isolated) { + depthNodes(node).forEach { + it.parentNode = null + } + node.childNodes?.clear() + } + } + + /** + * 检查当前节点深度,如果没有子节点,返回包含当前节点的list,反之返回包含所有子节点的list + * + * 将多级的链表转成单级的列表: + * ``` + * -- root1 + * |--child1-1 + * |--child1-2 + * -- root2 + * |--child2-1 + * + * to + * + * -root1 + * -child1-1 + * -child1-2 + * -root2 + * -child2-1 + * -child2-2 + * ``` + * [node] 需要遍历深度节点的对象 + * [depth] 当到达指定深度的时候停止遍历 + * [onFind] deepNodes方法不会修改对象的任何属性,如果业务需要对节点属性修改,通过此方法,当添加一个节点的时候就会通知 + */ + private fun depthNodes( + node: BaseNode, + depth: Int = Int.MAX_VALUE, + onFind: DepthFindCallBack? = null + ): List { + return if (node.childNodes.isNullOrEmpty()) { + onFind?.accept(depth, node) + listOf(node)//添加自己 + } else { + val list = mutableListOf() + onFind?.accept(depth, node) + list.add(node)//先把自己添加到节点 + if (depth > 0) { + //检查直接子节点 子节点已经增加了一个层级了 + node.childNodes?.forEach { childNode -> + //为子节点设置父节点 + childNode.parentNode = node + if (childNode.childNodes.isNullOrEmpty()) { + //唯一子节点,添加,因为是Node的子节点,所以这里depth-1 + onFind?.accept(depth - 1, childNode) + list.add(childNode) + } else { + //递归添加剩余子节点 + list.addAll(depthNodes(childNode, depth - 1, onFind)) + } + } + } + list + } + } + + /** + * 折叠检查,检查当前item下面有几个需要进行折叠/移除的item + */ + private fun internalDetachedInspect(node: BaseNode, needRemove: MutableList) { + node.childNodes?.forEachIndexed { _, childNode -> + //记录需要移除的item + needRemove.add(childNode) + //只有状态是展开的,并且子节点不为null的才进行再次检查 + if (childNode.isExpand && !childNode.childNodes.isNullOrEmpty()) { + internalDetachedInspect(childNode, needRemove) + } + //当上方结束,修改它的状态 + childNode.isExpand = false + } + } + + /** + * 检查 parentNode 到 stopNode为止一共有几个已经展开的Node + */ + private fun nodeAlreadyExpandSize(parentNode: BaseNode, stopNode: BaseNode): Int { + var size = 0 + parentNode.childNodes?.forEach { + if (it == stopNode) return size + if (it.isExpand && !it.childNodes.isNullOrEmpty()) { + size++ + size += nodeAlreadyExpandSize(it, stopNode) + } else { + size++ + } + } + return size + } + + + /** + * 获取当前节点深度 + */ + private fun internalGetNodeDepth(node: BaseNode): Int { + return if (node.parentNode != null) { + 1 + internalGetNodeDepth(node.parentNode!!) + } else { + 1 + } + } + + /** + * 检查数据的末端Node,没有ChildNode视为末端节点其展开状态将一直维持在false + */ + private val checkTheEndBranches: DepthFindCallBack = object : DepthFindCallBack { + override fun accept(deep: Int, node: BaseNode) { + if (node.childNodes.isNullOrEmpty()) { + node.isExpand = false + } + } + } + + private interface DepthFindCallBack { + /** + * - [deep] 深度检测值,当deep=0的时候,不会再检查[node]的子节点信息 + * - [node] 当前深度的节点值 + * 这个接口用于方法[depthNodes]使用。当你传入的depth值为3,此方法[accept]的[deep]的数值变化将是 [3,2,1,0] + * 为什么是倒着的,因为递归查询每个层级依次递减。当到0的时候结束。 + */ + fun accept(deep: Int, node: BaseNode) + } + +} \ No newline at end of file