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 extends TreeNode.BaseNodeViewHolder> 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 extends TreeNode.BaseNodeViewHolder> 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