diff --git a/README.md b/README.md index 89074e2..c3c389f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Tree view implementation for android + 3. Save state after rotation + 4. Selection mode for nodes + 5. Dynamic add/remove node ++ 6. Auto scroll to selected leaf ++ 7. Auto scroll to expanded node ### Known Limitations + For Android 4.0 (+/- nearest version) if you have too deep view hierarchy and with tree its easily possible, your app may crash diff --git a/app/src/main/java/com/unnamed/b/atv/sample/activity/MainActivity.java b/app/src/main/java/com/unnamed/b/atv/sample/activity/MainActivity.java index f74bea2..c62558e 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/activity/MainActivity.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/activity/MainActivity.java @@ -14,6 +14,7 @@ import com.unnamed.b.atv.sample.fragment.FolderStructureFragment; import com.unnamed.b.atv.sample.fragment.SelectableTreeFragment; import com.unnamed.b.atv.sample.fragment.TwoDScrollingArrowExpandFragment; +import com.unnamed.b.atv.sample.fragment.TwoDScrollingArrowExpandNodeFragment; import com.unnamed.b.atv.sample.fragment.TwoDScrollingFragment; import java.util.ArrayList; @@ -36,6 +37,7 @@ protected void onCreate(Bundle savedInstanceState) { listItems.put("Selectable Nodes", SelectableTreeFragment.class); listItems.put("2d scrolling", TwoDScrollingFragment.class); listItems.put("Expand with arrow only", TwoDScrollingArrowExpandFragment.class); + listItems.put("Expand with arrow one node only", TwoDScrollingArrowExpandNodeFragment.class); final List list = new ArrayList(listItems.keySet()); diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandFragment.java index bc64194..14a4264 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandFragment.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandFragment.java @@ -46,7 +46,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa tView.setDefaultNodeClickListener(TwoDScrollingArrowExpandFragment.this); tView.setDefaultViewHolder(ArrowExpandSelectableHeaderHolder.class); containerView.addView(tView.getView()); - tView.setUseAutoToggle(false); + tView.setExpansionAutoToggle(false); tView.expandAll(); diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandNodeFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandNodeFragment.java new file mode 100644 index 0000000..fccb279 --- /dev/null +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandNodeFragment.java @@ -0,0 +1,88 @@ +package com.unnamed.b.atv.sample.fragment; + +import android.app.Fragment; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.unnamed.b.atv.model.TreeNode; +import com.unnamed.b.atv.sample.R; +import com.unnamed.b.atv.sample.holder.ArrowExpandSelectableHeaderHolder; +import com.unnamed.b.atv.sample.holder.IconTreeItemHolder; +import com.unnamed.b.atv.view.AndroidTreeView; + +/** + * Created by Bogdan Melnychuk on 2/12/15 modified by Szigeti Peter 2/2/16. + */ +public class TwoDScrollingArrowExpandNodeFragment extends Fragment implements TreeNode.TreeNodeClickListener{ + private static final String NAME = "Very long name for folder"; + private AndroidTreeView tView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_selectable_nodes, null, false); + rootView.findViewById(R.id.status).setVisibility(View.GONE); + ViewGroup containerView = (ViewGroup) rootView.findViewById(R.id.container); + + TreeNode root = TreeNode.root(); + + TreeNode s1 = new TreeNode(new IconTreeItemHolder.IconTreeItem(R.string.ic_folder, "Folder with very long name ")).setViewHolder( + new ArrowExpandSelectableHeaderHolder(getActivity())); + TreeNode s2 = new TreeNode(new IconTreeItemHolder.IconTreeItem(R.string.ic_folder, "Another folder with very long name")).setViewHolder( + new ArrowExpandSelectableHeaderHolder(getActivity())); + + fillFolder(s1); + TreeNode nodeToExpand = fillFolder(s2); + + root.addChildren(s1, s2); + + tView = new AndroidTreeView(getActivity(), root); + tView.setDefaultAnimation(true); + tView.setUse2dScroll(true); + tView.setDefaultContainerStyle(R.style.TreeNodeStyleCustom); + tView.setDefaultNodeClickListener(TwoDScrollingArrowExpandNodeFragment.this); + tView.setDefaultViewHolder(ArrowExpandSelectableHeaderHolder.class); + containerView.addView(tView.getView()); + + tView.setAutoScrollToExpandedNode(true); + tView.setAutoScrollToSelectedLeafs(true); + tView.setLeafSelectionAutoToggle(true); + + tView.expandNode(s1); + tView.expandNodeIncludingParents(nodeToExpand, true); + + if (savedInstanceState != null) { + String state = savedInstanceState.getString("tState"); + if (!TextUtils.isEmpty(state)) { + tView.restoreState(state); + } + } + return rootView; + } + + private TreeNode fillFolder(TreeNode folder) { + TreeNode currentNode = folder; + TreeNode file = null; + for (int i = 0; i < 6; i++) { + file = new TreeNode(new IconTreeItemHolder.IconTreeItem(R.string.ic_folder, NAME + " " + i)); + currentNode.addChild(file); + currentNode = file; + } + return file; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("tState", tView.getSaveState()); + } + + @Override + public void onClick(TreeNode node, Object value) { + Toast toast = Toast.makeText(getActivity(), ((IconTreeItemHolder.IconTreeItem)value).text, Toast.LENGTH_SHORT); + toast.show(); + } +} diff --git a/app/src/main/java/com/unnamed/b/atv/sample/holder/ArrowExpandSelectableHeaderHolder.java b/app/src/main/java/com/unnamed/b/atv/sample/holder/ArrowExpandSelectableHeaderHolder.java index c58d084..841ab35 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/holder/ArrowExpandSelectableHeaderHolder.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/holder/ArrowExpandSelectableHeaderHolder.java @@ -42,7 +42,7 @@ public View createNodeView(final TreeNode node, IconTreeItemHolder.IconTreeItem arrowView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - tView.toggleNode(node); + tView.toggleNodeExpansion(node); } }); diff --git a/library/src/main/java/com/unnamed/b/atv/holder/SimpleViewHolder.java b/library/src/main/java/com/unnamed/b/atv/holder/SimpleViewHolder.java old mode 100644 new mode 100755 diff --git a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java old mode 100644 new mode 100755 index dbf33f8..394ffac --- a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java +++ b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java @@ -1,6 +1,7 @@ package com.unnamed.b.atv.model; import android.content.Context; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -97,6 +98,10 @@ public boolean isLeaf() { return size() == 0; } + public boolean isBranch() { + return size() > 0; + } + public Object getValue() { return mValue; } @@ -112,6 +117,9 @@ public TreeNode setExpanded(boolean expanded) { public void setSelected(boolean selected) { mSelected = selected; + if(mViewHolder != null) { + mViewHolder.toggleSelection(mSelected); + } } public boolean isSelected() { @@ -191,6 +199,18 @@ public BaseNodeViewHolder getViewHolder() { return mViewHolder; } + public boolean isInitialized() { + return mViewHolder != null ? mViewHolder.isInitialized() : false; + } + + public View getView() { + return mViewHolder != null ? mViewHolder.getView() : null; + } + + public Boolean hasView() { + return getView() != null; + } + public boolean isFirstChild() { if (!isRoot()) { List parentChildren = mParent.children; @@ -225,9 +245,11 @@ public static abstract class BaseNodeViewHolder { private View mView; protected int containerStyle; protected Context context; + protected LayoutInflater layoutInflater; public BaseNodeViewHolder(Context context) { this.context = context; + this.layoutInflater = LayoutInflater.from(context); } public View getView() { @@ -280,5 +302,9 @@ public void toggle(boolean active) { public void toggleSelectionMode(boolean editModeEnabled) { // empty } + + public void toggleSelection(boolean isSelected) { + + } } } diff --git a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java old mode 100644 new mode 100755 index 222a43a..77dcb35 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -5,6 +5,7 @@ import android.view.ContextThemeWrapper; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.LinearLayout; @@ -26,8 +27,8 @@ public class AndroidTreeView { private static final String NODES_PATH_SEPARATOR = ";"; - protected TreeNode mRoot; - private Context mContext; + protected Context mContext; + private TreeNode mRoot; private boolean applyForRoot; private int containerStyle = 0; private Class defaultViewHolderClass = SimpleViewHolder.class; @@ -36,7 +37,12 @@ public class AndroidTreeView { private boolean mSelectionModeEnabled; private boolean mUseDefaultAnimation = false; private boolean use2dScroll = false; - private boolean enableAutoToggle = true; + private boolean enableExpansionAutoToggle = true; + private boolean enableSelectionsAutoToggle = true; + private ViewGroup mRootView; + private TreeNode currentSelectedLeaf; + private boolean autoScrollToExpandedNode = true; + private boolean autoScrollToSelectedLeafs = false; public AndroidTreeView(Context context) { mContext = context; @@ -51,6 +57,14 @@ public AndroidTreeView(Context context, TreeNode root) { mContext = context; } + public void setAutoScrollToSelectedLeafs(boolean autoScrollToSelectedLeafs) { + this.autoScrollToSelectedLeafs = autoScrollToSelectedLeafs; + } + + public void setAutoScrollToExpandedNode(boolean autoScrollToExpandedNode) { + this.autoScrollToExpandedNode = autoScrollToExpandedNode; + } + public void setDefaultAnimation(boolean defaultAnimation) { this.mUseDefaultAnimation = defaultAnimation; } @@ -72,12 +86,16 @@ public boolean is2dScrollEnabled() { return use2dScroll; } - public void setUseAutoToggle(boolean enableAutoToggle) { - this.enableAutoToggle = enableAutoToggle; + public void setExpansionAutoToggle(boolean enableAutoToggle) { + this.enableExpansionAutoToggle = enableAutoToggle; } - public boolean isAutoToggleEnabled() { - return enableAutoToggle; + public boolean isExpansionAutoToggleEnabled() { + return enableExpansionAutoToggle; + } + + public void setLeafSelectionAutoToggle(boolean enableSelectionsAutoToggle) { + this.enableSelectionsAutoToggle = enableSelectionsAutoToggle; } public void setDefaultViewHolder(Class viewHolder) { @@ -104,12 +122,11 @@ public void collapseAll() { 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); + mRootView = use2dScroll ? new TwoDScrollView(newContext) : new ScrollView(newContext); } else { - view = use2dScroll ? new TwoDScrollView(mContext) : new ScrollView(mContext); + mRootView = use2dScroll ? new TwoDScrollView(mContext) : new ScrollView(mContext); } Context containerContext = mContext; @@ -120,7 +137,7 @@ public View getView(int style) { viewTreeItems.setId(R.id.tree_items); viewTreeItems.setOrientation(LinearLayout.VERTICAL); - view.addView(viewTreeItems); + mRootView.addView(viewTreeItems); mRoot.setViewHolder(new TreeNode.BaseNodeViewHolder(mContext) { @Override @@ -135,7 +152,7 @@ public ViewGroup getNodeItemsView() { }); expandNode(mRoot, false); - return view; + return mRootView; } public View getView() { @@ -150,12 +167,15 @@ public void expandLevel(int level) { } private void expandLevel(TreeNode node, int level) { + boolean lastAutoScrollEnabled = autoScrollToExpandedNode; + autoScrollToExpandedNode = false; if (node.getLevel() <= level) { expandNode(node, false); } for (TreeNode n : node.getChildren()) { expandLevel(n, level); } + autoScrollToExpandedNode = lastAutoScrollEnabled; } public void expandNode(TreeNode node) { @@ -203,13 +223,21 @@ private void getSaveState(TreeNode root, StringBuilder sBuilder) { } } - public void toggleNode(TreeNode node) { + public void toggleNodeExpansion(TreeNode node) { if (node.isExpanded()) { collapseNode(node, false); } else { expandNode(node, false); } + } + private void toggleLeafSelection(TreeNode node) { + if (node.isLeaf()) { + if (currentSelectedLeaf != null) { + selectNode(currentSelectedLeaf, false); + } + selectNode(node, !node.isSelected()); + } } private void collapseNode(TreeNode node, final boolean includeSubnodes) { @@ -251,6 +279,62 @@ private void expandNode(final TreeNode node, boolean includeSubnodes) { parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); } + if (node != mRoot) { + if ((node.isLeaf() && autoScrollToSelectedLeafs) || + (node.isBranch() && autoScrollToExpandedNode)) { + scrollToNode(node); + } + } + + } + + public void expandNodeIncludingParents(TreeNode node, boolean autoScroll) { + List parents = getParents(node); + for (TreeNode parentNode : parents) { + boolean lastAutoScrollEnabled = autoScrollToExpandedNode; + autoScrollToExpandedNode = false; + expandNode(parentNode); + autoScrollToExpandedNode = lastAutoScrollEnabled; + } + if (autoScroll) { + scrollToNode(node); + } + } + + private List getParents(TreeNode node) { + List parents = new ArrayList<>(); + TreeNode parent = node; + while (parent != mRoot) { + parents.add(0, parent); + parent = parent.getParent(); + } + return parents; + } + + public void scrollToNode(final TreeNode node) { + if (node.isInitialized()) { + if(node.getView().getHeight() == 0) { + node.getView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + node.getView().getViewTreeObserver().removeGlobalOnLayoutListener(this); + scrollToNode(node); + } + }); + return; + } + int yToScroll = ((int) node.getView().getY()); + ViewGroup parent = ((ViewGroup) node.getView().getParent()); + while (parent != mRootView) { + yToScroll += parent.getY(); + parent = ((ViewGroup) parent.getParent()); + } + if (mRootView instanceof TwoDScrollView) { + ((TwoDScrollView) mRootView).smoothScrollTo(0, yToScroll); + } else if (mRootView instanceof ScrollView) { + ((ScrollView) mRootView).smoothScrollTo(0, yToScroll); + } + } } private void addNode(ViewGroup container, final TreeNode n) { @@ -264,28 +348,24 @@ private void addNode(ViewGroup container, final TreeNode n) { nodeView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + toggleAll(n); if (n.getClickListener() != null) { n.getClickListener().onClick(n, n.getValue()); } else if (nodeClickListener != null) { nodeClickListener.onClick(n, n.getValue()); } - if (enableAutoToggle) { - toggleNode(n); - } } }); nodeView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { + toggleAll(n); if (n.getLongClickListener() != null) { return n.getLongClickListener().onLongClick(n, n.getValue()); } else if (nodeLongClickListener != null) { return nodeLongClickListener.onLongClick(n, n.getValue()); } - if (enableAutoToggle) { - toggleNode(n); - } return false; } }); @@ -294,6 +374,15 @@ public boolean onLongClick(View view) { //------------------------------------------------------------ // Selection methods + public void toggleAll(TreeNode node) { + if (enableExpansionAutoToggle) { + toggleNodeExpansion(node); + } + if (enableSelectionsAutoToggle) { + toggleLeafSelection(node); + } + } + public void setSelectionModeEnabled(boolean selectionModeEnabled) { if (!selectionModeEnabled) { // TODO fix double iteration over tree @@ -370,6 +459,13 @@ private void makeAllSelection(boolean selected, boolean skipCollapsed) { public void selectNode(TreeNode node, boolean selected) { if (mSelectionModeEnabled) { + if (node.isLeaf()) { + if (selected) { + currentSelectedLeaf = node; + } else if (node == currentSelectedLeaf) { + currentSelectedLeaf = null; + } + } node.setSelected(selected); toogleSelectionForNode(node, true); } diff --git a/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java b/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java old mode 100644 new mode 100755 diff --git a/library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java b/library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java old mode 100644 new mode 100755 index 298e060..ef638c1 --- a/library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java @@ -567,9 +567,9 @@ private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, bo * component is a good candidate for focus, this scrollview reclaims the * focus.

* - * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * @param direction the scroll direction: {@link View#FOCUS_UP} * to go the top of the view or - * {@link android.view.View#FOCUS_DOWN} to go the bottom + * {@link View#FOCUS_DOWN} to go the bottom * @return true if the key event is consumed by this method, false otherwise */ public boolean fullScroll(int direction, boolean horizontal) { @@ -610,9 +610,9 @@ public boolean fullScroll(int direction, boolean horizontal) { * to a component visible in this area. If no component can be focused in * the new visible area, the focus is reclaimed by this scrollview.

* - * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * @param direction the scroll direction: {@link View#FOCUS_UP} * to go upward - * {@link android.view.View#FOCUS_DOWN} to downward + * {@link View#FOCUS_DOWN} to downward * @param top the top offset of the new area to be made visible * @param bottom the bottom offset of the new area to be made visible * @return true if the key event is consumed by this method, false otherwise @@ -945,7 +945,7 @@ public void requestChildFocus(View child, View focused) { * When looking for focus in children of a scroll view, need to be a little * more careful not to give focus to something that is scrolled off screen. *

- * This is more expensive than the default {@link android.view.ViewGroup} + * This is more expensive than the default {@link ViewGroup} * implementation, otherwise this behavior might have been made the default. */ @Override