1. 程式人生 > >使用RecyclerView寫樹形結構的TreeRecyclerView

使用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

@Author: Zheng Haibo 莫川