package com.unnamed.b.atv.view; import android.content.Context; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.LinearLayout; import android.widget.ScrollView; import com.unnamed.b.atv.R; import com.unnamed.b.atv.holder.SimpleViewHolder; import com.unnamed.b.atv.model.TreeNode; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Created by Bogdan Melnychuk on 2/10/15. */ public class AndroidTreeView { private static final String NODES_PATH_SEPARATOR = ";"; private TreeNode mRoot; private Context mContext; private boolean applyForRoot; private int containerStyle = 0; private Class<? extends TreeNode.BaseNodeViewHolder> defaultViewHolderClass = SimpleViewHolder.class; private TreeNode.TreeNodeClickListener nodeClickListener; private boolean mSelectionModeEnabled; private boolean mUseDefaultAnimation = false; private boolean use2dScroll = false; public AndroidTreeView(Context context, TreeNode root) { mRoot = root; mContext = context; } public void setDefaultAnimation(boolean defaultAnimation) { this.mUseDefaultAnimation = defaultAnimation; } public void setDefaultContainerStyle(int style) { setDefaultContainerStyle(style, false); } public void setDefaultContainerStyle(int style, boolean applyForRoot) { containerStyle = style; this.applyForRoot = applyForRoot; } public void setUse2dScroll(boolean use2dScroll) { this.use2dScroll = use2dScroll; } public boolean is2dScrollEnabled() { return use2dScroll; } public void setDefaultViewHolder(Class<? extends TreeNode.BaseNodeViewHolder> viewHolder) { defaultViewHolderClass = viewHolder; } public void setDefaultNodeClickListener(TreeNode.TreeNodeClickListener listener) { nodeClickListener = listener; } public void expandAll() { expandNode(mRoot, true); } public void collapseAll() { for (TreeNode n : mRoot.getChildren()) { collapseNode(n, true); } } public View getView(int style) { final ViewGroup view; if (style > 0) { ContextThemeWrapper newContext = new ContextThemeWrapper(mContext, style); view = use2dScroll ? new TwoDScrollView(newContext) : new ScrollView(newContext); } else { view = use2dScroll ? new TwoDScrollView(mContext) : new ScrollView(mContext); } Context containerContext = mContext; if (containerStyle != 0 && applyForRoot) { containerContext = new ContextThemeWrapper(mContext, containerStyle); } final LinearLayout viewTreeItems = new LinearLayout(containerContext, null, containerStyle); viewTreeItems.setId(R.id.tree_items); viewTreeItems.setOrientation(LinearLayout.VERTICAL); view.addView(viewTreeItems); mRoot.setViewHolder(new TreeNode.BaseNodeViewHolder(mContext) { @Override public View createNodeView(TreeNode node, Object value) { return null; } @Override public ViewGroup getNodeItemsView() { return viewTreeItems; } }); expandNode(mRoot, false); return view; } public View getView() { return getView(-1); } public void expandLevel(int level) { for (TreeNode n : mRoot.getChildren()) { expandLevel(n, level); } } private void expandLevel(TreeNode node, int level) { if (node.getLevel() <= level) { expandNode(node, false); } for (TreeNode n : node.getChildren()) { expandLevel(n, level); } } public void expandNode(TreeNode node) { expandNode(node, false); } public void collapseNode(TreeNode node) { collapseNode(node, false); } public String getSaveState() { final StringBuilder builder = new StringBuilder(); getSaveState(mRoot, builder); if (builder.length() > 0) { builder.setLength(builder.length() - 1); } return builder.toString(); } public void restoreState(String saveState) { if (!TextUtils.isEmpty(saveState)) { collapseAll(); final String[] openNodesArray = saveState.split(NODES_PATH_SEPARATOR); final Set<String> openNodes = new HashSet<>(Arrays.asList(openNodesArray)); restoreNodeState(mRoot, openNodes); } } private void restoreNodeState(TreeNode node, Set<String> openNodes) { for (TreeNode n : node.getChildren()) { if (openNodes.contains(n.getPath())) { expandNode(n); restoreNodeState(n, openNodes); } } } private void getSaveState(TreeNode root, StringBuilder sBuilder) { for (TreeNode node : root.getChildren()) { if (node.isExpanded()) { sBuilder.append(node.getPath()); sBuilder.append(NODES_PATH_SEPARATOR); getSaveState(node, sBuilder); } } } private void toggleNode(TreeNode node) { if (node.isExpanded()) { collapseNode(node, false); } else { expandNode(node, false); } } private void collapseNode(TreeNode node, final boolean includeSubnodes) { node.setExpanded(false); TreeNode.BaseNodeViewHolder nodeViewHolder = getViewHolderForNode(node); if (mUseDefaultAnimation) { collapse(nodeViewHolder.getNodeItemsView()); } else { nodeViewHolder.getNodeItemsView().setVisibility(View.GONE); } nodeViewHolder.toggle(false); if (includeSubnodes) { for (TreeNode n : node.getChildren()) { collapseNode(n, includeSubnodes); } } } private void expandNode(final TreeNode node, boolean includeSubnodes) { node.setExpanded(true); final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(node); parentViewHolder.getNodeItemsView().removeAllViews(); parentViewHolder.toggle(true); for (final TreeNode n : node.getChildren()) { addNode(parentViewHolder.getNodeItemsView(), n); if (n.isExpanded() || includeSubnodes) { expandNode(n, includeSubnodes); } } if (mUseDefaultAnimation) { expand(parentViewHolder.getNodeItemsView()); } else { parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); } } private void addNode(ViewGroup container, final TreeNode n) { final TreeNode.BaseNodeViewHolder viewHolder = getViewHolderForNode(n); final View nodeView = viewHolder.getView(); container.addView(nodeView); if (mSelectionModeEnabled) { viewHolder.toggleSelectionMode(mSelectionModeEnabled); } nodeView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (n.getClickListener() != null) { n.getClickListener().onClick(n, n.getValue()); } else if (nodeClickListener != null) { nodeClickListener.onClick(n, n.getValue()); } toggleNode(n); } }); } //------------------------------------------------------------ // Selection methods public void setSelectionModeEnabled(boolean selectionModeEnabled) { if (!selectionModeEnabled) { // TODO fix double iteration over tree deselectAll(); } mSelectionModeEnabled = selectionModeEnabled; for (TreeNode node : mRoot.getChildren()) { toggleSelectionMode(node, selectionModeEnabled); } } public <E> List<E> getSelectedValues(Class<E> clazz) { List<E> result = new ArrayList<>(); List<TreeNode> selected = getSelected(); for (TreeNode n : selected) { Object value = n.getValue(); if (value != null && value.getClass().equals(clazz)) { result.add((E) value); } } return result; } public boolean isSelectionModeEnabled() { return mSelectionModeEnabled; } private void toggleSelectionMode(TreeNode parent, boolean mSelectionModeEnabled) { toogleSelectionForNode(parent, mSelectionModeEnabled); if (parent.isExpanded()) { for (TreeNode node : parent.getChildren()) { toggleSelectionMode(node, mSelectionModeEnabled); } } } public List<TreeNode> getSelected() { if (mSelectionModeEnabled) { return getSelected(mRoot); } else { return new ArrayList<>(); } } // TODO Do we need to go through whole tree? Save references or consider collapsed nodes as not selected private List<TreeNode> getSelected(TreeNode parent) { List<TreeNode> result = new ArrayList<>(); for (TreeNode n : parent.getChildren()) { if (n.isSelected()) { result.add(n); } result.addAll(getSelected(n)); } return result; } public void selectAll(boolean skipCollapsed) { makeAllSelection(true, skipCollapsed); } public void deselectAll() { makeAllSelection(false, false); } private void makeAllSelection(boolean selected, boolean skipCollapsed) { if (mSelectionModeEnabled) { for (TreeNode node : mRoot.getChildren()) { selectNode(node, selected, skipCollapsed); } } } public void selectNode(TreeNode node, boolean selected) { if (mSelectionModeEnabled) { node.setSelected(selected); toogleSelectionForNode(node, true); } } private void selectNode(TreeNode parent, boolean selected, boolean skipCollapsed) { parent.setSelected(selected); toogleSelectionForNode(parent, true); boolean toContinue = skipCollapsed ? parent.isExpanded() : true; if (toContinue) { for (TreeNode node : parent.getChildren()) { selectNode(node, selected, skipCollapsed); } } } private void toogleSelectionForNode(TreeNode node, boolean makeSelectable) { TreeNode.BaseNodeViewHolder holder = getViewHolderForNode(node); if (holder.isInitialized()) { getViewHolderForNode(node).toggleSelectionMode(makeSelectable); } } private TreeNode.BaseNodeViewHolder getViewHolderForNode(TreeNode node) { TreeNode.BaseNodeViewHolder viewHolder = node.getViewHolder(); if (viewHolder == null) { try { final Object object = defaultViewHolderClass.getConstructor(Context.class).newInstance(new Object[]{mContext}); viewHolder = (TreeNode.BaseNodeViewHolder) object; node.setViewHolder(viewHolder); } catch (Exception e) { throw new RuntimeException("Could not instantiate class " + defaultViewHolderClass); } } if (viewHolder.getContainerStyle() <= 0) { viewHolder.setContainerStyle(containerStyle); } if (viewHolder.getTreeView() == null) { viewHolder.setTreeViev(this); } return viewHolder; } private static void expand(final View v) { v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); final int targetHeight = v.getMeasuredHeight(); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LinearLayout.LayoutParams.WRAP_CONTENT : (int) (targetHeight * interpolatedTime); v.requestLayout(); } @Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration((int) (targetHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } private static void collapse(final View v) { final int initialHeight = v.getMeasuredHeight(); Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (interpolatedTime == 1) { v.setVisibility(View.GONE); } else { v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); v.requestLayout(); } } @Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } //----------------------------------------------------------------- //Add / Remove public void addNode(TreeNode parent, final TreeNode nodeToAdd) { parent.addChild(nodeToAdd); if (parent.isExpanded()) { final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent); addNode(parentViewHolder.getNodeItemsView(), nodeToAdd); } } public void removeNode(TreeNode node) { if (node.getParent() != null) { TreeNode parent = node.getParent(); int index = parent.deleteChild(node); if (parent.isExpanded() && index >= 0) { final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent); parentViewHolder.getNodeItemsView().removeViewAt(index); } } } }