From 8b743ff79f4e37f27abd970ecac0c7d260380209 Mon Sep 17 00:00:00 2001 From: dingyi222666 Date: Sun, 15 Jan 2023 04:43:13 +0800 Subject: [PATCH] docs: update readme --- README.md | 85 ++++---- .../java/com/dingyi/treeview/MainActivity.kt | 29 +-- app/src/main/res/layout/item_dir.xml | 7 +- app/src/main/res/layout/item_file.xml | 6 +- treeview/build.gradle.kts | 2 +- .../dingyi222666/view/treeview/TreeView.kt | 199 +++++++++++++++--- .../dingyi222666/view/treeview/Trees.kt | 11 +- 7 files changed, 235 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 0e545b3..894d579 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ An TreeView implement in Android with RecyclerView written in kotlin. - Introduction Dependency ```groovy -implementation("io.github.dingyi222666:treeview:1.0.2") +implementation("io.github.dingyi222666:treeview:1.0.4") ``` - First, we need a way to get the data to display, in this case we fake some unreal data @@ -108,26 +108,25 @@ fun createVirtualFile(): VirtualFile { - Create a new node generator for the transformation of your data to the nodes ```kotlin - inner class NodeGenerator : TreeNodeGenerator { private val root = createVirtualFile() - override suspend fun refreshNode( targetNode: TreeNode, - oldNodeSet: Set, - withChild: Boolean, + oldChildNodeSet: Set, tree: AbstractTree, ): Set> = withContext(Dispatchers.IO) { + // delay(100) - val oldNodes = tree.getNodes(oldNodeSet) - val child = checkNotNull(targetNode.extra?.getChild()).toMutableSet() + val oldNodes = tree.getNodes(oldChildNodeSet) + + val child = checkNotNull(targetNode.data?.getChild()).toMutableSet() val result = mutableSetOf>() oldNodes.forEach { node -> - val virtualFile = child.find { it.name == node.extra?.name } + val virtualFile = child.find { it.name == node.data?.name } if (virtualFile != null) { result.add(node) } @@ -141,8 +140,8 @@ inner class NodeGenerator : TreeNodeGenerator { child.forEach { result.add( TreeNode( - it, targetNode.level + 1, it.name, - tree.generateId(), it.isDir && it.getChild().isNotEmpty(), false + it, targetNode.depth + 1, it.name, + tree.generateId(), it.isDir && it.getChild().isNotEmpty(), it.isDir, false ) ) } @@ -150,28 +149,26 @@ inner class NodeGenerator : TreeNodeGenerator { result } - override fun createRootNode(): TreeNode { return TreeNode(root, 0, root.name, Tree.ROOT_NODE_ID, true, true) } - - } ``` -- Create a node binder to bind the node to the layout, and in most cases also implement node click - events in this class +- Create a node binder to bind the node to the layout, and in most case also implement node click + events in this class + + Note: For indenting itemView, we recommend to add a Space to the far left of your layout. The width of this space is the width of the indent. ```kotlin - -inner class ViewBinder : TreeViewBinder(), TreeNodeListener { +inner class ViewBinder : TreeViewBinder(), TreeNodeEventListener { override fun createView(parent: ViewGroup, viewType: Int): View { - if (viewType == 1) { - return ItemDirBinding.inflate(layoutInflater, parent, false).root + return if (viewType == 1) { + ItemDirBinding.inflate(layoutInflater, parent, false).root } else { - return ItemFileBinding.inflate(layoutInflater, parent, false).root + ItemFileBinding.inflate(layoutInflater, parent, false).root } } @@ -179,7 +176,7 @@ inner class ViewBinder : TreeViewBinder(), TreeNodeListener, newItem: TreeNode ): Boolean { - return oldItem == newItem && oldItem.extra?.name == newItem.extra?.name + return oldItem == newItem && oldItem.data?.name == newItem.data?.name } override fun areItemsTheSame( @@ -190,7 +187,7 @@ inner class ViewBinder : TreeViewBinder(), TreeNodeListener): Int { - if (node.extra?.isDir == true) { + if (node.data?.isDir == true) { return 1 } return 0 @@ -199,28 +196,28 @@ inner class ViewBinder : TreeViewBinder(), TreeNodeListener, - listener: TreeNodeListener + listener: TreeNodeEventListener ) { if (node.hasChild) { applyDir(holder, node) } else { applyFile(holder, node) } - - - holder.itemView.updatePadding( - top = 0, - right = 0, - bottom = 0, - left = node.level * 10.dp - ) + + val itemView = if (getItemViewType(node) == 1) + ItemDirBinding.bind(holder.itemView).space + else ItemFileBinding.bind(holder.itemView).space + + itemView.updateLayoutParams { + width = node.depth * 10.dp + } + //itemView.updatePadding(top = 0,right = 0, bottom = 0, left = node.level * 10.dp) } private fun applyFile(holder: TreeView.ViewHolder, node: TreeNode) { val binding = ItemFileBinding.bind(holder.itemView) binding.tvName.text = node.name.toString() - } private fun applyDir(holder: TreeView.ViewHolder, node: TreeNode) { @@ -231,7 +228,7 @@ inner class ViewBinder : TreeViewBinder(), TreeNodeListener(), TreeNodeListener(), TreeNodeListener() + val tree = Tree.createTree() tree.generator = NodeGenerator() - tree.initTree() -binding.treeview.apply { - // horizontalScroll support, default is false +(binding.treeview as TreeView).apply { supportHorizontalScroll = true bindCoroutineScope(lifecycleScope) - this.tree = tree as Tree - binder = ViewBinder() as TreeViewBinder - nodeClickListener = binder + this.tree = tree + binder = ViewBinder() + nodeEventListener = binder } lifecycleScope.launch { @@ -289,6 +283,9 @@ lifecycleScope.launch { ``` +- Done! Enjoy using it. + ## Special thanks - - [Rosemoe](https://github.com/Rosemoe) (Help improve the Treeview horizontal sliding support) + +- [Rosemoe](https://github.com/Rosemoe) (Help improve the TreeView horizontal scrolling support) diff --git a/app/src/main/java/com/dingyi/treeview/MainActivity.kt b/app/src/main/java/com/dingyi/treeview/MainActivity.kt index 53d5832..90c58d0 100644 --- a/app/src/main/java/com/dingyi/treeview/MainActivity.kt +++ b/app/src/main/java/com/dingyi/treeview/MainActivity.kt @@ -14,7 +14,7 @@ import io.github.dingyi222666.view.treeview.AbstractTree import io.github.dingyi222666.view.treeview.Tree import io.github.dingyi222666.view.treeview.TreeNode import io.github.dingyi222666.view.treeview.TreeNodeGenerator -import io.github.dingyi222666.view.treeview.TreeNodeListener +import io.github.dingyi222666.view.treeview.TreeNodeEventListener import io.github.dingyi222666.view.treeview.TreeView import io.github.dingyi222666.view.treeview.TreeViewBinder import kotlinx.coroutines.Dispatchers @@ -38,12 +38,12 @@ class MainActivity : AppCompatActivity() { tree.generator = NodeGenerator() tree.initTree() - binding.treeview.apply { + (binding.treeview as TreeView).apply { supportHorizontalScroll = true bindCoroutineScope(lifecycleScope) - this.tree = tree as Tree - binder = ViewBinder() as TreeViewBinder - nodeClickListener = binder + this.tree = tree + binder = ViewBinder() + nodeEventListener = binder } lifecycleScope.launch { @@ -106,13 +106,13 @@ class MainActivity : AppCompatActivity() { return root } - inner class ViewBinder : TreeViewBinder(), TreeNodeListener { + inner class ViewBinder : TreeViewBinder(), TreeNodeEventListener { override fun createView(parent: ViewGroup, viewType: Int): View { - if (viewType == 1) { - return ItemDirBinding.inflate(layoutInflater, parent, false).root + return if (viewType == 1) { + ItemDirBinding.inflate(layoutInflater, parent, false).root } else { - return ItemFileBinding.inflate(layoutInflater, parent, false).root + ItemFileBinding.inflate(layoutInflater, parent, false).root } } @@ -140,16 +140,18 @@ class MainActivity : AppCompatActivity() { override fun bindView( holder: TreeView.ViewHolder, node: TreeNode, - listener: TreeNodeListener + listener: TreeNodeEventListener ) { if (node.hasChild) { applyDir(holder, node) } else { applyFile(holder, node) } + val itemView = if (getItemViewType(node) == 1) ItemDirBinding.bind(holder.itemView).space else ItemFileBinding.bind(holder.itemView).space + itemView.updateLayoutParams { width = node.depth * 10.dp } @@ -197,13 +199,14 @@ class MainActivity : AppCompatActivity() { inner class NodeGenerator : TreeNodeGenerator { private val root = createVirtualFile() - override suspend fun refreshNode( targetNode: TreeNode, oldChildNodeSet: Set, tree: AbstractTree, ): Set> = withContext(Dispatchers.IO) { + delay(100) + val oldNodes = tree.getNodes(oldChildNodeSet) val child = checkNotNull(targetNode.data?.getChild()).toMutableSet() @@ -226,7 +229,7 @@ class MainActivity : AppCompatActivity() { result.add( TreeNode( it, targetNode.depth + 1, it.name, - tree.generateId(), it.isDir && it.getChild().isNotEmpty(), false + tree.generateId(), it.isDir && it.getChild().isNotEmpty(), it.isDir, false ) ) } @@ -237,8 +240,6 @@ class MainActivity : AppCompatActivity() { override fun createRootNode(): TreeNode { return TreeNode(root, 0, root.name, Tree.ROOT_NODE_ID, true, true) } - - } } diff --git a/app/src/main/res/layout/item_dir.xml b/app/src/main/res/layout/item_dir.xml index a2224ea..31d7283 100644 --- a/app/src/main/res/layout/item_dir.xml +++ b/app/src/main/res/layout/item_dir.xml @@ -8,7 +8,7 @@ android:paddingTop="10dp" android:paddingBottom="10dp"> - @@ -30,9 +30,4 @@ android:gravity="center_vertical" android:textSize="18sp" tools:text="@string/app_name" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_file.xml b/app/src/main/res/layout/item_file.xml index 4d86901..70f7095 100644 --- a/app/src/main/res/layout/item_file.xml +++ b/app/src/main/res/layout/item_file.xml @@ -8,15 +8,15 @@ android:background="?selectableItemBackground" android:orientation="horizontal"> - + /> { +/** + * TreeView. + * + * TreeView based on RecyclerView implementation. + * + * The data in the [AbstractTree] can be displayed. + * + * @param T Data type of [tree] + * @see [AbstractTree] + * @see [Tree] + * @see [TreeViewBinder] + */ +class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : + RecyclerView(context, attrs, defStyleAttr), TreeNodeEventListener { constructor(context: Context, attrs: AttributeSet?) : this( context, @@ -30,16 +42,42 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : private var pointerId = 0 private var pointerLastX = 0f private var slopExceeded = false - private val horizontalTouchSlop = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, resources.displayMetrics) + private val horizontalTouchSlop = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3f, resources.displayMetrics) private val maxHorizontalOffset get() = (maxChildWidth - width * 0.75f).coerceAtLeast(0f) - lateinit var tree: Tree - - lateinit var binder: TreeViewBinder - - var nodeClickListener: TreeNodeListener = EmptyTreeNodeListener() - + /** + * Tree structure. + * + * Set it to allow TreeView to fetch node data. + */ + lateinit var tree: Tree + + /** + * TreeView Binder. + * + * Set it to bind between node and view + */ + lateinit var binder: TreeViewBinder + + /** + * Event listener for the node. + * + * Set it to listen for event on the node, such as a click on the node event. + */ + var nodeEventListener: TreeNodeEventListener = + EmptyTreeNodeEventListener() as TreeNodeEventListener + + /** + * Whether horizontal scrolling is supported. + * + * In most cases, you don't need to turn it on. + * + * you only need to turn it on when the indentation of the node binded view is too large, or when the width of the view itself is too large for the screen to be fully displayed. + * + * Note: This is still an experimental feature and some problems may arise. + */ var supportHorizontalScroll by Delegates.observable(false) { _, old, new -> if (!this::coroutineScope.isInitialized || old == new) { return@observable @@ -60,8 +98,8 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : rootView: View, ) : RecyclerView.ViewHolder(rootView) - private inner class Adapter(val binder: TreeViewBinder) : - ListAdapter, ViewHolder>(binder as DiffUtil.ItemCallback>) { + private inner class Adapter(val binder: TreeViewBinder) : + ListAdapter, ViewHolder>(binder as DiffUtil.ItemCallback>) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemView = binder.createView(parent, viewType) @@ -69,16 +107,22 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : } override fun getItemViewType(position: Int): Int { - return binder.getItemViewType(getItem(position) as TreeNode) + return binder.getItemViewType(getItem(position)) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val rootView = this@TreeView - val node = getItem(position) as TreeNode + val node = getItem(position) + holder.itemView.setOnClickListener { rootView.onClick(node, holder) } - binder.bindView(holder, node, rootView as TreeNodeListener) + + holder.itemView.setOnLongClickListener { + return@setOnLongClickListener rootView.onLongClick(node, holder) + } + + binder.bindView(holder, node, rootView) if (supportHorizontalScroll) { holder.itemView.apply { @@ -101,7 +145,8 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ) } } else { - holder.itemView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + holder.itemView.layoutParams = + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } } @@ -117,10 +162,27 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : } + /** + * Bind the concurrent scope of the TreeView. + * + * Some operations need to run on a concurrent scope, set it to make TreeView work better. + * + * Note: TreeView is not responsible for closing the concurrent scope, it is up to the caller to do so. + */ fun bindCoroutineScope(coroutineScope: CoroutineScope) { this.coroutineScope = coroutineScope } + /** + * Refresh the data. + * + * Call this method to refresh the node data to display on the TreeView. + * + * @param [fastRefresh] Whether to quick refresh or not. + * + * If ture, only data from the cache will be fetched instead of calling the [TreeNodeGenerator] + * @see [AbstractTree.toSortedList] + */ suspend fun refresh(fastRefresh: Boolean = false) { if (!this::_adapter.isInitialized) { initAdapter() @@ -190,7 +252,9 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : slopExceeded = true } if (slopExceeded) { - horizontalOffset = (pointerLastX - ev.x + horizontalOffset).coerceAtLeast(0f).coerceAtMost(maxHorizontalOffset) + horizontalOffset = + (pointerLastX - ev.x + horizontalOffset).coerceAtLeast(0f) + .coerceAtMost(maxHorizontalOffset) pointerLastX = ev.x invalidate() } @@ -217,11 +281,16 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : canvas.restore() } - override fun onClick(node: TreeNode, holder: ViewHolder) { - if (node.hasChild) { + override fun onClick(node: TreeNode, holder: ViewHolder) { + if (node.isChild) { onToggle(node, !node.expand, holder) } - nodeClickListener.onClick(node, holder) + + nodeEventListener.onClick(node, holder) + + if (!node.isChild) { + return + } coroutineScope.launch { tree.refresh(node) @@ -230,37 +299,107 @@ class TreeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : } - override fun onLongClick(node: TreeNode, holder: ViewHolder) { - nodeClickListener.onLongClick(node, holder) + override fun onLongClick(node: TreeNode, holder: ViewHolder): Boolean { + return nodeEventListener.onLongClick(node, holder) } - override fun onToggle(node: TreeNode, isExpand: Boolean, holder: ViewHolder) { + override fun onToggle(node: TreeNode, isExpand: Boolean, holder: ViewHolder) { node.expand = isExpand - nodeClickListener.onToggle(node, isExpand, holder) + nodeEventListener.onToggle(node, isExpand, holder) } } +/** + * Binder for TreeView and nodes. + * + * TreeView calls this class to get the generated itemView and bind the node data to the itemView + * + * @see [TreeView.binder] + */ abstract class TreeViewBinder : DiffUtil.ItemCallback>(), - TreeNodeListener { - + TreeNodeEventListener { + + /** + * like [RecyclerView.Adapter.onCreateViewHolder]. + * + * Simply provide View. No need to provide a ViewHolder. + * + * @see [RecyclerView.Adapter.onCreateViewHolder] + */ abstract fun createView(parent: ViewGroup, viewType: Int): View + /** + * like [RecyclerView.Adapter.onBindViewHolder] + * + * The adapter calls this method to display the data in the node to the view + * + * + * @param [node] target node + * @param [listener] The root event listener of the TreeView. + * + * If you need to override the itemView's click event or other action separately, + * call this event listener after you have completed your action. + * + * Otherwise the listener you set will not work either. + * + * @see [RecyclerView.Adapter.onBindViewHolder] + */ abstract fun bindView( holder: TreeView.ViewHolder, node: TreeNode, - listener: TreeNodeListener + listener: TreeNodeEventListener ) + /** + * like [RecyclerView.Adapter.getItemViewType] + * + * For inter node data, the type (whether it is a leaf node or not) varies and different layouts may need to be provided. + * + * You can return different numbers and these return values are mapped in the viewType in the [createView] + * + * @see [RecyclerView.Adapter.getItemViewType] + * @see [createView] + */ abstract fun getItemViewType(node: TreeNode): Int } -class EmptyTreeNodeListener : TreeNodeListener - -interface TreeNodeListener { +class EmptyTreeNodeEventListener : TreeNodeEventListener + +/** + * Event listener interface for tree nodes. + * + * Currently supported, [onClick], [onLongClick], and [onToggle] + */ +interface TreeNodeEventListener { + + /** + * Called when a node has been clicked. + * + * @param node Clicked node + * @param holder Node binding of the holder + */ fun onClick(node: TreeNode, holder: TreeView.ViewHolder) {} - fun onLongClick(node: TreeNode, holder: TreeView.ViewHolder) {} + + /** + * Called when a node has been clicked and held. + * + * @param node Clicked node + * @param holder Node binding of the holder + * @return `true` if the callback consumed the long click, false otherwise. + */ + fun onLongClick(node: TreeNode, holder: TreeView.ViewHolder): Boolean { + return false + } + + /** + * Called when a node is clicked when it is a child node + * + * @param node Clicked node + * @param isExpand Is the node expanded + * @param holder Node binding of the holder + */ fun onToggle(node: TreeNode, isExpand: Boolean, holder: TreeView.ViewHolder) {} } diff --git a/treeview/src/main/java/io/github/dingyi222666/view/treeview/Trees.kt b/treeview/src/main/java/io/github/dingyi222666/view/treeview/Trees.kt index f59db8c..601ff77 100644 --- a/treeview/src/main/java/io/github/dingyi222666/view/treeview/Trees.kt +++ b/treeview/src/main/java/io/github/dingyi222666/view/treeview/Trees.kt @@ -125,8 +125,11 @@ class Tree internal constructor() : AbstractTree { override var rootNode: TreeNode set(value) { + if (this::_rootNode.isInitialized) { + removeNode(_rootNode.id) + } _rootNode = value - setRootNode(value) + putNode(value.id, value) } get() = _rootNode @@ -164,10 +167,6 @@ class Tree internal constructor() : AbstractTree { return allNodeAndChild.get(nodeId) } - private fun setRootNode(rootNode: TreeNode<*>) { - _rootNode = rootNode as TreeNode - putNode(rootNode.id, rootNode) - } override fun createRootNode(): TreeNode<*> { val rootNode = createRootNodeUseGenerator() ?: DefaultTreeNode( @@ -175,7 +174,7 @@ class Tree internal constructor() : AbstractTree { ) rootNode.isChild = true rootNode.expand = true - setRootNode(rootNode) + this.rootNode = rootNode as TreeNode return rootNode }