diff --git a/doric-android/devkit/src/main/AndroidManifest.xml b/doric-android/devkit/src/main/AndroidManifest.xml index c99562c9..a58da216 100644 --- a/doric-android/devkit/src/main/AndroidManifest.xml +++ b/doric-android/devkit/src/main/AndroidManifest.xml @@ -11,5 +11,6 @@ android:name=".ui.DoricDevActivity" android:theme="@style/Theme.Design.Light.NoActionBar" /> + diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricDevActivity.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricDevActivity.java index d9c685b5..51fe0516 100644 --- a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricDevActivity.java +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricDevActivity.java @@ -21,7 +21,6 @@ import android.widget.CompoundButton; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.Switch; import android.widget.TextView; import androidx.annotation.NonNull; @@ -50,6 +49,8 @@ import pub.doric.devkit.qrcode.DisplayUtil; import pub.doric.devkit.qrcode.activity.CaptureActivity; import pub.doric.devkit.qrcode.activity.CodeUtils; +import static pub.doric.devkit.ui.DoricShowNodeTreeActivity.DORIC_CONTEXT_ID_KEY; + public class DoricDevActivity extends AppCompatActivity implements DoricDev.StatusCallback { private int REQUEST_CODE = 100; private ContextCellAdapter cellAdapter; @@ -363,6 +364,9 @@ public class DoricDevActivity extends AppCompatActivity implements DoricDev.Stat builder.show(); } }); + ArrayList list = new ArrayList<>(); + list.add("View source"); + list.add("Show node tree"); if (DoricDev.getInstance().isInDevMode()) { if (context.getDriver() instanceof DoricDebugDriver) { actionMap.put("Stop debugging", new DialogInterface.OnClickListener() { diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricShowNodeTreeActivity.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricShowNodeTreeActivity.java new file mode 100644 index 00000000..60173977 --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/DoricShowNodeTreeActivity.java @@ -0,0 +1,100 @@ +package pub.doric.devkit.ui; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import pub.doric.DoricContext; +import pub.doric.DoricContextManager; +import pub.doric.devkit.R; +import pub.doric.devkit.ui.treeview.DoricViewNodeLayoutItemType; +import pub.doric.devkit.ui.treeview.DoricViewNodeTreeViewBinder; +import pub.doric.devkit.ui.treeview.TreeNode; +import pub.doric.devkit.ui.treeview.TreeViewAdapter; +import pub.doric.shader.GroupNode; +import pub.doric.shader.ViewNode; + +public class DoricShowNodeTreeActivity extends AppCompatActivity { + + public static final String DORIC_CONTEXT_ID_KEY = "DORIC_CONTEXT_ID"; + + private RecyclerView rv; + private TreeViewAdapter adapter; + + private DoricContext doricContext; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String contextId = getIntent().getStringExtra(DORIC_CONTEXT_ID_KEY); + doricContext = DoricContextManager.getContext(contextId); + + setContentView(R.layout.layout_show_node_tree); + initView(); + initData(); + } + + private void initData() { + List nodes = new ArrayList<>(); + TreeNode root = new TreeNode<>(new DoricViewNodeLayoutItemType(doricContext.getRootNode())); + + Queue viewQueue = new LinkedList<>(); + Queue treeQueue = new LinkedList<>(); + + viewQueue.offer(doricContext.getRootNode()); + treeQueue.offer(root); + + while (!viewQueue.isEmpty()) { + ViewNode viewNode = viewQueue.poll(); + TreeNode treeNode = treeQueue.poll(); + if (viewNode instanceof GroupNode) { + GroupNode groupNode = (GroupNode) viewNode; + for (int i = 0; i != groupNode.getChildNodes().size(); i++) { + assert treeNode != null; + TreeNode temp = new TreeNode(new DoricViewNodeLayoutItemType((ViewNode) groupNode.getChildNodes().get(i))); + treeNode.addChild(temp); + + viewQueue.offer((ViewNode) groupNode.getChildNodes().get(i)); + treeQueue.offer(temp); + } + } + } + + nodes.add(root); + + rv.setLayoutManager(new LinearLayoutManager(this)); + adapter = new TreeViewAdapter(nodes, Collections.singletonList(new DoricViewNodeTreeViewBinder())); + // whether collapse child nodes when their parent node was close. +// adapter.ifCollapseChildWhileCollapseParent(true); + adapter.setOnTreeNodeListener(new TreeViewAdapter.OnTreeNodeListener() { + @Override + public boolean onClick(TreeNode node, RecyclerView.ViewHolder holder) { + if (!node.isLeaf()) { + //Update and toggle the node. + onToggle(!node.isExpand(), holder); +// if (!node.isExpand()) +// adapter.collapseBrotherNode(node); + } + return false; + } + + @Override + public void onToggle(boolean isExpand, RecyclerView.ViewHolder holder) { + DoricViewNodeTreeViewBinder.ViewHolder viewNodeTreeViewBinder = (DoricViewNodeTreeViewBinder.ViewHolder) holder; + } + }); + rv.setAdapter(adapter); + } + + private void initView() { + rv = findViewById(R.id.show_node_tree_rv); + } +} \ No newline at end of file diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/DoricViewNodeLayoutItemType.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/DoricViewNodeLayoutItemType.java new file mode 100644 index 00000000..ed832ffc --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/DoricViewNodeLayoutItemType.java @@ -0,0 +1,17 @@ +package pub.doric.devkit.ui.treeview; + +import pub.doric.devkit.R; +import pub.doric.shader.ViewNode; + +public class DoricViewNodeLayoutItemType implements LayoutItemType { + private final ViewNode viewNode; + + public DoricViewNodeLayoutItemType(ViewNode viewNode) { + this.viewNode = viewNode; + } + + @Override + public int getLayoutId() { + return R.layout.layout_show_node_tree_cell; + } +} diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/DoricViewNodeTreeViewBinder.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/DoricViewNodeTreeViewBinder.java new file mode 100644 index 00000000..8ae12939 --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/DoricViewNodeTreeViewBinder.java @@ -0,0 +1,30 @@ +package pub.doric.devkit.ui.treeview; + +import android.view.View; + +import pub.doric.devkit.R; + +public class DoricViewNodeTreeViewBinder extends TreeViewBinder { + + @Override + public ViewHolder provideViewHolder(View itemView) { + return new ViewHolder(itemView); + } + + @Override + public void bindView(ViewHolder holder, int position, TreeNode node) { + DoricViewNodeLayoutItemType layoutItemType = (DoricViewNodeLayoutItemType) node.getContent(); + } + + @Override + public int getLayoutId() { + return R.layout.layout_show_node_tree_cell; + } + + public static class ViewHolder extends TreeViewBinder.ViewHolder { + + public ViewHolder(View rootView) { + super(rootView); + } + } +} diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/LayoutItemType.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/LayoutItemType.java new file mode 100644 index 00000000..0c4c505f --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/LayoutItemType.java @@ -0,0 +1,5 @@ +package pub.doric.devkit.ui.treeview; + +public interface LayoutItemType { + int getLayoutId(); +} \ No newline at end of file diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeNode.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeNode.java new file mode 100644 index 00000000..99d34211 --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeNode.java @@ -0,0 +1,145 @@ +package pub.doric.devkit.ui.treeview; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +public class TreeNode implements Cloneable { + private T content; + private TreeNode parent; + private List childList; + private boolean isExpand; + private boolean isLocked; + //the tree high + private int height = UNDEFINE; + + private static final int UNDEFINE = -1; + + public TreeNode(@NonNull T content) { + this.content = content; + this.childList = new ArrayList<>(); + } + + public int getHeight() { + if (isRoot()) + height = 0; + else if (height == UNDEFINE) + height = parent.getHeight() + 1; + return height; + } + + public boolean isRoot() { + return parent == null; + } + + public boolean isLeaf() { + return childList == null || childList.isEmpty(); + } + + public void setContent(T content) { + this.content = content; + } + + public T getContent() { + return content; + } + + public List getChildList() { + return childList; + } + + public void setChildList(List childList) { + this.childList.clear(); + for (TreeNode treeNode : childList) { + addChild(treeNode); + } + } + + public TreeNode addChild(TreeNode node) { + if (childList == null) + childList = new ArrayList<>(); + childList.add(node); + node.parent = this; + return this; + } + + public boolean toggle() { + isExpand = !isExpand; + return isExpand; + } + + public void collapse() { + if (isExpand) { + isExpand = false; + } + } + + public void collapseAll() { + if (childList == null || childList.isEmpty()) { + return; + } + for (TreeNode child : this.childList) { + child.collapseAll(); + } + } + + public void expand() { + if (!isExpand) { + isExpand = true; + } + } + + public void expandAll() { + expand(); + if (childList == null || childList.isEmpty()) { + return; + } + for (TreeNode child : this.childList) { + child.expandAll(); + } + } + + public boolean isExpand() { + return isExpand; + } + + public void setParent(TreeNode parent) { + this.parent = parent; + } + + public TreeNode getParent() { + return parent; + } + + public TreeNode lock() { + isLocked = true; + return this; + } + + public TreeNode unlock() { + isLocked = false; + return this; + } + + public boolean isLocked() { + return isLocked; + } + + @Override + public String toString() { + return "TreeNode{" + + "content=" + this.content + + ", parent=" + (parent == null ? "null" : parent.getContent().toString()) + + ", childList=" + (childList == null ? "null" : childList.toString()) + + ", isExpand=" + isExpand + + '}'; + } + + @Override + protected TreeNode clone() throws CloneNotSupportedException { + TreeNode clone = new TreeNode<>(this.content); + clone.isExpand = this.isExpand; + return clone; + } +} \ No newline at end of file diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeViewAdapter.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeViewAdapter.java new file mode 100644 index 00000000..e7438ab4 --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeViewAdapter.java @@ -0,0 +1,325 @@ +package pub.doric.devkit.ui.treeview; + +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class TreeViewAdapter extends RecyclerView.Adapter { + private static final String KEY_IS_EXPAND = "IS_EXPAND"; + private final List viewBinders; + private List displayNodes; + private int padding = 30; + private OnTreeNodeListener onTreeNodeListener; + private boolean toCollapseChild; + + public TreeViewAdapter(List viewBinders) { + this(null, viewBinders); + } + + public TreeViewAdapter(List nodes, List viewBinders) { + displayNodes = new ArrayList<>(); + if (nodes != null) + findDisplayNodes(nodes); + this.viewBinders = viewBinders; + } + + /** + * 从nodes的结点中寻找展开了的非叶结点,添加到displayNodes中。 + * + * @param nodes 基准点 + */ + private void findDisplayNodes(List nodes) { + for (TreeNode node : nodes) { + displayNodes.add(node); + if (!node.isLeaf() && node.isExpand()) + findDisplayNodes(node.getChildList()); + } + } + + @Override + public int getItemViewType(int position) { + return displayNodes.get(position).getContent().getLayoutId(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(viewType, parent, false); + if (viewBinders.size() == 1) + return viewBinders.get(0).provideViewHolder(v); + for (TreeViewBinder viewBinder : viewBinders) { + if (viewBinder.getLayoutId() == viewType) + return viewBinder.provideViewHolder(v); + } + return viewBinders.get(0).provideViewHolder(v); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) { + if (payloads != null && !payloads.isEmpty()) { + Bundle b = (Bundle) payloads.get(0); + for (String key : b.keySet()) { + switch (key) { + case KEY_IS_EXPAND: + if (onTreeNodeListener != null) + onTreeNodeListener.onToggle(b.getBoolean(key), holder); + break; + } + } + } + super.onBindViewHolder(holder, position, payloads); + } + + @Override + public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + holder.itemView.setPaddingRelative(displayNodes.get(position).getHeight() * padding, 3, 3, 3); + } else { + holder.itemView.setPadding(displayNodes.get(position).getHeight() * padding, 3, 3, 3); + } + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + TreeNode selectedNode = displayNodes.get(holder.getLayoutPosition()); + // Prevent multi-click during the short interval. + try { + long lastClickTime = (long) holder.itemView.getTag(); + if (System.currentTimeMillis() - lastClickTime < 500) + return; + } catch (Exception e) { + holder.itemView.setTag(System.currentTimeMillis()); + } + holder.itemView.setTag(System.currentTimeMillis()); + + if (onTreeNodeListener != null && onTreeNodeListener.onClick(selectedNode, holder)) + return; + if (selectedNode.isLeaf()) + return; + // This TreeNode was locked to click. + if (selectedNode.isLocked()) return; + boolean isExpand = selectedNode.isExpand(); + int positionStart = displayNodes.indexOf(selectedNode) + 1; + if (!isExpand) { + notifyItemRangeInserted(positionStart, addChildNodes(selectedNode, positionStart)); + } else { + notifyItemRangeRemoved(positionStart, removeChildNodes(selectedNode, true)); + } + } + }); + for (TreeViewBinder viewBinder : viewBinders) { + if (viewBinder.getLayoutId() == displayNodes.get(position).getContent().getLayoutId()) + viewBinder.bindView(holder, position, displayNodes.get(position)); + } + } + + private int addChildNodes(TreeNode pNode, int startIndex) { + List childList = pNode.getChildList(); + int addChildCount = 0; + for (TreeNode treeNode : childList) { + displayNodes.add(startIndex + addChildCount++, treeNode); + if (treeNode.isExpand()) { + addChildCount += addChildNodes(treeNode, startIndex + addChildCount); + } + } + if (!pNode.isExpand()) + pNode.toggle(); + return addChildCount; + } + + private int removeChildNodes(TreeNode pNode) { + return removeChildNodes(pNode, true); + } + + private int removeChildNodes(TreeNode pNode, boolean shouldToggle) { + if (pNode.isLeaf()) + return 0; + List childList = pNode.getChildList(); + int removeChildCount = childList.size(); + displayNodes.removeAll(childList); + for (TreeNode child : childList) { + if (child.isExpand()) { + if (toCollapseChild) + child.toggle(); + removeChildCount += removeChildNodes(child, false); + } + } + if (shouldToggle) + pNode.toggle(); + return removeChildCount; + } + + @Override + public int getItemCount() { + return displayNodes == null ? 0 : displayNodes.size(); + } + + public void setPadding(int padding) { + this.padding = padding; + } + + public void ifCollapseChildWhileCollapseParent(boolean toCollapseChild) { + this.toCollapseChild = toCollapseChild; + } + + public void setOnTreeNodeListener(OnTreeNodeListener onTreeNodeListener) { + this.onTreeNodeListener = onTreeNodeListener; + } + + public interface OnTreeNodeListener { + /** + * called when TreeNodes were clicked. + * + * @return weather consume the click event. + */ + boolean onClick(TreeNode node, RecyclerView.ViewHolder holder); + + /** + * called when TreeNodes were toggle. + * + * @param isExpand the status of TreeNodes after being toggled. + */ + void onToggle(boolean isExpand, RecyclerView.ViewHolder holder); + } + + public void refresh(List treeNodes) { + displayNodes.clear(); + findDisplayNodes(treeNodes); + notifyDataSetChanged(); + } + + public Iterator getDisplayNodesIterator() { + return displayNodes.iterator(); + } + + private void notifyDiff(final List temp) { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return temp.size(); + } + + @Override + public int getNewListSize() { + return displayNodes.size(); + } + + // judge if the same items + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return TreeViewAdapter.this.areItemsTheSame(temp.get(oldItemPosition), displayNodes.get(newItemPosition)); + } + + // if they are the same items, whether the contents has bean changed. + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return TreeViewAdapter.this.areContentsTheSame(temp.get(oldItemPosition), displayNodes.get(newItemPosition)); + } + + @Nullable + @Override + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + return TreeViewAdapter.this.getChangePayload(temp.get(oldItemPosition), displayNodes.get(newItemPosition)); + } + }); + diffResult.dispatchUpdatesTo(this); + } + + private Object getChangePayload(TreeNode oldNode, TreeNode newNode) { + Bundle diffBundle = new Bundle(); + if (newNode.isExpand() != oldNode.isExpand()) { + diffBundle.putBoolean(KEY_IS_EXPAND, newNode.isExpand()); + } + if (diffBundle.size() == 0) + return null; + return diffBundle; + } + + // For DiffUtil, if they are the same items, whether the contents has bean changed. + private boolean areContentsTheSame(TreeNode oldNode, TreeNode newNode) { + return oldNode.getContent() != null && oldNode.getContent().equals(newNode.getContent()) + && oldNode.isExpand() == newNode.isExpand(); + } + + // judge if the same item for DiffUtil + private boolean areItemsTheSame(TreeNode oldNode, TreeNode newNode) { + return oldNode.getContent() != null && oldNode.getContent().equals(newNode.getContent()); + } + + /** + * collapse all root nodes. + */ + public void collapseAll() { + // Back up the nodes are displaying. + List temp = backupDisplayNodes(); + //find all root nodes. + List roots = new ArrayList<>(); + for (TreeNode displayNode : displayNodes) { + if (displayNode.isRoot()) + roots.add(displayNode); + } + //Close all root nodes. + for (TreeNode root : roots) { + if (root.isExpand()) + removeChildNodes(root); + } + notifyDiff(temp); + } + + @NonNull + private List backupDisplayNodes() { + List temp = new ArrayList<>(); + for (TreeNode displayNode : displayNodes) { + try { + temp.add(displayNode.clone()); + } catch (CloneNotSupportedException e) { + temp.add(displayNode); + } + } + return temp; + } + + public void collapseNode(TreeNode pNode) { + List temp = backupDisplayNodes(); + removeChildNodes(pNode); + notifyDiff(temp); + } + + public void collapseBrotherNode(TreeNode pNode) { + List temp = backupDisplayNodes(); + if (pNode.isRoot()) { + List roots = new ArrayList<>(); + for (TreeNode displayNode : displayNodes) { + if (displayNode.isRoot()) + roots.add(displayNode); + } + //Close all root nodes. + for (TreeNode root : roots) { + if (root.isExpand() && !root.equals(pNode)) + removeChildNodes(root); + } + } else { + TreeNode parent = pNode.getParent(); + if (parent == null) + return; + List childList = parent.getChildList(); + for (TreeNode node : childList) { + if (node.equals(pNode) || !node.isExpand()) + continue; + removeChildNodes(node); + } + } + notifyDiff(temp); + } + +} \ No newline at end of file diff --git a/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeViewBinder.java b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeViewBinder.java new file mode 100644 index 00000000..b7918e75 --- /dev/null +++ b/doric-android/devkit/src/main/java/pub/doric/devkit/ui/treeview/TreeViewBinder.java @@ -0,0 +1,23 @@ +package pub.doric.devkit.ui.treeview; + +import android.view.View; + +import androidx.annotation.IdRes; +import androidx.recyclerview.widget.RecyclerView; + +public abstract class TreeViewBinder implements LayoutItemType { + public abstract VH provideViewHolder(View itemView); + + public abstract void bindView(VH holder, int position, TreeNode node); + + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View rootView) { + super(rootView); + } + + protected T findViewById(@IdRes int id) { + return itemView.findViewById(id); + } + } + +} \ No newline at end of file diff --git a/doric-android/devkit/src/main/res/layout/layout_show_node_tree.xml b/doric-android/devkit/src/main/res/layout/layout_show_node_tree.xml new file mode 100644 index 00000000..3a1969a4 --- /dev/null +++ b/doric-android/devkit/src/main/res/layout/layout_show_node_tree.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/doric-android/devkit/src/main/res/layout/layout_show_node_tree_cell.xml b/doric-android/devkit/src/main/res/layout/layout_show_node_tree_cell.xml new file mode 100644 index 00000000..4aadd3cc --- /dev/null +++ b/doric-android/devkit/src/main/res/layout/layout_show_node_tree_cell.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file