使用RecyclerView写树形结构的TreeRecyclerView
简单介绍
android是不提供树形控件的,假设须要使用树形控件。我们应该怎么做呢? 
先看效果 
 
上图是一个明显的树形结构
实现原理
在逻辑上,它们是包括关系。数据结构上是多叉树,这是毋庸置疑的。
可是,显示的时候。我们有必要嵌套ListView或RecyclerView吗?当然没有必要!
- 每一而Item。在显示的时候,都是平级的,仅仅是它们marginLeft不同而已。
- 更新marginLeft来体现它们的层级关系。
marginLeft的值与item在逻辑上的深度有线性关系。 
- 展开一个Item的时候,是动态的加入一系列的item。
- 收起一个Item的时候。我们是删除一系列的item.
好了。原理已经说明确了。那就看看源代码怎么写吧。
注:
- 我们以android的文件系统的树形结构为例
- 为了动画的流畅性,我们使用RecyclerView,注意,ListView在加入和删除item时。是直接突变的。
Code
- 数据模型ItemData
public class ItemData implements Comparable<ItemData> {
    public static final int ITEM_TYPE_PARENT = 0;
    public static final int ITEM_TYPE_CHILD = 1;
    private String uuid;
    private int type;// 显示类型
    private String text;
    private String path;// 路径
    private int treeDepth = 0;// 路径的深度
    private List<ItemData> children;
    private boolean expand;// 是否展开
    ...
}
- 父节点相应的ViewHolder
/**
 * @Author Zheng Haibo
 * @PersonalWebsite http://www.mobctrl.net
 * @Description
 */
public class ParentViewHolder extends BaseViewHolder {
    public ImageView image;
    public TextView text;
    public ImageView expand;
    public TextView count;
    public RelativeLayout relativeLayout;
    private int itemMargin;
    public ParentViewHolder(View itemView) {
        super(itemView);
        image = (ImageView) itemView.findViewById(R.id.image);
        text = (TextView) itemView.findViewById(R.id.text);
        expand = (ImageView) itemView.findViewById(R.id.expand);
        count = (TextView) itemView.findViewById(R.id.count);
        relativeLayout = (RelativeLayout) itemView.findViewById(R.id.container);
        itemMargin = itemView.getContext().getResources()
                .getDimensionPixelSize(R.dimen.item_margin);
    }
    public void bindView(final ItemData itemData, final int position,
            final ItemDataClickListener imageClickListener) {
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) expand
                .getLayoutParams();
        params.leftMargin = itemMargin * itemData.getTreeDepth();
        expand.setLayoutParams(params);
        text.setText(itemData.getText());
        if (itemData.isExpand()) {
            expand.setRotation(45);
            List<ItemData> children = itemData.getChildren();
            if (children != null) {
                count.setText(String.format("(%s)", itemData.getChildren()
                        .size()));
            }
            count.setVisibility(View.VISIBLE);
        } else {
            expand.setRotation(0);
            count.setVisibility(View.GONE);
        }
        relativeLayout.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (imageClickListener != null) {
                    if (itemData.isExpand()) {
                        imageClickListener.onHideChildren(itemData);
                        itemData.setExpand(false);
                        rotationExpandIcon(45, 0);
                        count.setVisibility(View.GONE);
                    } else {
                        imageClickListener.onExpandChildren(itemData);
                        itemData.setExpand(true);
                        rotationExpandIcon(0, 45);
                        List<ItemData> children = itemData.getChildren();
                        if (children != null) {
                            count.setText(String.format("(%s)", itemData
                                    .getChildren().size()));
                        }
                        count.setVisibility(View.VISIBLE);
                    }
                }
            }
        });
        image.setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                Toast.makeText(view.getContext(), "longclick",
                        Toast.LENGTH_SHORT).show();
                return false;
            }
        });
    }
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private void rotationExpandIcon(float from, float to) {
        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(from, to);
            valueAnimator.setDuration(150);
            valueAnimator.setInterpolator(new DecelerateInterpolator());
            valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    expand.setRotation((Float) valueAnimator.getAnimatedValue());
                }
            });
            valueAnimator.start();
        }
    }
}
- 子节点相应的ViewHolder
/**
 * @Author Zheng Haibo
 * @PersonalWebsite http://www.mobctrl.net
 * @Description
 */
public class ChildViewHolder extends BaseViewHolder {
    public TextView text;
    public ImageView image;
    public RelativeLayout relativeLayout;
    private int itemMargin;
    private int offsetMargin;
    public ChildViewHolder(View itemView) {
        super(itemView);
        text = (TextView) itemView.findViewById(R.id.text);
        image = (ImageView) itemView.findViewById(R.id.image);
        relativeLayout = (RelativeLayout) itemView.findViewById(R.id.container);
        itemMargin = itemView.getContext().getResources()
                .getDimensionPixelSize(R.dimen.item_margin);
        offsetMargin = itemView.getContext().getResources()
                .getDimensionPixelSize(R.dimen.expand_size);
    }
    public void bindView(final ItemData itemData, int position) {
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) image
                .getLayoutParams();
        params.leftMargin = itemMargin * itemData.getTreeDepth() + offsetMargin;
        image.setLayoutParams(params);
        text.setText(itemData.getText());
        relativeLayout.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                //TODO
            }
        });
    }
}
- RecyclerView的Adapter
该部分处理item点击之后的展开和收起,实质上就是将其全部的Children节点动态的加入或删除。加入的位置就是item当前的位置。实现代码在onExpandChildren和onHideChildren方法中。
/**
 * @Author Zheng Haibo
 * @PersonalWebsite http://www.mobctrl.net
 * @Description
 */
public class RecyclerAdapter extends RecyclerView.Adapter<BaseViewHolder> {
    private Context mContext;
    private List<ItemData> mDataSet;
    private OnScrollToListener onScrollToListener;
    public void setOnScrollToListener(OnScrollToListener onScrollToListener) {
        this.onScrollToListener = onScrollToListener;
    }
    public RecyclerAdapter(Context context) {
        mContext = context;
        mDataSet = new ArrayList<ItemData>();
    }
    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = null;
        switch (viewType) {
        case ItemData.ITEM_TYPE_PARENT:
            view = LayoutInflater.from(mContext).inflate(
                    R.layout.item_recycler_parent, parent, false);
            return new ParentViewHolder(view);
        case ItemData.ITEM_TYPE_CHILD:
            view = LayoutInflater.from(mContext).inflate(
                    R.layout.item_recycler_child, parent, false);
            return new ChildViewHolder(view);
        default:
            view = LayoutInflater.from(mContext).inflate(
                    R.layout.item_recycler_parent, parent, false);
            return new ChildViewHolder(view);
        }
    }
    @Override
    public void onBindViewHolder(BaseViewHolder holder, int position) {
        switch (getItemViewType(position)) {
        case ItemData.ITEM_TYPE_PARENT:
            ParentViewHolder imageViewHolder = (ParentViewHolder) holder;
            imageViewHolder.bindView(mDataSet.get(position), position,
                    imageClickListener);
            break;
        case ItemData.ITEM_TYPE_CHILD:
            ChildViewHolder textViewHolder = (ChildViewHolder) holder;
            textViewHolder.bindView(mDataSet.get(position), position);
            break;
        default:
            break;
        }
    }
    private ItemDataClickListener imageClickListener = new ItemDataClickListener() {
        @Override
        public void onExpandChildren(ItemData itemData) {
            int position = getCurrentPosition(itemData.getUuid());
            List<ItemData> children = getChildrenByPath(itemData.getPath(),
                    itemData.getTreeDepth());
            if (children == null) {
                return;
            }
            addAll(children, position + 1);// 插入到点击点的下方
            itemData.setChildren(children);
            if (onScrollToListener != null) {
                onScrollToListener.scrollTo(position + 1);
            }
        }
        @Override
        public void onHideChildren(ItemData itemData) {
            int position = getCurrentPosition(itemData.getUuid());
            List<ItemData> children = itemData.getChildren();
            if (children == null) {
                return;
            }
            removeAll(position + 1, getChildrenCount(itemData) - 1);
            if (onScrollToListener != null) {
                onScrollToListener.scrollTo(position);
            }
            itemData.setChildren(null);
        }
    };
    @Override
    public int getItemCount() {
        return mDataSet.size();
    }
    private int getChildrenCount(ItemData item) {
        List<ItemData> list = new ArrayList<ItemData>();
        printChild(item, list);
        return list.size();
    }
    private void printChild(ItemData item, List<ItemData> list) {
        list.add(item);
        if (item.getChildren() != null) {
            for (int i = 0; i < item.getChildren().size(); i++) {
                printChild(item.getChildren().get(i), list);
            }
        }
    }
    /**
     * 依据路径获取子文件夹或文件
     *
     * @param path
     * @param treeDepth
     * @return
     */
    public List<ItemData> getChildrenByPath(String path, int treeDepth) {
        treeDepth++;
        try {
            List<ItemData> list = new ArrayList<ItemData>();
            File file = new File(path);
            File[] children = file.listFiles();
            List<ItemData> fileList = new ArrayList<ItemData>();
            for (File child : children) {
                if (child.isDirectory()) {
                    list.add(new ItemData(ItemData.ITEM_TYPE_PARENT, child
                            .getName(), child.getAbsolutePath(), UUID
                            .randomUUID().toString(), treeDepth, null));
                } else {
                    fileList.add(new ItemData(ItemData.ITEM_TYPE_CHILD, child
                            .getName(), child.getAbsolutePath(), UUID
                            .randomUUID().toString(), treeDepth, null));
                }
            }
            Collections.sort(list);
            Collections.sort(fileList);
            list.addAll(fileList);
            return list;
        } catch (Exception e) {
        }
        return null;
    }
    /**
     * 从position開始删除,删除
     *
     * @param position
     * @param itemCount
     *            删除的数目
     */
    protected void removeAll(int position, int itemCount) {
        for (int i = 0; i < itemCount; i++) {
            mDataSet.remove(position);
        }
        notifyItemRangeRemoved(position, itemCount);
    }
    protected int getCurrentPosition(String uuid) {
        for (int i = 0; i < mDataSet.size(); i++) {
            if (uuid.equalsIgnoreCase(mDataSet.get(i).getUuid())) {
                return i;
            }
        }
        return -1;
    }
    @Override
    public int getItemViewType(int position) {
        return mDataSet.get(position).getType();
    }
    public void add(ItemData text, int position) {
        mDataSet.add(position, text);
        notifyItemInserted(position);
    }
    public void addAll(List<ItemData> list, int position) {
        mDataSet.addAll(position, list);
        notifyItemRangeInserted(position, list.size());
    }
}
- 在MainActivity中调用
因为使用的是RecyclerView,在动态加入和删除孩子节点时。会有明显的“展开”和“收起”效果。
/**
 * @Author Zheng Haibo
 * @PersonalWebsite http://www.mobctrl.net
 * @Description
 */
public class MainActivity extends Activity {
    private RecyclerView recyclerView;
    private RecyclerAdapter myAdapter;
    private LinearLayoutManager linearLayoutManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        linearLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.getItemAnimator().setAddDuration(100);
        recyclerView.getItemAnimator().setRemoveDuration(100);
        recyclerView.getItemAnimator().setMoveDuration(200);
        recyclerView.getItemAnimator().setChangeDuration(100);
        myAdapter = new RecyclerAdapter(this);
        recyclerView.setAdapter(myAdapter);
        myAdapter.setOnScrollToListener(new OnScrollToListener() {
            @Override
            public void scrollTo(int position) {
                recyclerView.scrollToPosition(position);
            }
        });
        initDatas();
    }
    private void initDatas() {
        List<ItemData> list = myAdapter.getChildrenByPath("/", 0);
        myAdapter.addAll(list, 0);
    }
}
Project
Demo的Github地址:https://github.com/nuptboyzhb/TreeRecyclerView
@Author: Zheng Haibo 莫川
使用RecyclerView写树形结构的TreeRecyclerView的更多相关文章
- js文章列表的树形结构输出
		文章表设计成这样了 后端直接给了无任何处理的json数据,现在要前端实现树形结构的输出,其实后端处理更简单写,不过既然来了就码出来 var doclist = [{ "id": 1 ... 
- [从产品角度学EXCEL 02]-EXCEL里的树形结构
		这是<从产品角度学EXCEL>系列第三篇. 前言请看: 0 为什么要关注EXCEL的本质 1 excel是怎样运作的 或者你可以去微信公众号@尾巴说数 获得连载目录. 本文仅由尾巴本人发布 ... 
- C# EasyUI树形结构权限管理模块
		最近悟出来一个道理,在这儿分享给大家:学历代表你的过去,能力代表你的现在,学习代表你的将来. 十年河东十年河西,莫欺少年穷 学无止境,精益求精 本节和大家探讨下C#使用EasyUI树形结构/Tree构 ... 
- Atitit 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树 attilax理解与总结
		Atitit 常见的树形结构 红黑树 二叉树 B树 B+树 Trie树 attilax理解与总结 1.1. 树形结构-- 一对多的关系1 1.2. 树的相关术语: 1 1.3. 常见的树形结构 ... 
- 使用ztree.js,受益一生,十分钟学会使用tree树形结构插件
		看到ztree.js,这几个字眼,毋庸置疑,那肯定就是tree树形结构了,曾经的swing年代有jtree,后来jquery年代有jstree和treeview,虽然我没写过,但是我见过,一些小功能做 ... 
- jQuery+zTree加载树形结构菜单
		jQuery+zTree加载树形结构菜单 由于项目中需要设计树形菜单功能,经过一番捣腾之后,终于给弄出来了,所以便记下来,也算是学习zTree的一个总结吧. zTree的介绍: 1.zTree 是利用 ... 
- Qt QTreeWidget 树形结构实现(转)
		Qt中实现树形结构可以使用QTreeWidget类,也可以使用QTreeView类,QTreeWidget继承自QTreeView类.树形效果如下图所示: 这是怎么实现的呢?还有点击节点时会有相应的事 ... 
- 树形结构部门的 sqlserver 排序
		树形结构部门的 sqlserver 排序 因为要实现部门排序功能,而且要考虑部门的层级,直接用 sql 排序是不行的,所以写个 sql function 来支持. 首先部门表:company CREA ... 
- [SQL Server]树形结构的创建
		对于SQL Server来说,构建显示一个树形结构不是一件容易的事情,逻辑构造能力不是它的强项.不过也不是说它没有能力干这个事情,只要换一种思维方式就可以理解它的工作原理. 例如,现在有一张表的内容如 ... 
随机推荐
- Oracle数据库生成UUID
			从Data Ghost的blog得知,原来可以用Oracle来生成UUID,做法很简单,如下: select sys_guid() from dual; 数据类型是 raw(16) 有32个字符. 
- UVa 11054 Wine trading in Gergovia
			题意: 直线上有n个等距的酒庄,每个酒庄对酒的需求量为ai(正数说明需要买酒,负数需要卖酒),而且保证所有的酒庄供需平衡. 搬运x个单位的酒到相邻的酒庄需要x个劳动力,求要使所有酒庄供需平衡最少需要多 ... 
- 三个流行MySQL分支的对比
			MySQL是历史上最受欢迎的免费开源程序之一.它是成千上万个网站的数据库骨干,并且可以将它(和Linux)作为过去10年里Internet呈指数级增长的一个有力证明. 那么,如果MySQL真的这么重要 ... 
- 浅谈HTTP中Get、Post、Put与Delete的区别
			Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE.URL全称是资源描述符,我们可以这样认为:一个URL地址,它用于描述一个网络上的资源,而HTTP ... 
- Linux sysfs device_attribute
			/*************************************************************************** * Linux sysfs device_at ... 
- Zepto picLazyLoad Plugin,图片懒加载的Zepto插件
			嗯,学着国外人起名字Zepto picLazyLoad Plugin确实看起来高大上,其实js代码没几句,而且我每次写js都捉襟见肘,泪奔--- 图片懒加载有很多js插件,非常著名的属jQuery的L ... 
- 【转】[MTK软件原创] [SELinux] 如何设置确认selinux模式
			原文网址:http://bbs.16rd.com/thread-54766-1-1.html [Description] linux SELinux 分成Enforce 以及 Permissive 两 ... 
- Hrbust 2240  土豪的时代
			题意:中文题……不总结了……(好懒0-0) 土豪圈有一个习惯:从来不告诉别人自己到底有多少钱.但他们总是喜欢和其他土豪比较,来看看谁更土豪.于是每每几天,就会爆出一些关于土豪资产的消息,比如A土豪比B ... 
- web.xml 配置的详解
			http://my.oschina.net/u/1383439/blog/224448 http://blog.csdn.net/guihaijinfen/article/details/836383 ... 
- 如何学习ios开发
			著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处.作者:Franz Fang链接:http://www.zhihu.com/question/20264108/answer/3026 ... 
