From 108527e0a56599161b99ac18d15e05acce915262 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Fri, 13 Mar 2015 11:33:14 -0500 Subject: [PATCH 01/28] expand/collapse animation added --- .../fragment/CustomViewHolderFragment.java | 1 + .../fragment/FolderStructureFragment.java | 1 + .../fragment/SelectableTreeFragment.java | 1 + .../unnamed/b/atv/view/AndroidTreeView.java | 92 ++++++++++++++++++- 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/CustomViewHolderFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/CustomViewHolderFragment.java index 1deacbc..77c96f4 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/fragment/CustomViewHolderFragment.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/CustomViewHolderFragment.java @@ -41,6 +41,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa root.addChildren(myProfile, bruce, barry, clark); tView = new AndroidTreeView(getActivity(), root); + tView.setDefaultAnimation(true); tView.setDefaultContainerStyle(R.style.TreeNodeStyleDivided, true); containerView.addView(tView.getView()); diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java index c0af924..dc55f1a 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java @@ -63,6 +63,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa root.addChildren(computerRoot); tView = new AndroidTreeView(getActivity(), root); + tView.setDefaultAnimation(true); tView.setDefaultContainerStyle(R.style.TreeNodeStyleCustom); tView.setDefaultViewHolder(IconTreeItemHolder.class); tView.setDefaultNodeClickListener(nodeClickListener); diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/SelectableTreeFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/SelectableTreeFragment.java index e07c31c..3a1a151 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/fragment/SelectableTreeFragment.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/SelectableTreeFragment.java @@ -93,6 +93,7 @@ public void onClick(View v) { root.addChildren(s1, s2); tView = new AndroidTreeView(getActivity(), root); + tView.setDefaultAnimation(true); containerView.addView(tView.getView()); if (savedInstanceState != null) { 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 index b4c3e20..919eab8 100644 --- 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,8 @@ 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; @@ -31,12 +33,17 @@ public class AndroidTreeView { private Class defaultViewHolderClass = SimpleViewHolder.class; private TreeNode.TreeNodeClickListener nodeClickListener; private boolean mSelectionModeEnabled; + private boolean mUseDefaultAnimation = 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); } @@ -177,7 +184,12 @@ private void toggleNode(TreeNode node) { private void collapseNode(TreeNode node, final boolean includeSubnodes) { node.setExpanded(false); TreeNode.BaseNodeViewHolder nodeViewHolder = getViewHolderForNode(node); - nodeViewHolder.getNodeItemsView().setVisibility(View.GONE); + + if(mUseDefaultAnimation) { + collapse(nodeViewHolder.getNodeItemsView()); + } else { + nodeViewHolder.getNodeItemsView().setVisibility(View.GONE); + } nodeViewHolder.toggle(false); if (includeSubnodes) { for (TreeNode n : node.getChildren()) { @@ -190,7 +202,8 @@ private void expandNode(final TreeNode node, boolean includeSubnodes) { node.setExpanded(true); final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(node); parentViewHolder.getNodeItemsView().removeAllViews(); - parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); + + parentViewHolder.toggle(true); for (final TreeNode n : node.getChildren()) { @@ -201,6 +214,12 @@ private void expandNode(final TreeNode node, boolean includeSubnodes) { } } + if(mUseDefaultAnimation) { + expand(parentViewHolder.getNodeItemsView()); + } else { + parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); + } + } private void addNode(ViewGroup container, final TreeNode n) { @@ -241,6 +260,22 @@ public void setSelectionModeEnabled(boolean selectionModeEnabled) { } + public List getSelectedValues(Class clazz) { + List result = new ArrayList<>(); + List 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()) { @@ -331,6 +366,59 @@ private TreeNode.BaseNodeViewHolder getViewHolderForNode(TreeNode node) { 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 From b25f5da939684ff2dc1a1a11bcd8c118f5e44f7f Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Fri, 13 Mar 2015 11:41:33 -0500 Subject: [PATCH 02/28] Version updated --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 18265eb..c5d7b56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.1 -VERSION_CODE=3 +VERSION_NAME=1.2.2 +VERSION_CODE=4 ANDROID_BUILD_MIN_SDK_VERSION=11 From 7ed986a56755ca62659bab6a3cd45ba697018eab Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Fri, 13 Mar 2015 11:56:33 -0500 Subject: [PATCH 03/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9002c82..5078146 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ AndroidTreeView ### Recent changes -Dynamic add/remove nodes. Added to maven central +Colapse/expand animation added ### Description From 926f5254e3a4e71ca4551f1e52e8db851b69f606 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Sat, 21 Mar 2015 13:44:52 -0500 Subject: [PATCH 04/28] Fixed restore state issue #7. (Ty Fahim) --- gradle.properties | 4 ++-- library/src/main/java/com/unnamed/b/atv/model/TreeNode.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index c5d7b56..542993a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.2 -VERSION_CODE=4 +VERSION_NAME=1.2.3 +VERSION_CODE=5 ANDROID_BUILD_MIN_SDK_VERSION=11 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 index f890038..5d64af9 100644 --- a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java +++ b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java @@ -129,7 +129,7 @@ public String getPath() { final StringBuilder path = new StringBuilder(); TreeNode node = this; while (node.mParent != null) { - path.append(mId); + path.append(node.getId()); node = node.mParent; if (node.mParent != null) { path.append(NODES_ID_SEPARATOR); From 15eb9d8f50af8a5c5e48b8062a71d6d76ef6f272 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Sat, 11 Apr 2015 22:22:01 +0300 Subject: [PATCH 05/28] Fixed issues #1 and #11 --- gradle.properties | 4 ++-- .../unnamed/b/atv/view/AndroidTreeView.java | 24 +++++++++---------- .../b/atv/view/TreeNodeWrapperView.java | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/gradle.properties b/gradle.properties index 542993a..69f5789 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.3 -VERSION_CODE=5 +VERSION_NAME=1.2.4 +VERSION_CODE=6 ANDROID_BUILD_MIN_SDK_VERSION=11 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 index 919eab8..50f251b 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -85,7 +85,7 @@ public View getView(int style) { if (containerStyle != 0 && applyForRoot) { containerContext = new ContextThemeWrapper(mContext, containerStyle); } - final LinearLayout viewTreeItems = new LinearLayout(containerContext); + final LinearLayout viewTreeItems = new LinearLayout(containerContext, null, containerStyle); viewTreeItems.setId(R.id.tree_items); viewTreeItems.setOrientation(LinearLayout.VERTICAL); @@ -185,7 +185,7 @@ private void collapseNode(TreeNode node, final boolean includeSubnodes) { node.setExpanded(false); TreeNode.BaseNodeViewHolder nodeViewHolder = getViewHolderForNode(node); - if(mUseDefaultAnimation) { + if (mUseDefaultAnimation) { collapse(nodeViewHolder.getNodeItemsView()); } else { nodeViewHolder.getNodeItemsView().setVisibility(View.GONE); @@ -214,7 +214,7 @@ private void expandNode(final TreeNode node, boolean includeSubnodes) { } } - if(mUseDefaultAnimation) { + if (mUseDefaultAnimation) { expand(parentViewHolder.getNodeItemsView()); } else { parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); @@ -372,13 +372,12 @@ private static void expand(final View v) { v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); - Animation a = new Animation() - { + Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LinearLayout.LayoutParams.WRAP_CONTENT - : (int)(targetHeight * interpolatedTime); + : (int) (targetHeight * interpolatedTime); v.requestLayout(); } @@ -389,21 +388,20 @@ public boolean willChangeBounds() { }; // 1dp/ms - a.setDuration((int)(targetHeight / v.getContext().getResources().getDisplayMetrics().density)); + 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() - { + Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { - if(interpolatedTime == 1){ + if (interpolatedTime == 1) { v.setVisibility(View.GONE); - }else{ - v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime); + } else { + v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); v.requestLayout(); } } @@ -415,7 +413,7 @@ public boolean willChangeBounds() { }; // 1dp/ms - a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density)); + a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } 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 index d2c8182..21f7362 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java @@ -31,7 +31,7 @@ private void init() { nodeContainer.setId(R.id.node_header); ContextThemeWrapper newContext = new ContextThemeWrapper(getContext(), containerStyle); - nodeItemsContainer = new LinearLayout(newContext); + nodeItemsContainer = new LinearLayout(newContext, null, containerStyle); nodeItemsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); nodeItemsContainer.setId(R.id.node_items); nodeItemsContainer.setOrientation(LinearLayout.VERTICAL); From 654b4ac79ec3d81cd670cb0f3f5c274dda9fbaee Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Sun, 26 Apr 2015 20:01:04 +0300 Subject: [PATCH 06/28] Selectable background added --- app/src/main/res/layout/layout_header_node.xml | 1 + app/src/main/res/layout/layout_icon_node.xml | 1 + app/src/main/res/layout/layout_place_node.xml | 1 + app/src/main/res/layout/layout_profile_node.xml | 1 + app/src/main/res/layout/layout_selectable_header.xml | 1 + app/src/main/res/layout/layout_selectable_item.xml | 1 + app/src/main/res/layout/layout_social_node.xml | 1 + 7 files changed, 7 insertions(+) diff --git a/app/src/main/res/layout/layout_header_node.xml b/app/src/main/res/layout/layout_header_node.xml index 686bc0a..1423434 100644 --- a/app/src/main/res/layout/layout_header_node.xml +++ b/app/src/main/res/layout/layout_header_node.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:minHeight="48dp" android:paddingLeft="10dp" + android:background="?android:attr/selectableItemBackground" android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/layout_icon_node.xml b/app/src/main/res/layout/layout_icon_node.xml index 07b124e..41434d4 100644 --- a/app/src/main/res/layout/layout_icon_node.xml +++ b/app/src/main/res/layout/layout_icon_node.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:minHeight="48dp" + android:background="?android:attr/selectableItemBackground" android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/layout_place_node.xml b/app/src/main/res/layout/layout_place_node.xml index f99e215..89b1ec6 100644 --- a/app/src/main/res/layout/layout_place_node.xml +++ b/app/src/main/res/layout/layout_place_node.xml @@ -5,6 +5,7 @@ android:minHeight="48dp" android:paddingLeft="10dp" android:paddingRight="10dp" + android:background="?android:attr/selectableItemBackground" android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/layout_profile_node.xml b/app/src/main/res/layout/layout_profile_node.xml index 94b65f8..aeb7a09 100644 --- a/app/src/main/res/layout/layout_profile_node.xml +++ b/app/src/main/res/layout/layout_profile_node.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:minHeight="56dp" android:paddingLeft="10dp" + android:background="?android:attr/selectableItemBackground" android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/layout_selectable_header.xml b/app/src/main/res/layout/layout_selectable_header.xml index 319a50a..d326bff 100644 --- a/app/src/main/res/layout/layout_selectable_header.xml +++ b/app/src/main/res/layout/layout_selectable_header.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:minHeight="48dp" android:paddingLeft="10dp" + android:background="?android:attr/selectableItemBackground" android:layout_height="match_parent"> From 07318faa1eda83a726ed4a066e6f8b360bab932f Mon Sep 17 00:00:00 2001 From: Michael Allon Date: Mon, 25 May 2015 16:23:41 -0500 Subject: [PATCH 07/28] Update README to include a project using the library. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5078146..a8dfb30 100644 --- a/README.md +++ b/README.md @@ -105,4 +105,6 @@ For more details use sample application as example Let me know if i missed something, appreciate your support, thanks! -If you are using this library I can post link to your project here! +### Projects using this library + +[Blue Dot : World Chat](https://play.google.com/store/apps/details?id=com.commandapps.bluedot) From 42d8bbe924ff2273c69a556a1bc2c0df4069ea2e Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Tue, 2 Jun 2015 12:01:37 +0300 Subject: [PATCH 08/28] Fix for issue #4. 2D scrolling mode added, keep in mind this comes with few limitations: you won't be able not place views on right side like alignParentRight. Everything should be align left. Is not enabled by default --- .../b/atv/sample/activity/MainActivity.java | 2 + .../fragment/TwoDScrollingFragment.java | 70 ++ .../res/layout/layout_selectable_header.xml | 1 + .../unnamed/b/atv/view/AndroidTreeView.java | 15 +- .../unnamed/b/atv/view/TwoDScrollView.java | 1110 +++++++++++++++++ 5 files changed, 1195 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingFragment.java create mode 100644 library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java 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 cd7eebd..4029a26 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 @@ -13,6 +13,7 @@ import com.unnamed.b.atv.sample.fragment.CustomViewHolderFragment; import com.unnamed.b.atv.sample.fragment.FolderStructureFragment; import com.unnamed.b.atv.sample.fragment.SelectableTreeFragment; +import com.unnamed.b.atv.sample.fragment.TwoDScrollingFragment; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -32,6 +33,7 @@ protected void onCreate(Bundle savedInstanceState) { listItems.put("Folder Structure Example", FolderStructureFragment.class); listItems.put("Custom Holder Example", CustomViewHolderFragment.class); listItems.put("Selectable Nodes", SelectableTreeFragment.class); + listItems.put("2d scrolling", TwoDScrollingFragment.class); final List list = new ArrayList(listItems.keySet()); diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingFragment.java new file mode 100644 index 0000000..37d62fe --- /dev/null +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingFragment.java @@ -0,0 +1,70 @@ +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 com.unnamed.b.atv.model.TreeNode; +import com.unnamed.b.atv.sample.R; +import com.unnamed.b.atv.sample.holder.IconTreeItemHolder; +import com.unnamed.b.atv.sample.holder.SelectableHeaderHolder; +import com.unnamed.b.atv.view.AndroidTreeView; + +/** + * Created by Bogdan Melnychuk on 2/12/15. + */ +public class TwoDScrollingFragment extends Fragment { + 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 SelectableHeaderHolder(getActivity())); + TreeNode s2 = new TreeNode(new IconTreeItemHolder.IconTreeItem(R.string.ic_folder, "Another folder with very long name")).setViewHolder(new SelectableHeaderHolder(getActivity())); + + fillFolder(s1); + fillFolder(s2); + + root.addChildren(s1, s2); + + tView = new AndroidTreeView(getActivity(), root); + tView.setDefaultAnimation(true); + tView.setUse2dScroll(true); + tView.setDefaultContainerStyle(R.style.TreeNodeStyleCustom); + containerView.addView(tView.getView()); + + tView.expandAll(); + + if (savedInstanceState != null) { + String state = savedInstanceState.getString("tState"); + if (!TextUtils.isEmpty(state)) { + tView.restoreState(state); + } + } + return rootView; + } + + private void fillFolder(TreeNode folder) { + TreeNode currentNode = folder; + for (int i = 0; i < 10; i++) { + TreeNode file = new TreeNode(new IconTreeItemHolder.IconTreeItem(R.string.ic_folder, NAME)).setViewHolder(new SelectableHeaderHolder(getActivity())); + currentNode.addChild(file); + currentNode = file; + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString("tState", tView.getSaveState()); + } +} diff --git a/app/src/main/res/layout/layout_selectable_header.xml b/app/src/main/res/layout/layout_selectable_header.xml index d326bff..d565078 100644 --- a/app/src/main/res/layout/layout_selectable_header.xml +++ b/app/src/main/res/layout/layout_selectable_header.xml @@ -44,6 +44,7 @@ app:iconSize="24dp" /> viewHolder) { defaultViewHolderClass = viewHolder; } @@ -73,12 +82,12 @@ public void collapseAll() { public View getView(int style) { - final ScrollView view; + final ViewGroup view; if (style > 0) { ContextThemeWrapper newContext = new ContextThemeWrapper(mContext, style); - view = new ScrollView(newContext); + view = use2dScroll ? new TwoDScrollView(newContext) : new ScrollView(newContext); } else { - view = new ScrollView(mContext); + view = use2dScroll ? new TwoDScrollView(mContext) : new ScrollView(mContext); } Context containerContext = mContext; 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 new file mode 100644 index 0000000..298e060 --- /dev/null +++ b/library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java @@ -0,0 +1,1110 @@ +package com.unnamed.b.atv.view; + + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Scroller; +import android.widget.TextView; + +import java.util.List; + +/** + * Layout container for a view hierarchy that can be scrolled by the user, + * allowing it to be larger than the physical display. A TwoDScrollView + * is a {@link FrameLayout}, meaning you should place one child in it + * containing the entire contents to scroll; this child may itself be a layout + * manager with a complex hierarchy of objects. A child that is often used + * is a {@link LinearLayout} in a vertical orientation, presenting a vertical + * array of top-level items that the user can scroll through. + *

+ *

The {@link TextView} class also + * takes care of its own scrolling, so does not require a TwoDScrollView, but + * using the two together is possible to achieve the effect of a text view + * within a larger container. + */ +public class TwoDScrollView extends FrameLayout { + static final int ANIMATED_SCROLL_GAP = 250; + static final float MAX_SCROLL_FACTOR = 0.5f; + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private Scroller mScroller; + + /** + * Flag to indicate that we are moving focus ourselves. This is so the + * code that watches for focus changes initiated outside this TwoDScrollView + * knows that it does not have to do anything. + */ + private boolean mTwoDScrollViewMovedFocus; + + /** + * Position of the last motion event. + */ + private float mLastMotionY; + private float mLastMotionX; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this TwoDScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * Whether arrow scrolling is animated. + */ + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + public TwoDScrollView(Context context) { + super(context); + initTwoDScrollView(); + } + + public TwoDScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + initTwoDScrollView(); + } + + public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initTwoDScrollView(); + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + if (getScrollY() < length) { + return getScrollY() / (float) length; + } + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + return 1.0f; + } + + @Override + protected float getLeftFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + if (getScrollX() < length) { + return getScrollX() / (float) length; + } + return 1.0f; + } + + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (span < length) { + return span / (float) length; + } + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmountVertical() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + public int getMaxScrollAmountHorizontal() { + return (int) (MAX_SCROLL_FACTOR * getWidth()); + } + + private void initTwoDScrollView() { + mScroller = new Scroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index, params); + } + + /** + * @return Returns true this TwoDScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + int childWidth = child.getWidth(); + return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) || + (getWidth() < childWidth + getPaddingLeft() + getPaddingRight()); + } + return false; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + boolean handled = super.dispatchKeyEvent(event); + if (handled) { + return true; + } + return executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + if (!canScroll()) { + if (isFocused()) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); + return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP, false); + } else { + handled = fullScroll(View.FOCUS_UP, false); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN, false); + } else { + handled = fullScroll(View.FOCUS_DOWN, false); + } + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_LEFT, true); + } else { + handled = fullScroll(View.FOCUS_LEFT, true); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_RIGHT, true); + } else { + handled = fullScroll(View.FOCUS_RIGHT, true); + } + break; + } + } + return handled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + * + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + if (!canScroll()) { + mIsBeingDragged = false; + return false; + } + final float y = ev.getY(); + final float x = ev.getX(); + switch (action) { + case MotionEvent.ACTION_MOVE: + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int yDiff = (int) Math.abs(y - mLastMotionY); + final int xDiff = (int) Math.abs(x - mLastMotionX); + if (yDiff > mTouchSlop || xDiff > mTouchSlop) { + mIsBeingDragged = true; + } + break; + + case MotionEvent.ACTION_DOWN: + /* Remember location of down touch */ + mLastMotionY = y; + mLastMotionX = x; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (!canScroll()) { + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + final float y = ev.getY(); + final float x = ev.getX(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = y; + mLastMotionX = x; + break; + case MotionEvent.ACTION_MOVE: + // Scroll to follow the motion event + int deltaX = (int) (mLastMotionX - x); + int deltaY = (int) (mLastMotionY - y); + mLastMotionX = x; + mLastMotionY = y; + + if (deltaX < 0) { + if (getScrollX() < 0) { + deltaX = 0; + } + } else if (deltaX > 0) { + final int rightEdge = getWidth() - getPaddingRight(); + final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (availableToScroll > 0) { + deltaX = Math.min(availableToScroll, deltaX); + } else { + deltaX = 0; + } + } + if (deltaY < 0) { + if (getScrollY() < 0) { + deltaY = 0; + } + } else if (deltaY > 0) { + final int bottomEdge = getHeight() - getPaddingBottom(); + final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (availableToScroll > 0) { + deltaY = Math.min(availableToScroll, deltaY); + } else { + deltaY = 0; + } + } + if (deltaY != 0 || deltaX != 0) + scrollBy(deltaX, deltaY); + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int) velocityTracker.getXVelocity(); + int initialYVelocity = (int) velocityTracker.getYVelocity(); + if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && getChildCount() > 0) { + fling(-initialXVelocity, -initialYVelocity); + } + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + return true; + } + + /** + * Finds the next focusable component that fits in this View's bounds + * (excluding fading edges) pretending that this View's top is located at + * the parameter top. + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found (the fading edge is assumed to start at this position) + * @param preferredFocusable the View that has highest priority and will be + * returned if it is within my bounds (null is valid) + * @return the next focusable component in the bounds or null if none can be + * found + */ + private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) { + /* + * The fading edge's transparent side should be considered for focus + * since it's mostly visible, so we divide the actual fading edge length + * by 2. + */ + final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2; + final int topWithoutFadingEdge = top + verticalFadingEdgeLength; + final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength; + final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2; + final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength; + final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength; + + if ((preferredFocusable != null) + && (preferredFocusable.getTop() < bottomWithoutFadingEdge) + && (preferredFocusable.getBottom() > topWithoutFadingEdge) + && (preferredFocusable.getLeft() < rightWithoutFadingEdge) + && (preferredFocusable.getRight() > leftWithoutFadingEdge)) { + return preferredFocusable; + } + return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge); + } + + /** + * Finds the next focusable component that fits in the specified bounds. + *

+ * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) { + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + int viewLeft = view.getLeft(); + int viewRight = view.getRight(); + + if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && (left < viewLeft) && (viewRight < right); + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToVerticalBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate.getBottom()); + final boolean viewIsCloserToHorizontalBoundary = + (leftFocus && viewLeft < focusCandidate.getLeft()) || + (!leftFocus && viewRight > focusCandidate.getRight()); + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + return focusCandidate; + } + + /** + *

Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.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) { + if (!horizontal) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + mTempRect.top = 0; + mTempRect.bottom = height; + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0); + } else { + boolean right = direction == View.FOCUS_DOWN; + int width = getWidth(); + mTempRect.left = 0; + mTempRect.right = width; + if (right) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.right = view.getBottom(); + mTempRect.left = mTempRect.right - width; + } + } + return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom); + } + } + + /** + *

Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * 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} + * to go upward + * {@link android.view.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 + */ + private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) { + boolean handled = true; + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = directionY == View.FOCUS_UP; + int width = getWidth(); + int containerLeft = getScrollX(); + int containerRight = containerLeft + width; + boolean leftwards = directionX == View.FOCUS_UP; + View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right); + if (newFocused == null) { + newFocused = this; + } + if ((top >= containerTop && bottom <= containerBottom) || (left >= containerLeft && right <= containerRight)) { + handled = false; + } else { + int deltaY = up ? (top - containerTop) : (bottom - containerBottom); + int deltaX = leftwards ? (left - containerLeft) : (right - containerRight); + doScroll(deltaX, deltaY); + } + if (newFocused != findFocus() && newFocused.requestFocus(directionY)) { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction, boolean horizontal) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical(); + + if (!horizontal) { + if (nextFocused != null) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(0, scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + } else { + if (nextFocused != null) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta, 0); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0); + } + } + return true; + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScroll(int deltaX, int deltaY) { + if (deltaX != 0 || deltaY != 0) { + smoothScrollBy(deltaX, deltaY); + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + mScroller.startScroll(getScrollX(), getScrollY(), dx, dy); + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

The scroll range of a scroll view is the overall height of all of its + * children.

+ */ + @Override + protected int computeVerticalScrollRange() { + int count = getChildCount(); + return count == 0 ? getHeight() : (getChildAt(0)).getBottom(); + } + + @Override + protected int computeHorizontalScrollRange() { + int count = getChildCount(); + return count == 0 ? getWidth() : (getChildAt(0)).getRight(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + // This is called at drawing time by ViewGroup. We don't want to + // re-show the scrollbars at this point, which scrollTo will do, + // so we replicate most of scrollTo here. + // + // It's a little odd to call onScrollChanged from inside the drawing. + // + // It is, except when you remember that computeScroll() is used to + // animate scrolling. So unless we want to defer the onScrollChanged() + // until the end of the animated scrolling, we don't really have a + // choice here. + // + // I agree. The alternative, which I think would be worse, is to post + // something and tell the subclasses later. This is bad because there + // will be a window where mScrollX/Y is different from what the app + // thinks it is. + // + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()), + clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight())); + } else { + scrollTo(x, y); + } + if (oldX != getScrollX() || oldY != getScrollY()) { + onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); + } + + // Keep on drawing until the animation has finished. + postInvalidate(); + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + /* Offset from child's local coordinates to TwoDScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + int fadingEdge = getVerticalFadingEdgeLength(); + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + int scrollYDelta = 0; + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mTwoDScrollViewMovedFocus) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + } + super.requestChildFocus(child, 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} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null ? + FocusFinder.getInstance().findNextFocus(this, null, direction) : + FocusFinder.getInstance().findNextFocusFromRect(this, + previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + // Calling this with the present values causes it to re-clam them + scrollTo(getScrollX(), getScrollY()); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDeltaX, scrollDeltaY); + } + + /** + * Return true if child is an descendant of parent, (or equal to the parent). + */ + private boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/curor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + int width = getWidth() - getPaddingRight() - getPaddingLeft(); + int right = getChildAt(0).getWidth(); + + mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height); + + final boolean movingDown = velocityY > 0; + final boolean movingRight = velocityX > 0; + + View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus()); + if (newFocused == null) { + newFocused = this; + } + + if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } + } + + /** + * {@inheritDoc} + *

+ *

This version also clamps the scrolling to the bounds of our child. + */ + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } +} From aac03b522951e240ca98227ea635bc966ae47cb0 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Tue, 2 Jun 2015 12:04:22 +0300 Subject: [PATCH 09/28] readme, versions --- README.md | 3 ++- gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a8dfb30..3754363 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ AndroidTreeView ### Recent changes -Colapse/expand animation added + +2D scrolling mode added, keep in mind this comes with few limitations: you won't be able not place views on right side like alignParentRight. Everything should be align left. Is not enabled by default ### Description diff --git a/gradle.properties b/gradle.properties index 69f5789..6bb8610 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.4 -VERSION_CODE=6 +VERSION_NAME=1.2.5 +VERSION_CODE=7 ANDROID_BUILD_MIN_SDK_VERSION=11 From a9a83a1c26c59755787999710b4cebf15eee7616 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Thu, 18 Jun 2015 11:53:40 +0300 Subject: [PATCH 10/28] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3754363..606f2ca 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Tree view implementation for android + ### Integration From 185fb1cf612c5844ced7e3c1a3e4f305a2bf2c9d Mon Sep 17 00:00:00 2001 From: mathiasberwig Date: Tue, 1 Sep 2015 10:56:09 -0300 Subject: [PATCH 11/28] added long click listener to treeview and folder structure example --- .../fragment/FolderStructureFragment.java | 9 ++++++++ .../com/unnamed/b/atv/model/TreeNode.java | 21 +++++++++++++++---- .../unnamed/b/atv/view/AndroidTreeView.java | 19 +++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java index dc55f1a..49ca252 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java @@ -67,6 +67,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa tView.setDefaultContainerStyle(R.style.TreeNodeStyleCustom); tView.setDefaultViewHolder(IconTreeItemHolder.class); tView.setDefaultNodeClickListener(nodeClickListener); + tView.setDefaultNodeLongClickListener(nodeLongClickListener); containerView.addView(tView.getView()); @@ -119,6 +120,14 @@ public void onClick(TreeNode node, Object value) { } }; + private TreeNode.TreeNodeLongClickListener nodeLongClickListener = new TreeNode.TreeNodeLongClickListener() { + @Override + public void onLongClick(TreeNode node, Object value) { + IconTreeItemHolder.IconTreeItem item = (IconTreeItemHolder.IconTreeItem) value; + Toast.makeText(getActivity(), "Long click: " + item.text, Toast.LENGTH_SHORT).show(); + } + }; + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); 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 index 5d64af9..b16d8e5 100644 --- a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java +++ b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java @@ -25,7 +25,8 @@ public class TreeNode { private boolean mSelectable = true; private final List children; private BaseNodeViewHolder mViewHolder; - private TreeNodeClickListener mListener; + private TreeNodeClickListener mClickListener; + private TreeNodeLongClickListener mLongClickListener; private Object mValue; private boolean mExpanded; @@ -169,13 +170,21 @@ public TreeNode setViewHolder(BaseNodeViewHolder viewHolder) { } public TreeNode setClickListener(TreeNodeClickListener listener) { - mListener = listener; + mClickListener = listener; return this; } - public TreeNodeClickListener getClickListener() { - return this.mListener; + return this.mClickListener; + } + + public TreeNode setLongClickListener(TreeNodeLongClickListener listener) { + mLongClickListener = listener; + return this; + } + + public TreeNodeLongClickListener getLongClickListener() { + return mLongClickListener; } public BaseNodeViewHolder getViewHolder() { @@ -206,6 +215,10 @@ public interface TreeNodeClickListener { void onClick(TreeNode node, Object value); } + public interface TreeNodeLongClickListener { + void onLongClick(TreeNode node, Object value); + } + public static abstract class BaseNodeViewHolder { protected AndroidTreeView tView; protected TreeNode mNode; 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 index 8c731da..0d596ce 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -32,6 +32,7 @@ public class AndroidTreeView { private int containerStyle = 0; private Class defaultViewHolderClass = SimpleViewHolder.class; private TreeNode.TreeNodeClickListener nodeClickListener; + private TreeNode.TreeNodeLongClickListener nodeLongClickListener; private boolean mSelectionModeEnabled; private boolean mUseDefaultAnimation = false; private boolean use2dScroll = false; @@ -70,6 +71,10 @@ public void setDefaultNodeClickListener(TreeNode.TreeNodeClickListener listener) nodeClickListener = listener; } + public void setDefaultNodeLongClickListener(TreeNode.TreeNodeLongClickListener listener) { + nodeLongClickListener = listener; + } + public void expandAll() { expandNode(mRoot, true); } @@ -250,6 +255,20 @@ public void onClick(View v) { toggleNode(n); } }); + + nodeView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (n.getLongClickListener() != null) { + n.getLongClickListener().onLongClick(n, n.getValue()); + return true; + } else if (nodeLongClickListener != null) { + nodeLongClickListener.onLongClick(n, n.getValue()); + return true; + } + return false; + } + }); } From 125033b6a2420fb954a90ed62ccbe3809969e2fd Mon Sep 17 00:00:00 2001 From: mathiasberwig Date: Tue, 1 Sep 2015 13:44:49 -0300 Subject: [PATCH 12/28] added long click listener to treeview and folder structure example --- .../main/java/com/unnamed/b/atv/view/AndroidTreeView.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 index 0d596ce..b56e138 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -260,11 +260,9 @@ public void onClick(View v) { @Override public boolean onLongClick(View view) { if (n.getLongClickListener() != null) { - n.getLongClickListener().onLongClick(n, n.getValue()); - return true; + return n.getLongClickListener().onLongClick(n, n.getValue()); } else if (nodeLongClickListener != null) { - nodeLongClickListener.onLongClick(n, n.getValue()); - return true; + return nodeLongClickListener.onLongClick(n, n.getValue()); } return false; } From 5fbcb2c86df2e1577034940d5e84244618a58135 Mon Sep 17 00:00:00 2001 From: mathiasberwig Date: Tue, 1 Sep 2015 13:45:39 -0300 Subject: [PATCH 13/28] added long click listener to treeview and folder structure example --- library/src/main/java/com/unnamed/b/atv/model/TreeNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b16d8e5..14958f1 100644 --- a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java +++ b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java @@ -216,7 +216,7 @@ public interface TreeNodeClickListener { } public interface TreeNodeLongClickListener { - void onLongClick(TreeNode node, Object value); + boolean onLongClick(TreeNode node, Object value); } public static abstract class BaseNodeViewHolder { From a9799b698c193985145df918818decd960950722 Mon Sep 17 00:00:00 2001 From: Mathias Berwig Date: Mon, 14 Sep 2015 11:34:01 -0300 Subject: [PATCH 14/28] improved id generation. fix #20 --- .../src/main/java/com/unnamed/b/atv/model/TreeNode.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 index 14958f1..315288c 100644 --- a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java +++ b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java @@ -20,6 +20,7 @@ public class TreeNode { public static final String NODES_ID_SEPARATOR = ":"; private int mId; + private int mLastId; private TreeNode mParent; private boolean mSelected; private boolean mSelectable = true; @@ -36,6 +37,10 @@ public static TreeNode root() { return root; } + private int generateId() { + return ++mLastId; + } + public TreeNode(Object value) { children = new ArrayList<>(); mValue = value; @@ -43,8 +48,7 @@ public TreeNode(Object value) { public TreeNode addChild(TreeNode childNode) { childNode.mParent = this; - //TODO think about id generation - childNode.mId = size(); + childNode.mId = generateId(); children.add(childNode); return this; } From fcc7e72f9911e591cee5a17a361d54f0ccf925b1 Mon Sep 17 00:00:00 2001 From: Mathias Berwig Date: Mon, 14 Sep 2015 11:35:40 -0300 Subject: [PATCH 15/28] removed redundant array creation --- .../src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index b56e138..7234208 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -269,7 +269,6 @@ public boolean onLongClick(View view) { }); } - //------------------------------------------------------------ // Selection methods @@ -376,7 +375,7 @@ 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}); + final Object object = defaultViewHolderClass.getConstructor(Context.class).newInstance(mContext); viewHolder = (TreeNode.BaseNodeViewHolder) object; node.setViewHolder(viewHolder); } catch (Exception e) { From 223a2d056706ef6374fa7bc5d3f318c61642a564 Mon Sep 17 00:00:00 2001 From: Mathias Berwig Date: Mon, 14 Sep 2015 11:36:36 -0300 Subject: [PATCH 16/28] simplified statement --- library/src/main/java/com/unnamed/b/atv/model/TreeNode.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 index 315288c..dbf33f8 100644 --- a/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java +++ b/library/src/main/java/com/unnamed/b/atv/model/TreeNode.java @@ -115,11 +115,7 @@ public void setSelected(boolean selected) { } public boolean isSelected() { - if (mSelectable) { - return mSelected; - } else { - return false; - } + return mSelectable && mSelected; } public void setSelectable(boolean selectable) { From d48cbc6539b4f3236c03a3d1f3b225489e8064b5 Mon Sep 17 00:00:00 2001 From: Mathias Berwig Date: Mon, 14 Sep 2015 11:39:10 -0300 Subject: [PATCH 17/28] corrected return type to match interface --- .../b/atv/sample/fragment/FolderStructureFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java b/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java index 49ca252..a59790c 100644 --- a/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/FolderStructureFragment.java @@ -78,7 +78,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa } } - return rootView; } @@ -122,9 +121,10 @@ public void onClick(TreeNode node, Object value) { private TreeNode.TreeNodeLongClickListener nodeLongClickListener = new TreeNode.TreeNodeLongClickListener() { @Override - public void onLongClick(TreeNode node, Object value) { + public boolean onLongClick(TreeNode node, Object value) { IconTreeItemHolder.IconTreeItem item = (IconTreeItemHolder.IconTreeItem) value; Toast.makeText(getActivity(), "Long click: " + item.text, Toast.LENGTH_SHORT).show(); + return true; } }; From 839ee2fc9386b81d8711c8c3a41e6bfdf6c01ab5 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Thu, 24 Sep 2015 10:17:19 +0300 Subject: [PATCH 18/28] Version updated --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 6bb8610..ec56e5c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.5 -VERSION_CODE=7 +VERSION_NAME=1.2.6 +VERSION_CODE=8 ANDROID_BUILD_MIN_SDK_VERSION=11 From 7e61bd9afee2f1eda453b337c29bc57d6c9e4e0c Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Thu, 24 Sep 2015 10:28:19 +0300 Subject: [PATCH 19/28] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 606f2ca..30aac26 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ Tree view implementation for android + 4. Selection mode for nodes + 5. Dynamic add/remove 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 + +
+
+ From 3927d0d61b8103e799823c41e5b4877162639d5c Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Thu, 5 Nov 2015 12:58:06 +0200 Subject: [PATCH 20/28] #38 removed allowBackup in library --- gradle.properties | 4 ++-- library/src/main/AndroidManifest.xml | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index ec56e5c..87aebb0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.6 -VERSION_CODE=8 +VERSION_NAME=1.2.7 +VERSION_CODE=9 ANDROID_BUILD_MIN_SDK_VERSION=11 diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 5e40d2f..0dec550 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + - + From 5948d4d4a58f5ca070900b15cd1db5eea898e8ab Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Mon, 28 Dec 2015 14:10:22 +0200 Subject: [PATCH 21/28] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30aac26..eac8970 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Tree view implementation for android [![AndroidTreeView Demo on Google Play Store](http://developer.android.com/images/brand/en_generic_rgb_wo_60.png)](https://play.google.com/store/apps/details?id=com.unnamed.b.atv.demo) -### Featrues +### Features + 1. N - level expandable/collapsable tree + 2. Custom values, views, styles for nodes + 3. Save state after rotation From ea56075d0ce3a5790a62a4d867f1fabe915dbaae Mon Sep 17 00:00:00 2001 From: Ye He Date: Fri, 22 Jan 2016 14:43:43 +1100 Subject: [PATCH 22/28] Expose fields for subclass. --- .../java/com/unnamed/b/atv/view/AndroidTreeView.java | 10 +++++++++- .../com/unnamed/b/atv/view/TreeNodeWrapperView.java | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) 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 index 7234208..d56241a 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -26,7 +26,7 @@ public class AndroidTreeView { private static final String NODES_PATH_SEPARATOR = ";"; - private TreeNode mRoot; + protected TreeNode mRoot; private Context mContext; private boolean applyForRoot; private int containerStyle = 0; @@ -37,6 +37,14 @@ public class AndroidTreeView { private boolean mUseDefaultAnimation = false; private boolean use2dScroll = false; + public AndroidTreeView(Context context) { + mContext = context; + } + + public void setRoot(TreeNode mRoot) { + this.mRoot = mRoot; + } + public AndroidTreeView(Context context, TreeNode root) { mRoot = root; mContext = context; 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 index 21f7362..b660563 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java @@ -46,4 +46,7 @@ public void insertNodeView(View nodeView) { nodeContainer.addView(nodeView); } + public ViewGroup getNodeContainer() { + return nodeContainer; + } } From e0fd5bc741ec8b8a57c566182269b3b908242f58 Mon Sep 17 00:00:00 2001 From: SzigetiPeter Date: Tue, 2 Feb 2016 11:44:39 +0200 Subject: [PATCH 23/28] Added disable for auto tree node toggle, and test use case --- .../b/atv/sample/activity/MainActivity.java | 2 + .../TwoDScrollingArrowExpandFragment.java | 82 +++++++++++++++++++ .../ArrowExpandSelectableHeaderHolder.java | 74 +++++++++++++++++ .../unnamed/b/atv/view/AndroidTreeView.java | 18 +++- 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandFragment.java create mode 100644 app/src/main/java/com/unnamed/b/atv/sample/holder/ArrowExpandSelectableHeaderHolder.java 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 4029a26..f74bea2 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 @@ -13,6 +13,7 @@ import com.unnamed.b.atv.sample.fragment.CustomViewHolderFragment; 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.TwoDScrollingFragment; import java.util.ArrayList; @@ -34,6 +35,7 @@ protected void onCreate(Bundle savedInstanceState) { listItems.put("Custom Holder Example", CustomViewHolderFragment.class); listItems.put("Selectable Nodes", SelectableTreeFragment.class); listItems.put("2d scrolling", TwoDScrollingFragment.class); + listItems.put("Expand with arrow only", TwoDScrollingArrowExpandFragment.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 new file mode 100644 index 0000000..bc64194 --- /dev/null +++ b/app/src/main/java/com/unnamed/b/atv/sample/fragment/TwoDScrollingArrowExpandFragment.java @@ -0,0 +1,82 @@ +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 TwoDScrollingArrowExpandFragment 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); + fillFolder(s2); + + root.addChildren(s1, s2); + + tView = new AndroidTreeView(getActivity(), root); + tView.setDefaultAnimation(true); + tView.setUse2dScroll(true); + tView.setDefaultContainerStyle(R.style.TreeNodeStyleCustom); + tView.setDefaultNodeClickListener(TwoDScrollingArrowExpandFragment.this); + tView.setDefaultViewHolder(ArrowExpandSelectableHeaderHolder.class); + containerView.addView(tView.getView()); + tView.setUseAutoToggle(false); + + tView.expandAll(); + + if (savedInstanceState != null) { + String state = savedInstanceState.getString("tState"); + if (!TextUtils.isEmpty(state)) { + tView.restoreState(state); + } + } + return rootView; + } + + private void fillFolder(TreeNode folder) { + TreeNode currentNode = folder; + for (int i = 0; i < 4; i++) { + TreeNode file = new TreeNode(new IconTreeItemHolder.IconTreeItem(R.string.ic_folder, NAME + " " + i)); + currentNode.addChild(file); + currentNode = 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 new file mode 100644 index 0000000..c58d084 --- /dev/null +++ b/app/src/main/java/com/unnamed/b/atv/sample/holder/ArrowExpandSelectableHeaderHolder.java @@ -0,0 +1,74 @@ +package com.unnamed.b.atv.sample.holder; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import com.github.johnkil.print.PrintView; +import com.unnamed.b.atv.model.TreeNode; +import com.unnamed.b.atv.sample.R; + +/** + * Created by Bogdan Melnychuk on 2/15/15, modified by Szigeti Peter 2/2/16. + */ +public class ArrowExpandSelectableHeaderHolder extends TreeNode.BaseNodeViewHolder { + private TextView tvValue; + private PrintView arrowView; + private CheckBox nodeSelector; + + public ArrowExpandSelectableHeaderHolder(Context context) { + super(context); + } + + @Override + public View createNodeView(final TreeNode node, IconTreeItemHolder.IconTreeItem value) { + final LayoutInflater inflater = LayoutInflater.from(context); + final View view = inflater.inflate(R.layout.layout_selectable_header, null, false); + + tvValue = (TextView) view.findViewById(R.id.node_value); + tvValue.setText(value.text); + + final PrintView iconView = (PrintView) view.findViewById(R.id.icon); + iconView.setIconText(context.getResources().getString(value.icon)); + + arrowView = (PrintView) view.findViewById(R.id.arrow_icon); + arrowView.setPadding(20,10,10,10); + if (node.isLeaf()) { + arrowView.setVisibility(View.GONE); + } + arrowView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + tView.toggleNode(node); + } + }); + + nodeSelector = (CheckBox) view.findViewById(R.id.node_selector); + nodeSelector.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + node.setSelected(isChecked); + for (TreeNode n : node.getChildren()) { + getTreeView().selectNode(n, isChecked); + } + } + }); + nodeSelector.setChecked(node.isSelected()); + + return view; + } + + @Override + public void toggle(boolean active) { + arrowView.setIconText(context.getResources().getString(active ? R.string.ic_keyboard_arrow_down : R.string.ic_keyboard_arrow_right)); + } + + @Override + public void toggleSelectionMode(boolean editModeEnabled) { + nodeSelector.setVisibility(editModeEnabled ? View.VISIBLE : View.GONE); + nodeSelector.setChecked(mNode.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 index d56241a..222a43a 100644 --- a/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java +++ b/library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java @@ -36,6 +36,7 @@ public class AndroidTreeView { private boolean mSelectionModeEnabled; private boolean mUseDefaultAnimation = false; private boolean use2dScroll = false; + private boolean enableAutoToggle = true; public AndroidTreeView(Context context) { mContext = context; @@ -71,6 +72,14 @@ public boolean is2dScrollEnabled() { return use2dScroll; } + public void setUseAutoToggle(boolean enableAutoToggle) { + this.enableAutoToggle = enableAutoToggle; + } + + public boolean isAutoToggleEnabled() { + return enableAutoToggle; + } + public void setDefaultViewHolder(Class viewHolder) { defaultViewHolderClass = viewHolder; } @@ -194,7 +203,7 @@ private void getSaveState(TreeNode root, StringBuilder sBuilder) { } } - private void toggleNode(TreeNode node) { + public void toggleNode(TreeNode node) { if (node.isExpanded()) { collapseNode(node, false); } else { @@ -260,7 +269,9 @@ public void onClick(View v) { } else if (nodeClickListener != null) { nodeClickListener.onClick(n, n.getValue()); } - toggleNode(n); + if (enableAutoToggle) { + toggleNode(n); + } } }); @@ -272,6 +283,9 @@ public boolean onLongClick(View view) { } else if (nodeLongClickListener != null) { return nodeLongClickListener.onLongClick(n, n.getValue()); } + if (enableAutoToggle) { + toggleNode(n); + } return false; } }); From 44fff00b2a15e33d9bb1f61f6ad947ea1f0609c9 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Fri, 12 Feb 2016 10:51:07 +0200 Subject: [PATCH 24/28] Update README.md Google play image updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eac8970..e95a170 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Tree view implementation for android ### Demo -[![AndroidTreeView Demo on Google Play Store](http://developer.android.com/images/brand/en_generic_rgb_wo_60.png)](https://play.google.com/store/apps/details?id=com.unnamed.b.atv.demo) +[![AndroidTreeView Demo on Google Play Store](http://style.anu.edu.au/_anu/images/icons/icon-google-play-small.png)](https://play.google.com/store/apps/details?id=com.unnamed.b.atv.demo) ### Features From 5a015a1281ad995184ab310bf1dfdc260b974e12 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Fri, 12 Feb 2016 11:07:02 +0200 Subject: [PATCH 25/28] Update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index e95a170..89074e2 100644 --- a/README.md +++ b/README.md @@ -105,12 +105,6 @@ AndroidTreeView.setDefaultNodeClickListener For more details use sample application as example -### Upcoming changes - -**1)** Horizontal scroll issue - -**2)** Add wiki? - Let me know if i missed something, appreciate your support, thanks! ### Projects using this library From 654856509eb4e12ad241fd43cd4f3edd600c2f07 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Mon, 25 Apr 2016 17:12:50 +0200 Subject: [PATCH 26/28] version updated --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 87aebb0..3cff844 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.7 -VERSION_CODE=9 +VERSION_NAME=1.2.8 +VERSION_CODE=10 ANDROID_BUILD_MIN_SDK_VERSION=11 From d051ce75f5c9bd5206481808f6133b51f581c8f1 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Tue, 3 May 2016 09:31:03 +0200 Subject: [PATCH 27/28] Update gradle.properties --- gradle.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3cff844..ddb8205 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=1.2.8 -VERSION_CODE=10 +VERSION_NAME=1.2.9 +VERSION_CODE=11 ANDROID_BUILD_MIN_SDK_VERSION=11 @@ -36,4 +36,4 @@ POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=unnamed_b -POM_DEVELOPER_NAME=Bogdan Melnychuk \ No newline at end of file +POM_DEVELOPER_NAME=Bogdan Melnychuk From 5850dc8252b94e3ce9c5f2a375dea57683b326a1 Mon Sep 17 00:00:00 2001 From: Bogdan Melnychuk Date: Wed, 9 Feb 2022 08:25:31 +0100 Subject: [PATCH 28/28] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 89074e2..d18ece8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +## This project is deprecated. You can still use it as it. Let me know if someone is interested in supporting it. + AndroidTreeView ====================