Bitliker 9 лет назад
Родитель
Сommit
f85d038223
26 измененных файлов с 2348 добавлено и 13 удалено
  1. 1 0
      WeiChat/build.gradle
  2. 2 1
      WeiChat/src/main/AndroidManifest.xml
  3. 125 2
      WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/activity/oa/DepartmentActivity.java
  4. 74 0
      WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/adapter/oa/SelectableHeaderHolder.java
  5. 9 1
      WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/presenter/WorkPresenter.java
  6. 3 0
      WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/util/attendance/AutoErpService.java
  7. BIN
      WeiChat/src/main/res/drawable-xxhdpi/xiaji.png
  8. BIN
      WeiChat/src/main/res/drawable-xxhdpi/xiaji_pass.png
  9. 8 5
      WeiChat/src/main/res/layout/activity_department.xml
  10. 2 1
      WeiChat/src/main/res/layout/fragment_me.xml
  11. 48 0
      WeiChat/src/main/res/layout/item_department.xml
  12. 0 1
      WeiChat/src/main/res/layout/sign_senior_setting.xml
  13. 5 1
      WeiChat/src/main/res/values/styles.xml
  14. 26 0
      library/.gitignore
  15. 24 0
      library/build.gradle
  16. 22 0
      library/gradle.properties
  17. 17 0
      library/proguard-rules.pro
  18. 5 0
      library/src/main/AndroidManifest.xml
  19. 29 0
      library/src/main/java/com/unnamed/b/atv/holder/SimpleViewHolder.java
  20. 284 0
      library/src/main/java/com/unnamed/b/atv/model/TreeNode.java
  21. 488 0
      library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java
  22. 52 0
      library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java
  23. 1110 0
      library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java
  24. 6 0
      library/src/main/res/values/ids.xml
  25. 7 0
      library/src/main/res/values/styles.xml
  26. 1 1
      settings.gradle

+ 1 - 0
WeiChat/build.gradle

@@ -133,4 +133,5 @@ dependencies {
     //    compile fileTree(include: ['*.jar'], dir: 'libs')
     testCompile 'junit:junit:4.12'
     compile files('libs/Msc.jar')
+    compile project(':library')
 }

+ 2 - 1
WeiChat/src/main/AndroidManifest.xml

@@ -839,7 +839,8 @@
         <activity
             android:name=".ui.erp.activity.oa.StatisticsActivity"
             android:label="考勤统计" />
-        <activity android:name=".ui.erp.activity.oa.DepartmentActivity"></activity>
+        <activity android:name=".ui.erp.activity.oa.DepartmentActivity"
+            />
     </application>
 
 </manifest>

+ 125 - 2
WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/activity/oa/DepartmentActivity.java

@@ -1,26 +1,149 @@
 package com.xzjmyk.pm.activity.ui.erp.activity.oa;
 
 import android.os.Bundle;
+import android.util.Log;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
 import android.widget.FrameLayout;
+import android.widget.TextView;
 
+import com.lidroid.xutils.ViewUtils;
 import com.lidroid.xutils.view.annotation.ViewInject;
+import com.unnamed.b.atv.model.TreeNode;
+import com.unnamed.b.atv.view.AndroidTreeView;
 import com.xzjmyk.pm.activity.R;
 import com.xzjmyk.pm.activity.ui.base.BaseActivity;
+import com.xzjmyk.pm.activity.ui.erp.adapter.oa.SelectableHeaderHolder;
+import com.xzjmyk.pm.activity.ui.erp.db.DBManager;
+import com.xzjmyk.pm.activity.ui.erp.entity.HrorgsEntity;
+import com.xzjmyk.pm.activity.ui.erp.util.CommonUtil;
+import com.xzjmyk.pm.activity.ui.erp.util.ListUtils;
+
+import java.util.ArrayList;
+import java.util.List;
 
 
 public class DepartmentActivity extends BaseActivity {
 
     @ViewInject(R.id.context_fl)
     private FrameLayout context_fl;
+    @ViewInject(R.id.mumber_tv)
+    private TextView mumber_tv;
+    @ViewInject(R.id.all_sure_cb)
+    private CheckBox all_sure_cb;
+    private AndroidTreeView tView;
+
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_department);
-        initView();
+        ViewUtils.inject(this);
+        initData();
+    }
+
+    private void initData() {
+        DBManager manager = new DBManager(ct);
+        String master = CommonUtil.getSharedPreferences(ct, "erp_master");
+        List<HrorgsEntity> hrorgsEntities = manager.queryHrorgList(new String[]{master}, "whichsys=?");
+        manager.closeDB();
+        if (ListUtils.isEmpty(hrorgsEntities)) return;
+        List<TreeNode> parent = new ArrayList();//每个节点一个
+        for (int i = 0; i < hrorgsEntities.size(); i++) {//先获取所有父节点
+            if (hrorgsEntities.get(i).getOr_subof() == 0) {
+                parent.add(getNewNode(hrorgsEntities.get(i)));
+                hrorgsEntities.remove(i);
+                i--;
+            }
+        }
+        if (ListUtils.isEmpty(parent)) return;
+        if (!ListUtils.isEmpty(hrorgsEntities))
+            recursive(hrorgsEntities, parent);
+        initView(parent);
+    }
+
+    private void initView(List<TreeNode> parent) {
+        //放界面
+        TreeNode root = TreeNode.root();
+        for (TreeNode e : parent)
+            root.addChild(e);
+        tView = new AndroidTreeView(ct, root);
+        tView.setDefaultAnimation(true);
+        tView.setSelectionModeEnabled(true);
+        tView.setDefaultContainerStyle(R.style.TreeNodeStyleCustom);
+        context_fl.addView(tView.getView());
+
+        all_sure_cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+                Log.i("gongpengming", "onCheckedChanged=" + b);
+                if (b) {
+                    tView.selectAll(true);
+                } else tView.deselectAll();
+
+            }
+        });
+    }
+
+    /**
+     * 递归找当前节点的下级
+     *
+     * @param hrorgsEntities 所有的
+     * @param parent         当前节点
+     */
+    private void recursive(List<HrorgsEntity> hrorgsEntities, List<TreeNode> parent) {
+        if (ListUtils.isEmpty(hrorgsEntities) || ListUtils.isEmpty(parent)) return;
+        for (int i = 0; i < hrorgsEntities.size(); i++) {
+            HrorgsEntity e = hrorgsEntities.get(i);
+            for (TreeNode m : parent) {
+                HrorgsEntity che = (HrorgsEntity) m.getValue();
+                if (che.getOr_id() == e.getOr_subof()) {//当前节点捕获到了
+                    m.addChild(getNewNode(e));
+                    hrorgsEntities.remove(i);
+                    i--;
+                }
+            }
+        }
+        recursive(hrorgsEntities, getAllChild(parent));
+
+    }
+
+    /**
+     * 获取当前父节点所有的子节点
+     *
+     * @param parent
+     * @return
+     */
+    private List<TreeNode> getAllChild(List<TreeNode> parent) {
+        if (ListUtils.isEmpty(parent)) return null;
+        List<TreeNode> allChild = new ArrayList<>();
+        for (TreeNode e : parent) {
+            if (!ListUtils.isEmpty(e.getChildren()))
+                allChild.addAll(e.getChildren());
+        }
+        return allChild;
+    }
+
+
+    private TreeNode getNewNode(HrorgsEntity e) {
+        TreeNode treeNode = new TreeNode(e);
+        treeNode.setViewHolder(new SelectableHeaderHolder(this));
+        return treeNode;
+    }
+
+    public void onCheckedChanged(boolean isClicked) {
+        int selectNum = 0;
+        Log.i("gongpengming", "hrorgsEntities=" + tView.getSelected());
+        selectNum = tView.getSelected().size();
+        mumber_tv.setText("选择 " + selectNum + " 个人员");
     }
 
-    private void initView() {
 
+    private void recursiveNum(int i, List<TreeNode> parent) {
+        if (ListUtils.isEmpty(parent)) return;
+        for (TreeNode n : parent) {
+            if (n.isSelected()) i++;
+        }
+        recursiveNum(i, getAllChild(parent));
     }
 }

+ 74 - 0
WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/adapter/oa/SelectableHeaderHolder.java

@@ -0,0 +1,74 @@
+package com.xzjmyk.pm.activity.ui.erp.adapter.oa;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.unnamed.b.atv.model.TreeNode;
+import com.xzjmyk.pm.activity.MyApplication;
+import com.xzjmyk.pm.activity.R;
+import com.xzjmyk.pm.activity.ui.erp.activity.oa.DepartmentActivity;
+import com.xzjmyk.pm.activity.ui.erp.entity.HrorgsEntity;
+
+/**
+ * Created by Bogdan Melnychuk on 2/15/15.
+ */
+public class SelectableHeaderHolder extends TreeNode.BaseNodeViewHolder<HrorgsEntity> {
+    private DepartmentActivity context;
+
+    public SelectableHeaderHolder(Context context) {
+        super(context);
+        if (context instanceof DepartmentActivity) {
+            Log.i("gongpengming", "context instanceof DepartmentActivity");
+            this.context = (DepartmentActivity) context;
+        }
+    }
+
+    @Override
+    public View createNodeView(final TreeNode node, HrorgsEntity value) {
+        View view = LayoutInflater.from(MyApplication.getInstance()).inflate(R.layout.item_department, null, false);
+        CheckBox node_cb = (CheckBox) view.findViewById(R.id.node_cb);
+        ImageView image = (ImageView) view.findViewById(R.id.image);
+        TextView node_value = (TextView) view.findViewById(R.id.node_value);
+        node_value.setText(value.getOr_name());
+        node_cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
+                node.setSelected(isChecked);
+                if (context != null)
+                    context.onCheckedChanged(isChecked);
+                for (TreeNode n : node.getChildren()) {
+                    getTreeView().selectNode(n, isChecked);
+                }
+            }
+        });
+        node_cb.setChecked(node.isSelected());
+        int reid;
+        if (node.isLeaf()) {//最底层
+            reid = R.drawable.xiaji_pass;
+        } else {//非最底层
+            if (node.getChildren().get(0).isExpanded())
+                reid = R.drawable.xiaji_pass;
+            else
+                reid = R.drawable.xiaji;
+        }
+        image.setImageResource(reid);
+        return view;
+    }
+
+    private boolean isSelected(TreeNode node) {
+        if (node.isSelected()) return true;
+        if (node.getParent() != null) {
+            if (node.getParent().isSelected()) return true;
+            else return isSelected(node.getParent());
+        } else {
+            return false;
+        }
+    }
+
+}

+ 9 - 1
WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/presenter/WorkPresenter.java

@@ -526,7 +526,15 @@ public class WorkPresenter implements HttpHandler.OnResultListener, IWorkPresent
      */
     public void submitWork(final ArrayList<WorkModel> models, String macAddress) {
         subMitTime = System.currentTimeMillis();
-        validatorMac(models, macAddress);
+//        validatorMac(models, macAddress);
+        CommonInterface.getInstance().getCodeByNet("CardLog", new CommonInterface.OnResultListener() {
+            @Override
+            public void result(boolean isOk, String result, String message) {
+                Bundle bundle = new Bundle();
+                bundle.putParcelableArrayList("models", models);
+                signinWork(bundle, result);
+            }
+        });
     }
 
 

+ 3 - 0
WeiChat/src/main/java/com/xzjmyk/pm/activity/ui/erp/util/attendance/AutoErpService.java

@@ -296,6 +296,9 @@ public class AutoErpService extends Service {
                                 if (mission != null) {//TODO 符合外勤签到
                                     log("符合外勤签到");
                                     save("外勤", "符合外勤签到");
+                                    if (mission.getStatus() == 2) {
+                                        missionIter = -1;
+                                    }
                                     Message message = handler.obtainMessage();
                                     message.what = STATUS_MISSION;
                                     Bundle bundle = new Bundle();

BIN
WeiChat/src/main/res/drawable-xxhdpi/xiaji.png


BIN
WeiChat/src/main/res/drawable-xxhdpi/xiaji_pass.png


+ 8 - 5
WeiChat/src/main/res/layout/activity_department.xml

@@ -6,9 +6,12 @@
     android:layout_height="match_parent"
     android:orientation="vertical"
     tools:context="com.xzjmyk.pm.activity.ui.erp.activity.oa.DepartmentActivity">
-<FrameLayout
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:id="@+id/context_fl"
-    />
+
+    <FrameLayout
+        android:id="@+id/context_fl"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+
+    <include layout="@layout/include_select"/>
 </LinearLayout>

+ 2 - 1
WeiChat/src/main/res/layout/fragment_me.xml

@@ -488,7 +488,7 @@
             style="@style/IMTbleLine_UP_Me"
             android:layout_marginTop="7dp"
             android:background="@drawable/selector_me_menu_item_bg"
-            android:visibility="gone">
+         >
 
             <TextView
                 android:layout_width="wrap_content"
@@ -515,6 +515,7 @@
             android:id="@+id/uu_sport_step"
             style="@style/IMTbleLine_UP_Me"
             android:layout_marginTop="7dp"
+            android:visibility="gone"
             android:background="@drawable/selector_me_menu_item_bg"
        >
 

+ 48 - 0
WeiChat/src/main/res/layout/item_department.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:minHeight="48dp"
+    android:paddingLeft="10dp">
+
+    <CheckBox
+        android:id="@+id/node_cb"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_centerHorizontal="true"
+        android:layout_centerVertical="true"
+        android:button="@drawable/oa_checkbox"
+        android:padding="10dp" />
+
+    <ImageView
+        android:id="@+id/image"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_centerHorizontal="true"
+        android:layout_centerVertical="true"
+        android:layout_marginRight="10dp"
+        android:src="@drawable/oa_next" />
+
+    <TextView
+        android:id="@+id/node_value"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginLeft="10dp"
+        android:layout_marginRight="10dp"
+        android:layout_toLeftOf="@+id/image"
+        android:layout_toRightOf="@id/node_cb"
+        android:ellipsize="end"
+        android:lines="1"
+        android:text="hhhh"
+        android:textColor="@color/text_hine"
+        android:textSize="@dimen/text_main" />
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="2px"
+        android:layout_alignParentBottom="true"
+        android:background="@color/item_line" />
+</RelativeLayout>

+ 0 - 1
WeiChat/src/main/res/layout/sign_senior_setting.xml

@@ -43,7 +43,6 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:src="@drawable/uu_step"
-            android:layout_toRightOf="@+id/senior_setting_auto_sign_reply"
             android:layout_centerVertical="true"
             android:visibility="gone"
             />

+ 5 - 1
WeiChat/src/main/res/values/styles.xml

@@ -12,7 +12,11 @@
     <style name="style_actionbar_title_style">
         <item name="android:textSize">13sp</item>
     </style>
-
+    <style name="TreeNodeStyleCustom">
+        <item name="android:paddingLeft">20dp</item>
+        <!--<item name="android:showDividers">middle</item>-->
+        <!--<item name="android:divider">@android:drawable/divider_horizontal_bright</item>-->
+    </style>
     <style name="style_bg_bar_bule">
         <item name="android:background">@color/titleBlue</item>
     </style>

+ 26 - 0
library/.gitignore

@@ -0,0 +1,26 @@
+# Built application files
+*.apk
+*.ap_
+
+# Files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# IntelliJ IDEA
+.idea/
+*.iml
+*.iws
+*.ipr
+
+# Gradle
+.gradle
+build/

+ 24 - 0
library/build.gradle

@@ -0,0 +1,24 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 22
+    buildToolsVersion "22.0.1"
+
+    defaultConfig {
+        minSdkVersion 9
+        targetSdkVersion 22
+//        versionName project.VERSION_NAME
+//        versionCode Integer.parseInt(project.VERSION_CODE)
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+        }
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+    compile 'com.android.support:appcompat-v7:22.2.0'
+}
+

+ 22 - 0
library/gradle.properties

@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+POM_NAME=AndroidTreeView
+POM_ARTIFACT_ID=atv
+POM_PACKAGING=aar

+ 17 - 0
library/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/bogdan/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 5 - 0
library/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<manifest package="com.unnamed.b.atv">
+
+    <application />
+
+</manifest>

+ 29 - 0
library/src/main/java/com/unnamed/b/atv/holder/SimpleViewHolder.java

@@ -0,0 +1,29 @@
+package com.unnamed.b.atv.holder;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.TextView;
+
+import com.unnamed.b.atv.model.TreeNode;
+
+/**
+ * Created by Bogdan Melnychuk on 2/11/15.
+ */
+public class SimpleViewHolder extends TreeNode.BaseNodeViewHolder<Object> {
+
+    public SimpleViewHolder(Context context) {
+        super(context);
+    }
+
+    @Override
+    public View createNodeView(TreeNode node, Object value) {
+        final TextView tv = new TextView(context);
+        tv.setText(String.valueOf(value));
+        return tv;
+    }
+
+    @Override
+    public void toggle(boolean active) {
+
+    }
+}

+ 284 - 0
library/src/main/java/com/unnamed/b/atv/model/TreeNode.java

@@ -0,0 +1,284 @@
+package com.unnamed.b.atv.model;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.unnamed.b.atv.R;
+import com.unnamed.b.atv.view.AndroidTreeView;
+import com.unnamed.b.atv.view.TreeNodeWrapperView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by Bogdan Melnychuk on 2/10/15.
+ */
+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;
+    private final List<TreeNode> children;
+    private BaseNodeViewHolder mViewHolder;
+    private TreeNodeClickListener mClickListener;
+    private TreeNodeLongClickListener mLongClickListener;
+    private Object mValue;
+    private boolean mExpanded;
+
+    public static TreeNode root() {
+        TreeNode root = new TreeNode(null);
+        root.setSelectable(false);
+        return root;
+    }
+
+    private int generateId() {
+        return ++mLastId;
+    }
+
+    public TreeNode(Object value) {
+        children = new ArrayList<>();
+        mValue = value;
+    }
+
+    public TreeNode addChild(TreeNode childNode) {
+        childNode.mParent = this;
+        childNode.mId = generateId();
+        children.add(childNode);
+        return this;
+    }
+
+    public TreeNode addChildren(TreeNode... nodes) {
+        for (TreeNode n : nodes) {
+            addChild(n);
+        }
+        return this;
+    }
+
+    public TreeNode addChildren(Collection<TreeNode> nodes) {
+        for (TreeNode n : nodes) {
+            addChild(n);
+        }
+        return this;
+    }
+
+    public int deleteChild(TreeNode child) {
+        for (int i = 0; i < children.size(); i++) {
+            if (child.mId == children.get(i).mId) {
+                children.remove(i);
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public List<TreeNode> getChildren() {
+        return Collections.unmodifiableList(children);
+    }
+
+    public int size() {
+        return children.size();
+    }
+
+    public TreeNode getParent() {
+        return mParent;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public boolean isLeaf() {
+        return size() == 0;
+    }
+
+    public Object getValue() {
+        return mValue;
+    }
+
+    public boolean isExpanded() {
+        return mExpanded;
+    }
+
+    public TreeNode setExpanded(boolean expanded) {
+        mExpanded = expanded;
+        return this;
+    }
+
+    public void setSelected(boolean selected) {
+        mSelected = selected;
+    }
+
+    public boolean isSelected() {
+        return mSelectable && mSelected;
+    }
+
+    public void setSelectable(boolean selectable) {
+        mSelectable = selectable;
+    }
+
+    public boolean isSelectable() {
+        return mSelectable;
+    }
+
+    public String getPath() {
+        final StringBuilder path = new StringBuilder();
+        TreeNode node = this;
+        while (node.mParent != null) {
+            path.append(node.getId());
+            node = node.mParent;
+            if (node.mParent != null) {
+                path.append(NODES_ID_SEPARATOR);
+            }
+        }
+        return path.toString();
+    }
+
+
+    public int getLevel() {
+        int level = 0;
+        TreeNode root = this;
+        while (root.mParent != null) {
+            root = root.mParent;
+            level++;
+        }
+        return level;
+    }
+
+    public boolean isLastChild() {
+        if (!isRoot()) {
+            int parentSize = mParent.children.size();
+            if (parentSize > 0) {
+                final List<TreeNode> parentChildren = mParent.children;
+                return parentChildren.get(parentSize - 1).mId == mId;
+            }
+        }
+        return false;
+    }
+
+    public TreeNode setViewHolder(BaseNodeViewHolder viewHolder) {
+        mViewHolder = viewHolder;
+        if (viewHolder != null) {
+            viewHolder.mNode = this;
+        }
+        return this;
+    }
+
+    public TreeNode setClickListener(TreeNodeClickListener listener) {
+        mClickListener = listener;
+        return this;
+    }
+
+    public TreeNodeClickListener getClickListener() {
+        return this.mClickListener;
+    }
+
+    public TreeNode setLongClickListener(TreeNodeLongClickListener listener) {
+        mLongClickListener = listener;
+        return this;
+    }
+
+    public TreeNodeLongClickListener getLongClickListener() {
+        return mLongClickListener;
+    }
+
+    public BaseNodeViewHolder getViewHolder() {
+        return mViewHolder;
+    }
+
+    public boolean isFirstChild() {
+        if (!isRoot()) {
+            List<TreeNode> parentChildren = mParent.children;
+            return parentChildren.get(0).mId == mId;
+        }
+        return false;
+    }
+
+    public boolean isRoot() {
+        return mParent == null;
+    }
+
+    public TreeNode getRoot() {
+        TreeNode root = this;
+        while (root.mParent != null) {
+            root = root.mParent;
+        }
+        return root;
+    }
+
+    public interface TreeNodeClickListener {
+        void onClick(TreeNode node, Object value);
+    }
+
+    public interface TreeNodeLongClickListener {
+        boolean onLongClick(TreeNode node, Object value);
+    }
+
+    public static abstract class BaseNodeViewHolder<E> {
+        protected AndroidTreeView tView;
+        protected TreeNode mNode;
+        private View mView;
+        protected int containerStyle;
+        protected Context context;
+
+        public BaseNodeViewHolder(Context context) {
+            this.context = context;
+        }
+
+        public View getView() {
+            if (mView != null) {
+                return mView;
+            }
+            final View nodeView = getNodeView();
+            final TreeNodeWrapperView nodeWrapperView = new TreeNodeWrapperView(nodeView.getContext(), getContainerStyle());
+            nodeWrapperView.insertNodeView(nodeView);
+            mView = nodeWrapperView;
+
+            return mView;
+        }
+
+        public void setTreeViev(AndroidTreeView treeViev) {
+            this.tView = treeViev;
+        }
+
+        public AndroidTreeView getTreeView() {
+            return tView;
+        }
+
+        public void setContainerStyle(int style) {
+            containerStyle = style;
+        }
+
+        public View getNodeView() {
+            return createNodeView(mNode, (E) mNode.getValue());
+        }
+
+        public ViewGroup getNodeItemsView() {
+            return (ViewGroup) getView().findViewById(R.id.node_items);
+        }
+
+        public boolean isInitialized() {
+            return mView != null;
+        }
+
+        public int getContainerStyle() {
+            return containerStyle;
+        }
+
+
+        public abstract View createNodeView(TreeNode node, E value);
+
+        public void toggle(boolean active) {
+            // empty
+        }
+
+        public void toggleSelectionMode(boolean editModeEnabled) {
+            // empty
+        }
+    }
+}

+ 488 - 0
library/src/main/java/com/unnamed/b/atv/view/AndroidTreeView.java

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

+ 52 - 0
library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java

@@ -0,0 +1,52 @@
+package com.unnamed.b.atv.view;
+
+import android.content.Context;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.unnamed.b.atv.R;
+
+/**
+ * Created by Bogdan Melnychuk on 2/10/15.
+ */
+public class TreeNodeWrapperView extends LinearLayout {
+    private LinearLayout nodeItemsContainer;
+    private ViewGroup nodeContainer;
+    private final int containerStyle;
+
+    public TreeNodeWrapperView(Context context, int containerStyle) {
+        super(context);
+        this.containerStyle = containerStyle;
+        init();
+    }
+
+    private void init() {
+        setOrientation(LinearLayout.VERTICAL);
+
+        nodeContainer = new RelativeLayout(getContext());
+        nodeContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+        nodeContainer.setId(R.id.node_header);
+
+        ContextThemeWrapper newContext = new ContextThemeWrapper(getContext(), containerStyle);
+        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);
+        nodeItemsContainer.setVisibility(View.GONE);
+
+        addView(nodeContainer);
+        addView(nodeItemsContainer);
+    }
+
+
+    public void insertNodeView(View nodeView) {
+        nodeContainer.addView(nodeView);
+    }
+
+    public ViewGroup getNodeContainer() {
+        return nodeContainer;
+    }
+}

+ 1110 - 0
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.
+ * <p/>
+ * <p>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.
+     * </p>
+     *
+     * @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<View> 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;
+    }
+
+    /**
+     * <p>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.</p>
+     *
+     * @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);
+        }
+    }
+
+    /**
+     * <p>Scrolls the view to make the area defined by <code>top</code> and
+     * <code>bottom</code> 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.</p>
+     *
+     * @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());
+    }
+
+    /**
+     * <p>The scroll range of a scroll view is the overall height of all of its
+     * children.</p>
+     */
+    @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.
+     * <p/>
+     * 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}
+     * <p/>
+     * <p>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;
+    }
+}

+ 6 - 0
library/src/main/res/values/ids.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item name="node_header" type="id" />
+    <item name="node_items" type="id" />
+    <item name="tree_items" type="id" />
+</resources>

+ 7 - 0
library/src/main/res/values/styles.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="TreeNodeStyle">
+        <item name="android:paddingLeft">20dp</item>
+    </style>
+</resources>

+ 1 - 1
settings.gradle

@@ -1,2 +1,2 @@
-include ':pullToRefershLibraryMy', ':materialdialogs', ':MPAndroidChart', ':libedittextformlibrary', ':libfloatingactionbutton', ':libbdupdatesdk', ':library-viewpager-indicator', ':library-swipemenu_lv', ':lib-zxing', ':library-refreshlayout'
+include ':pullToRefershLibraryMy', ':materialdialogs', ':MPAndroidChart', ':libedittextformlibrary', ':libfloatingactionbutton', ':libbdupdatesdk', ':library-viewpager-indicator', ':library-swipemenu_lv', ':lib-zxing', ':library-refreshlayout', ':library'
 include ':WeiChat'