如何在Xamarin Android中实现多级ExpandableListView或树形视图
嘿,刚接触Xamarin Android就能搞定单级ListView,已经很厉害啦!下面我给你详细捋捋ExpandableListView的二级实现,还有多级树形视图的两种思路,结合你的代码场景一步步来~
一、先搞定原生二级ExpandableListView
ExpandableListView本身就是官方为二级列表设计的组件,核心是自定义BaseExpandableListAdapter,步骤如下:
1. 准备层级数据结构
先定义两个简单的模型类,分别存组项和子项的数据:
// 一级组项模型 public class GroupModel { public string GroupName { get; set; } public List<ChildModel> ChildItems { get; set; } } // 二级子项模型 public class ChildModel { public string ChildName { get; set; } }
2. 自定义ExpandableListAdapter
继承BaseExpandableListAdapter,实现组和子项的视图渲染、数据绑定逻辑:
public class CustomExpandableAdapter : BaseExpandableListAdapter { private readonly Context _context; private readonly List<GroupModel> _groupData; public CustomExpandableAdapter(Context context, List<GroupModel> groupData) { _context = context; _groupData = groupData; } // 获取指定组的子项数量 public override int GetChildrenCount(int groupPosition) { return _groupData[groupPosition].ChildItems.Count; } // 获取指定位置的子项数据 public override Java.Lang.Object GetChild(int groupPosition, int childPosition) { return _groupData[groupPosition].ChildItems[childPosition]; } public override long GetChildId(int groupPosition, int childPosition) { return childPosition; } // 渲染子项视图 public override View GetChildView(int groupPosition, int childPosition, bool isLastChild, View convertView, ViewGroup parent) { var child = (ChildModel)GetChild(groupPosition, childPosition); if (convertView == null) { var inflater = (LayoutInflater)_context.GetSystemService(Context.LayoutInflaterService); convertView = inflater.Inflate(Resource.Layout.child_item_layout, null); } var childTxt = convertView.FindViewById<TextView>(Resource.Id.child_txt); childTxt.Text = child.ChildName; return convertView; } // 获取指定组的数据 public override Java.Lang.Object GetGroup(int groupPosition) { return _groupData[groupPosition]; } // 获取组的总数 public override int GroupCount => _groupData.Count; public override long GetGroupId(int groupPosition) { return groupPosition; } // 渲染组项视图 public override View GetGroupView(int groupPosition, bool isExpanded, View convertView, ViewGroup parent) { var group = (GroupModel)GetGroup(groupPosition); if (convertView == null) { var inflater = (LayoutInflater)_context.GetSystemService(Context.LayoutInflaterService); convertView = inflater.Inflate(Resource.Layout.group_item_layout, null); } var groupTxt = convertView.FindViewById<TextView>(Resource.Id.group_txt); groupTxt.Text = group.GroupName; // 可选:添加展开/折叠箭头图标 var arrowImg = convertView.FindViewById<ImageView>(Resource.Id.expand_arrow); arrowImg.SetImageResource(isExpanded ? Resource.Drawable.ic_expanded : Resource.Drawable.ic_collapsed); return convertView; } public override bool IsChildSelectable(int groupPosition, int childPosition) { return true; // 允许子项被点击 } public override bool HasStableIds => true; }
3. 准备布局文件
- 主布局(Main.axml):添加ExpandableListView
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ExpandableListView android:id="@+id/main_expandable_list" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
- 组项布局(group_item_layout.axml)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:orientation="horizontal" android:gravity="center_vertical"> <TextView android:id="@+id/group_txt" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:textSize="18sp" android:textStyle="bold"/> <ImageView android:id="@+id/expand_arrow" android:layout_width="24dp" android:layout_height="24dp" android:src="@drawable/ic_collapsed"/> </LinearLayout>
- 子项布局(child_item_layout.axml)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="32dp" android:paddingVertical="12dp"> <TextView android:id="@+id/child_txt" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16sp"/> </LinearLayout>
4. 在MainActivity中绑定数据
把你的MainActivity代码补充完整,初始化数据并绑定适配器:
protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); SetContentView(Resource.Layout.Main); // 初始化测试数据 var groupList = new List<GroupModel> { new GroupModel { GroupName = "系统设置", ChildItems = new List<ChildModel> { new ChildModel{ChildName="显示设置"}, new ChildModel{ChildName="声音设置"}, new ChildModel{ChildName="隐私设置"} } }, new GroupModel { GroupName = "应用管理", ChildItems = new List<ChildModel> { new ChildModel{ChildName="已安装应用"}, new ChildModel{ChildName="权限管理"} } } }; // 绑定适配器 var expandableList = FindViewById<ExpandableListView>(Resource.Id.main_expandable_list); var adapter = new CustomExpandableAdapter(this, groupList); expandableList.SetAdapter(adapter); // 可选:监听组点击事件,手动控制展开/折叠 expandableList.GroupClick += (sender, e) => { if (expandableList.IsGroupExpanded(e.GroupPosition)) { expandableList.CollapseGroup(e.GroupPosition); } else { expandableList.ExpandGroup(e.GroupPosition); } e.Handled = true; }; // 可选:监听子项点击事件 expandableList.ChildClick += (sender, e) => { var selectedChild = (ChildModel)adapter.GetChild(e.GroupPosition, e.ChildPosition); Toast.MakeText(this, $"选中了:{selectedChild.ChildName}", ToastLength.Short).Show(); }; }
二、实现多级/树形视图(三级及以上)
ExpandableListView原生只支持二级,要做三级或无限层级的树形结构,有两种常用思路:
1. 递归嵌套Adapter(推荐)
让每个子项也具备展开/折叠能力,也就是在子项的布局中嵌套另一个ExpandableListView,递归创建适配器。首先修改数据模型为树形结构:
public class TreeNodeModel { public string NodeName { get; set; } public List<TreeNodeModel> ChildNodes { get; set; } public bool IsExpanded { get; set; } // 标记当前节点是否展开 }
然后自定义TreeAdapter,在子项视图中嵌入ExpandableListView,递归绑定子节点数据。这种方式能支持无限层级,交互体验更接近原生树形控件。
2. 扁平化数据+缩进模拟
把所有层级的数据扁平化,通过设置不同的左边距来区分层级(比如一级无缩进,二级缩进32dp,三级缩进64dp),用普通ListView实现。这种方式简单易上手,适合层级不多的场景:
// 扁平化树形数据模型 public class FlatTreeItem { public string ItemName { get; set; } public int Level { get; set; } // 0=一级,1=二级,2=三级... } // ListView适配器中设置缩进 public class FlatTreeAdapter : BaseAdapter<FlatTreeItem> { private readonly Context _context; private readonly List<FlatTreeItem> _dataList; public FlatTreeAdapter(Context context, List<FlatTreeItem> dataList) { _context = context; _dataList = dataList; } public override FlatTreeItem this[int position] => _dataList[position]; public override int Count => _dataList.Count; public override long GetItemId(int position) => position; public override View GetView(int position, View convertView, ViewGroup parent) { var item = _dataList[position]; if (convertView == null) { convertView = LayoutInflater.From(_context).Inflate(Resource.Layout.flat_tree_item, null); } var txt = convertView.FindViewById<TextView>(Resource.Id.tree_item_txt); txt.Text = item.ItemName; // 根据层级设置左边距 int indent = item.Level * 32; // 每级缩进32dp txt.SetPadding(indent, txt.PaddingTop, txt.PaddingRight, txt.PaddingBottom); return convertView; } }
三、小提示
- 递归嵌套Adapter时,注意避免Activity的强引用导致内存泄漏,可以用
WeakReference包装Context。 - 展开/折叠的动画可以通过Android的属性动画(
PropertyAnimation)自定义实现,提升交互体验。 - 数据量大时,一定要做好ViewHolder复用,避免列表卡顿。
内容的提问来源于stack exchange,提问作者Rachel
相关产品推荐
相关产品推荐

