快速實現(xiàn)Android多級樹形列表,這個庫是在鴻洋多級樹形列表demo中修改而來。
解決的問題:
- 支持ID為int類型和String類型春锋。
- 支持多級復選框選中,使用只需一行代碼差凹。
- 支持動態(tài)更新數(shù)據(jù)并保持原有展開/關閉狀態(tài)期奔。
- 支持ListView、RecyclerView危尿。
一呐萌、概述
這幾天項目中有一個多級列表的菜單,最開始給我的感覺應該就是嵌套ListView,或者用ExpandableListView谊娇,但問題是ExpandableListView只支持兩級列表肺孤,而且關鍵的是具體分幾級是不確定的,也就是可能一級,可能多級赠堵,這要是五六級嵌套ListView小渊,想想那酸爽。茫叭。酬屉。最終在偷懶心態(tài)的驅使下到網(wǎng)上查查看有沒有類似的,也確實查到鴻洋大佬之前寫的一篇關于實現(xiàn)Android多級樹形列表的文章杂靶,實現(xiàn)很巧妙梆惯,使用一個ListView就可以實現(xiàn)多級列表效果,就download下demo吗垮,在demo基礎上做了部分修改垛吗,功能順利實現(xiàn)。
其實到這里應該就結束了烁登,但使用過程中遇到的一些問題讓我覺得這個可以進一步優(yōu)化怯屉,比如我要做個多級復選列表,每次處理子級選中與父級選中搞得很累饵沧,生怕哪個遞歸錯了锨络。而且在新增數(shù)據(jù)的時候刷新頁面也需要自己處理,直接刷新沒有效果狼牺,再加上現(xiàn)在RecyclerView已經(jīng)用的越來越多了羡儿。就想著在周末好好總結下,封裝個方便使用的庫是钥,方便下一次有類似需求的時候使用掠归。說到底,還是為了下次可以偷偷懶唄悄泥。如果小伙伴有類似需求虏冻,也可以直接拿來用~
先看下效果吧:
這篇文章主要介紹這個庫如何使用,如果對具體實現(xiàn)細節(jié)感興趣弹囚,可以查看源碼或者搜索鴻洋的博客厨相。
二、具體使用
我們關聯(lián)列表樹需要有三個必須元素鸥鹉,當前id蛮穿、父級id即pid,顯示的內容宋舷。id和pid可以為int或者String以及其他類型绪撵。要顯示的內容需要包裝一下:
//id pid name FileNode為實際用的實體Bean對象
mlist.add(new Node("223","0","我也是添加的root節(jié)點",new FileNode()));
對于ListView,需要繼承自TreeListViewAdapter
,如:
public class SimpleTreeAdapter extends TreeListViewAdapter
{
public SimpleTreeAdapter(ListView mTree, Context context, List<Node> datas, int defaultExpandLevel, int iconExpand, int iconNoExpand) {
super(mTree, context, datas, defaultExpandLevel, iconExpand, iconNoExpand);
}
public SimpleTreeAdapter(ListView mTree, Context context, List<Node> datas,
int defaultExpandLevel) {
super(mTree, context, datas, defaultExpandLevel);
}
@Override
public View getConvertView(final Node node , int position, View convertView, ViewGroup parent)
{
final ViewHolder viewHolder ;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.list_item, parent, false);
viewHolder = new ViewHolder();
viewHolder.cb = (CheckBox) convertView
.findViewById(R.id.cb_select_tree);
viewHolder.label = (TextView) convertView
.findViewById(R.id.id_treenode_label);
viewHolder.icon = (ImageView) convertView.findViewById(R.id.icon);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setChecked(node,viewHolder.cb.isChecked());
}
});
if (node.isChecked()){
viewHolder.cb.setChecked(true);
}else {
viewHolder.cb.setChecked(false);
}
if (node.getIcon() == -1) {
viewHolder.icon.setVisibility(View.INVISIBLE);
} else {
viewHolder.icon.setVisibility(View.VISIBLE);
viewHolder.icon.setImageResource(node.getIcon());
}
viewHolder.label.setText(node.getName());
return convertView;
}
private final class ViewHolder
{
ImageView icon;
CheckBox cb;
TextView label;
}
}
對于RecyclerView祝蝠,需繼承自TreeRecyclerAdapter
,如:
public class SimpleTreeRecyclerAdapter extends TreeRecyclerAdapter {
public SimpleTreeRecyclerAdapter(RecyclerView mTree, Context context, List<Node> datas, int defaultExpandLevel, int iconExpand, int iconNoExpand) {
super(mTree, context, datas, defaultExpandLevel, iconExpand, iconNoExpand);
}
public SimpleTreeRecyclerAdapter(RecyclerView mTree, Context context, List<Node> datas, int defaultExpandLevel) {
super(mTree, context, datas, defaultExpandLevel);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyHoder(View.inflate(mContext, R.layout.list_item,null));
}
@Override
public void onBindViewHolder(final Node node, RecyclerView.ViewHolder holder, int position) {
final MyHoder viewHolder = (MyHoder) holder;
//todo do something
viewHolder.cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setChecked(node,viewHolder.cb.isChecked());
}
});
if (node.isChecked()){
viewHolder.cb.setChecked(true);
}else {
viewHolder.cb.setChecked(false);
}
if (node.getIcon() == -1) {
viewHolder.icon.setVisibility(View.INVISIBLE);
} else {
viewHolder.icon.setVisibility(View.VISIBLE);
viewHolder.icon.setImageResource(node.getIcon());
}
viewHolder.label.setText(node.getName());
}
class MyHoder extends RecyclerView.ViewHolder{
public CheckBox cb;
public TextView label;
public ImageView icon;
public MyHoder(View itemView) {
super(itemView);
cb = (CheckBox) itemView
.findViewById(R.id.cb_select_tree);
label = (TextView) itemView
.findViewById(R.id.id_treenode_label);
icon = (ImageView) itemView.findViewById(R.id.icon);
}
}
}
初始化:
ListView:
//第一個參數(shù) ListView
//第二個參數(shù) 上下文
//第三個參數(shù) 數(shù)據(jù)集
//第四個參數(shù) 默認展開層級數(shù) 0為不展開
//第五個參數(shù) 展開的圖標
//第六個參數(shù) 閉合的圖標
mAdapter = new SimpleTreeAdapter(mTree, ListViewActivity.this,
mDatas, 1,R.mipmap.tree_ex,R.mipmap.tree_ec);
mTree.setAdapter(mAdapter);
RecyclerView:
//第一個參數(shù) RecyclerView
//第二個參數(shù) 上下文
//第三個參數(shù) 數(shù)據(jù)集
//第四個參數(shù) 默認展開層級數(shù) 0為不展開
//第五個參數(shù) 展開的圖標
//第六個參數(shù) 閉合的圖標
mAdapter = new SimpleTreeRecyclerAdapter(mTree, RecyclerViewActivity.this,
mDatas, 1,R.mipmap.tree_ex,R.mipmap.tree_ec);
mTree.setAdapter(mAdapter);
添加數(shù)據(jù),可以保持原有選中或者展開狀態(tài):
List<Node> mlist = new ArrayList<>();
mlist.add(new Node("223","0","我也是添加的root節(jié)點",new FileNode()));
mAdapter.addData(0,mlist);
獲取選中內容:如果node的isChecked()為true,即為選中狀態(tài)绎狭。
StringBuilder sb = new StringBuilder();
//獲取排序過的nodes
//如果不需要刻意直接用 mDatas既可
final List<Node> allNodes = mAdapter.getAllNodes();
for (int i = 0; i < allNodes.size(); i++) {
if (allNodes.get(i).isChecked()){
sb.append(allNodes.get(i).getName()+",");
}
}
String strNodesName = sb.toString();
if (!TextUtils.isEmpty(strNodesName))
Toast.makeText(this, strNodesName.substring(0, strNodesName.length()-1),Toast.LENGTH_SHORT).show();
控制父子之間聯(lián)動的選中與取消狀態(tài)细溅,只需調用setChecked
方法既可,注意如果在setOnCheckedChangeListener
中處理會有問題:因為如果要子節(jié)點/父節(jié)點選中或者取消需要刷新頁面儡嘶,而刷新頁面又會觸發(fā)viewHolder.cb.setChecked(true/false);
的判斷從而又會進入setOnCheckedChangeListener
喇聊,會導致如果父節(jié)點選中某些子節(jié)點取消不了的情況。
//viewHolder.cb 為CheckBox
viewHolder.cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setChecked(node,viewHolder.cb.isChecked());
}
});
三蹦狂、簡單介紹
通過一個ListView來展示所有數(shù)據(jù)誓篱,每一級內容的顯示根據(jù)當前展示數(shù)據(jù)的等級縮進一定的padding
值,讓我們看起來有縮進效果凯楔。
使用過程中感覺不是很舒服的地方在于最終用于顯示在界面實體Bean并不是我們傳進去的數(shù)據(jù)窜骄,而是經(jīng)過轉化并且過濾的數(shù)據(jù),這樣最直接的影響就是在我新增數(shù)據(jù)的數(shù)據(jù)之后摆屯,拿著Adapter來刷新的時候邻遏,并沒有任何效果。因為我們沒有將后面新加的數(shù)據(jù)進行轉化虐骑。
而我們如何能在不改變原有數(shù)據(jù)結構的基礎上准验,添加我們的新內容,并保持原有的選中或者展開正常呢廷没?我的想法是這樣的糊饱,如果可以直接給它傳入轉化后的Node節(jié)點類型數(shù)據(jù)就好了,我想到了繼承颠黎,讓實體類去繼承基類Node另锋,但一旦繼承Node則意味著實體類就不能再繼承其他類了,感覺不是很靈活盏缤,而且也影響了實體類本身的結構砰蠢。后來想到了包裝設計模式的一些東西,那我就在實體類外再包上一層唉铜,也就是將實體類傳給Node台舱,最終我們使用的還是Node,但也可以用node.bean
很輕松的取出實體類做其他操作潭流,并且實體類本身的結構并沒有被破壞竞惋。
在此基礎上,因為我們的Node不需要轉化重新創(chuàng)建灰嫉,那么它就可以保存一些狀態(tài)比如展開拆宛、選中等等,而在新加入數(shù)據(jù)時只需標記下新加入的數(shù)據(jù)讼撒,只需對新加入的數(shù)據(jù)進行初始化狀態(tài)浑厚,已有老數(shù)據(jù)不進行狀態(tài)改變:
if (node.isNewAdd && defaultExpandLeval >= currentLevel) {
node.setExpand(true);
}
這樣股耽,我們可以保持動態(tài)更新添加數(shù)據(jù)又可以保持原有的展開或選中狀態(tài)不發(fā)生變化,實現(xiàn)我們的需求钳幅。