更快實現Android多級樹形選擇列表
快速實現Android多級樹形列表,這個庫是在鴻洋多級樹形列表demo中修改而來。
解決的問題:
-
支持ID為int類型和String類型。
-
支持多級復選框選中,使用只需一行代碼。
-
支持動態更新數據并保持原有展開/關閉狀態。
-
支持ListView、RecyclerView。
一、概述
這幾天項目中有一個多級列表的菜單,最開始給我的感覺應該就是嵌套ListView,或者用ExpandableListView,但問題是ExpandableListView只支持兩級列表,而且關鍵的是具體分幾級是不確定的,也就是可能一級,可能多級,這要是五六級嵌套ListView,想想那酸爽。。。最終在偷懶心態的驅使下到網上查查看有沒有類似的,也確實查到鴻洋大佬之前寫的一篇關于實現Android多級樹形列表的文章,實現很巧妙,使用一個ListView就可以實現多級列表效果,就download下demo,在demo基礎上做了部分修改,功能順利實現。
其實到這里應該就結束了,但使用過程中遇到的一些問題讓我覺得這個可以進一步優化,比如我要做個多級復選列表,每次處理子級選中與父級選中搞得很累,生怕哪個遞歸錯了。而且在新增數據的時候刷新頁面也需要自己處理,直接刷新沒有效果,再加上現在RecyclerView已經用的越來越多了。就想著在周末好好總結下,封裝個方便使用的庫,方便下一次有類似需求的時候使用。說到底,還是為了下次可以偷偷懶唄。如果小伙伴有類似需求,也可以直接拿來用~
先看下效果吧:
MultilevelTreeList
這篇文章主要介紹這個庫如何使用,如果對具體實現細節感興趣,可以查看源碼或者搜索鴻洋的博客。
二、具體使用
我們關聯列表樹需要有三個必須元素,當前id、父級id即pid,顯示的內容。id和pid可以為int或者String以及其他類型。要顯示的內容需要包裝一下:
//id pid name FileNode為實際用的實體Bean對象
mlist.add(new Node("223","0","我也是添加的root節點",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:
//第一個參數 ListView
//第二個參數 上下文
//第三個參數 數據集
//第四個參數 默認展開層級數 0為不展開
//第五個參數 展開的圖標
//第六個參數 閉合的圖標
mAdapter = new SimpleTreeAdapter(mTree, ListViewActivity.this,
mDatas, 1,R.mipmap.tree_ex,R.mipmap.tree_ec);
mTree.setAdapter(mAdapter);
RecyclerView:
//第一個參數 RecyclerView
//第二個參數 上下文
//第三個參數 數據集
//第四個參數 默認展開層級數 0為不展開
//第五個參數 展開的圖標
//第六個參數 閉合的圖標
mAdapter = new SimpleTreeRecyclerAdapter(mTree, RecyclerViewActivity.this,
mDatas, 1,R.mipmap.tree_ex,R.mipmap.tree_ec);
mTree.setAdapter(mAdapter);
添加數據,可以保持原有選中或者展開狀態:
List<Node> mlist = new ArrayList<>();
mlist.add(new Node("223","0","我也是添加的root節點",new FileNode()));
mAdapter.addData(0,mlist);
獲取選中內容:如果node的isChecked()為true,即為選中狀態。
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();
控制父子之間聯動的選中與取消狀態,只需調用 setChecked 方法既可 ,注意如果在 setOnCheckedChangeListener 中處理會有問題:因為如果要子節點/父節點選中或者取消需要刷新頁面,而刷新頁面又會觸發 viewHolder.cb.setChecked(true/false); 的判斷從而又會進入 setOnCheckedChangeListener ,會導致如果父節點選中某些子節點取消不了的情況。
//viewHolder.cb 為CheckBox
viewHolder.cb.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setChecked(node,viewHolder.cb.isChecked());
}
});
三、簡單介紹
通過一個ListView來展示所有數據,每一級內容的顯示根據當前展示數據的等級縮進一定的 padding 值,讓我們看起來有縮進效果。
使用過程中感覺不是很舒服的地方在于最終用于顯示在界面實體Bean并不是我們傳進去的數據,而是經過轉化并且過濾的數據,這樣最直接的影響就是在我新增數據的數據之后,拿著Adapter來刷新的時候,并沒有任何效果。因為我們沒有將后面新加的數據進行轉化。
而我們如何能在不改變原有數據結構的基礎上,添加我們的新內容,并保持原有的選中或者展開正常呢?我的想法是這樣的,如果可以直接給它傳入轉化后的Node節點類型數據就好了,我想到了繼承,讓實體類去繼承基類Node,但一旦繼承Node則意味著實體類就不能再繼承其他類了,感覺不是很靈活,而且也影響了實體類本身的結構。后來想到了包裝設計模式的一些東西,那我就在實體類外再包上一層,也就是將實體類傳給Node,最終我們使用的還是Node,但也可以用 node.bean 很輕松的取出實體類做其他操作,并且實體類本身的結構并沒有被破壞。
在此基礎上,因為我們的Node不需要轉化重新創建,那么它就可以保存一些狀態比如展開、選中等等,而在新加入數據時只需標記下新加入的數據,只需對新加入的數據進行初始化狀態,已有老數據不進行狀態改變:
if (node.isNewAdd && defaultExpandLeval >= currentLevel) {
node.setExpand(true);
}
這樣,我們可以保持動態更新添加數據又可以保持原有的展開或選中狀態不發生變化,實現我們的需求。
來自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0115/7024.html