使用RecyclerView写树形结构的TreeRecyclerView

简单介绍

android是不提供树形控件的,假设须要使用树形控件。我们应该怎么做呢?
先看效果
GIF
上图是一个明显的树形结构

实现原理

在逻辑上,它们是包括关系。数据结构上是多叉树,这是毋庸置疑的。

可是,显示的时候。我们有必要嵌套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 莫川

posted @ 2016-04-01 15:17  zfyouxi  阅读(431)  评论(0编辑  收藏  举报