目录
3.5 右键创建一个Empty Activity 命名为:AlertTreectivity
3.6 在res/layout创建一个view_tree.xml文件
3.7 在res/layout下创建tree_item.xml文件
3.8 创建一个TreeListViewAdapter适配器类
3.9 创建一个属于自己自定义的MyTreeListViewAdapter适配器类
3.11 更改AndroidManifest配置文件,使运行的时候访问AlertTreectivity类
一. 简介
最近接到一个需求,需要开发一个树状的下拉框,于是就开始百度,经过大量的查找和自己一些的改进,终于做出一个实用简单的 Android 树状下拉框。
1.1 效果图
二. 设计思路
2.1 数据设计
树状下拉框第一个想到的就层级的判断 ,例如谁是谁的上一层,谁是谁的下一层,所以数据只需要3个值就行了,分别是节点ID,节点名称,父节点;如下图表,湖南和广东都是中国的,所以他们的父节点都是1,深圳的级别在广东之下,所以父节点对应的广东ID就是3。
设计表如下:
ID 节点名称 父节点
1 中国 0
2 湖南 1
3 广东 1
4 深圳 3
2.2 递归判断层级
数据设计完成,下一步就是如何判断他们层级关系,就要用到递归了。由于本人是萌新,递归的逻辑是在网上参考别人的,所以大致原理是这样,让我们直接进入开发过程。
三. 开发过程
3.1 创建一个我的节点类
package top.xiewenwen.alerttreeview.vo;
public class MyNodeVo {
/**
* 节点Id
*/
private int id;
/**
* 节点父id
*/
private int pId;
/**
* 节点name
*/
private String name;
/**
*
*/
private String desc;
/**
* 节点名字长度
*/
private long length;
public MyNodeVo(int id, int pId, String name) {
super();
this.id = id;
this.pId = pId;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPid() {
return pId;
}
public void setPid(int pId) {
this.pId = pId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
}
3.2 再创建一个节点NodeVo类
package top.xiewenwen.alerttreeview.vo;
import java.util.ArrayList;
import java.util.List;
public class NodeVo {
/**
* 节点id
*/
private int id;
/**
* 父节点id
*/
private int pId;
/**
* 是否展开
*/
private boolean isExpand = false;
private boolean isChecked = false;
private boolean isHideChecked = true;
/**
* 节点名字
*/
private String name;
/**
* 节点级别
*/
private int level;
/**
* 节点展示图标
*/
private int icon;
/**
* 节点所含的子节点
*/
private List<NodeVo> childrenNodeVos = new ArrayList<>();
/**
* 节点的父节点
*/
private NodeVo parent;
public NodeVo(int id, int pId, String name) {
super();
this.id = id;
this.pId = pId;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getpId() {
return pId;
}
public void setpId(int pId) {
this.pId = pId;
}
public boolean isExpand() {
return isExpand;
}
/**
* 当父节点收起,其子节点也收起
* @param isExpand
*/
public void setExpand(boolean isExpand) {
this.isExpand = isExpand;
if (!isExpand) {
for (NodeVo NodeVo : childrenNodeVos) {
NodeVo.setExpand(isExpand);
}
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getLevel() {
return parent == null ? 0 : parent.getLevel() + 1;
}
public void setLevel(int level) {
this.level = level;
}
public int getIcon() {
return icon;
}
public void setIcon(int icon) {
this.icon = icon;
}
public List<NodeVo> getChildrenNodeVos() {
return childrenNodeVos;
}
public NodeVo getParent() {
return parent;
}
public void setParent(NodeVo parent) {
this.parent = parent;
}
/**
* 判断是否是根节点
*
* @return
*/
public boolean isRoot() {
return parent == null;
}
/**
* 判断是否是叶子节点
*
* @return
*/
public boolean isLeaf() {
return childrenNodeVos.size() == 0;
}
/**
* 判断父节点是否展开
*
* @return
*/
public boolean isParentExpand()
{
if (parent == null)
return false;
return parent.isExpand();
}
public boolean isChecked() {
return isChecked;
}
public void setChecked(boolean isChecked) {
this.isChecked = isChecked;
}
public boolean isHideChecked() {
return isHideChecked;
}
public void setHideChecked(boolean isHideChecked) {
this.isHideChecked = isHideChecked;
}
}
3.3 把下面两张图片放入res/drawable
命名为:tree_econpand.png 命名为:tree_expand.png
3.4 创建TreeUtil工具类
package top.xiewenwen.alerttreeview.util;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import top.xiewenwen.alerttreeview.R;
import top.xiewenwen.alerttreeview.vo.NodeVo;
public class TreeUtil {
/**
* 根据所有节点获取可见节点
*
* @param allNodeVos
* @return
*/
public static List<NodeVo> filterVisibleNodeVo(List<NodeVo> allNodeVos) {
List<NodeVo> visibleNodeVos = new ArrayList<NodeVo>();
for (NodeVo NodeVo : allNodeVos) {
// 如果为根节点,或者上层目录为展开状态
if (NodeVo.isRoot() || NodeVo.isParentExpand()) {
setNodeVoIcon(NodeVo);
visibleNodeVos.add(NodeVo);
}
}
return visibleNodeVos;
}
/**
* 获取排序的所有节点
*
* @param datas
* @param defaultExpandLevel
* @return
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public static <T> List<NodeVo> getSortedNodeVos(List<T> datas,
int defaultExpandLevel, boolean isHide)
throws IllegalAccessException, IllegalArgumentException {
List<NodeVo> sortedNodeVos = new ArrayList<NodeVo>();
// 将用户数据转化为List<NodeVo>
List<NodeVo> NodeVos = convertData2NodeVos(datas, isHide);
// 拿到根节点
List<NodeVo> rootNodeVos = getRootNodeVos(NodeVos);
// 排序以及设置NodeVo间关系
for (NodeVo NodeVo : rootNodeVos) {
addNodeVo(sortedNodeVos, NodeVo, defaultExpandLevel, 1);
}
return sortedNodeVos;
}
/**
* 把一个节点上的所有的内容都挂上去
*/
private static void addNodeVo(List<NodeVo> NodeVos, NodeVo NodeVo,
int defaultExpandLeval, int currentLevel) {
NodeVos.add(NodeVo);
if (defaultExpandLeval >= currentLevel) {
NodeVo.setExpand(true);
}
if (NodeVo.isLeaf())
return;
for (int i = 0; i < NodeVo.getChildrenNodeVos().size(); i++) {
addNodeVo(NodeVos, NodeVo.getChildrenNodeVos().get(i), defaultExpandLeval,
currentLevel + 1);
}
}
/**
* 获取所有的根节点
*
* @param NodeVos
* @return
*/
public static List<NodeVo> getRootNodeVos(List<NodeVo> NodeVos) {
List<NodeVo> rootNodeVos = new ArrayList<NodeVo>();
for (NodeVo NodeVo : NodeVos) {
if (NodeVo.isRoot()) {
rootNodeVos.add(NodeVo);
}
}
return rootNodeVos;
}
/**
* 将泛型datas转换为NodeVo
*
* @param datas
* @return
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public static <T> List<NodeVo> convertData2NodeVos(List<T> datas, boolean isHide)
throws IllegalAccessException, IllegalArgumentException {
List<NodeVo> NodeVos = new ArrayList<NodeVo>();
NodeVo NodeVo = null;
for (T t : datas) {
int id = -1;
int pId = -1;
String name = null;
Class<? extends Object> clazz = t.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
/**
* 与MyNodeVoBean实体一一对应
*/
for (Field f : declaredFields) {
if ("id".equals(f.getName())) {
f.setAccessible(true);
id = f.getInt(t);
}
if ("pId".equals(f.getName())) {
f.setAccessible(true);
pId = f.getInt(t);
}
if ("name".equals(f.getName())) {
f.setAccessible(true);
name = (String) f.get(t);
}
if ("desc".equals(f.getName())) {
continue;
}
if ("length".equals(f.getName())) {
continue;
}
if (id == -1 && pId == -1 && name == null) {
break;
}
}
NodeVo = new NodeVo(id, pId, name);
NodeVo.setHideChecked(isHide);
NodeVos.add(NodeVo);
}
/**
* 比较NodeVos中的所有节点,分别添加children和parent
*/
for (int i = 0; i < NodeVos.size(); i++) {
NodeVo n = NodeVos.get(i);
for (int j = i + 1; j < NodeVos.size(); j++) {
NodeVo m = NodeVos.get(j);
if (n.getId() == m.getpId()) {
n.getChildrenNodeVos().add(m);
m.setParent(n);
} else if (n.getpId() == m.getId()) {
n.setParent(m);
m.getChildrenNodeVos().add(n);
}
}
}
for (NodeVo n : NodeVos) {
setNodeVoIcon(n);
}
return NodeVos;
}
/**
* 设置打开,关闭icon
*
* @param NodeVo
*/
public static void setNodeVoIcon(NodeVo NodeVo) {
if (NodeVo.getChildrenNodeVos().size() > 0 && NodeVo.isExpand()) {
NodeVo.setIcon(R.drawable.tree_expand);
} else if (NodeVo.getChildrenNodeVos().size() > 0 && !NodeVo.isExpand()) {
NodeVo.setIcon(R.drawable.tree_econpand);
} else
NodeVo.setIcon(-1);
}
public static void setNodeVoChecked(NodeVo NodeVo, boolean isChecked) {
// 自己设置是否选择
NodeVo.setChecked(isChecked);
/**
* 非叶子节点,子节点处理
*/
setChildrenNodeVoChecked(NodeVo, isChecked);
/** 父节点处理 */
setParentNodeVoChecked(NodeVo);
}
/**
* 非叶子节点,子节点处理
*/
private static void setChildrenNodeVoChecked(NodeVo NodeVo, boolean isChecked) {
NodeVo.setChecked(isChecked);
if (!NodeVo.isLeaf()) {
for (NodeVo n : NodeVo.getChildrenNodeVos()) {
// 所有子节点设置是否选择
setChildrenNodeVoChecked(n, isChecked);
}
}
}
/**
* 设置父节点选择
*
* @param NodeVo
*/
private static void setParentNodeVoChecked(NodeVo NodeVo) {
/** 非根节点 */
if (!NodeVo.isRoot()) {
NodeVo rootNodeVo = NodeVo.getParent();
boolean isAllChecked = true;
for (NodeVo n : rootNodeVo.getChildrenNodeVos()) {
if (!n.isChecked()) {
isAllChecked = false;
break;
}
}
if (isAllChecked) {
rootNodeVo.setChecked(true);
} else {
rootNodeVo.setChecked(false);
}
setParentNodeVoChecked(rootNodeVo);
}
}
}
3.5 右键创建一个Empty Activity 命名为:AlertTreectivity
创建Activity会在res/layout自动创建一个activity_alert_treectivity.xml文件,修改这个布局文件里的代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AlertTreectivity">
<EditText
android:id="@+id/getTreeIdName"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:ems="10"
android:hint="请点击下面的选择按钮"
android:inputType="textPersonName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/choice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="选择"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/getTreeIdName" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.6 在res/layout创建一个view_tree.xml文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<ListView
android:id="@+id/tree_lv"
android:layout_width="0dp"
android:layout_height="430dp"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</ListView>
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textSize="22sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tree_lv" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.7 在res/layout下创建tree_item.xml文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="@+id/id_treenode_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerVertical="true"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:src="@drawable/tree_econpand" />
<TextView
android:id="@+id/id_treenode_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/id_treenode_icon"
android:textSize="18sp" />
</RelativeLayout>
3.8 创建一个TreeListViewAdapter适配器类
package top.xiewenwen.alerttreeview.Adapter;
import java.util.List;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import top.xiewenwen.alerttreeview.util.TreeUtil;
import top.xiewenwen.alerttreeview.vo.NodeVo;
/**
* tree适配器
*
* @param <T>
*/
public abstract class TreeListViewAdapter<T> extends BaseAdapter {
protected Context mContext;
/**
* 存储所有可见的NodeVo
*/
protected List<NodeVo> mNodeVos;
protected LayoutInflater mInflater;
/**
* 存储所有的NodeVo
*/
protected List<NodeVo> mAllNodeVos;
/**
* 点击的回调接口
*/
private OnTreeNodeClickListener onTreeNodeVoClickListener;
public interface OnTreeNodeClickListener {
/**
* 处理NodeVo click事件
*
* @param Node 节点对象
* @param position 位置
*/
void onClick(NodeVo Node, int position);
}
public void setOnTreeNodeClickListener(
OnTreeNodeClickListener onTreeNodeVoClickListener) {
this.onTreeNodeVoClickListener = onTreeNodeVoClickListener;
}
/**
* @param mTree
* @param context
* @param datas
* @param defaultExpandLevel 默认展开几级树
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
public TreeListViewAdapter(ListView mTree, Context context, List<T> datas, int defaultExpandLevel, boolean isHide)
throws IllegalArgumentException, IllegalAccessException {
mContext = context;
/**
* 对所有的Node进行排序
*/
mAllNodeVos = TreeUtil
.getSortedNodeVos(datas, defaultExpandLevel, isHide);
/**
* 过滤出可见的Node
*/
mNodeVos = TreeUtil.filterVisibleNodeVo(mAllNodeVos);
mInflater = LayoutInflater.from(context);
/**
* 设置节点点击时,可以展开以及关闭;并且将ItemClick事件继续往外公布
*/
mTree.setOnItemClickListener((parent, view, position, id) -> {
expandOrCollapse(position);
if (onTreeNodeVoClickListener != null) {
onTreeNodeVoClickListener.onClick(mNodeVos.get(position), position);
}
});
}
/**
* 相应ListView的点击事件 展开或关闭某节点
*
* @param position 位置
*/
public void expandOrCollapse(int position) {
NodeVo n = mNodeVos.get(position);
if (n != null)// 排除传入参数错误异常
{
if (!n.isLeaf()) {
n.setExpand(!n.isExpand());
mNodeVos = TreeUtil.filterVisibleNodeVo(mAllNodeVos);
notifyDataSetChanged();// 刷新视图
}
}
}
@Override
public int getCount() {
return mNodeVos.size();
}
@Override
public Object getItem(int position) {
return mNodeVos.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final NodeVo NodeVo = mNodeVos.get(position);
convertView = getConvertView(NodeVo, position, convertView, parent);
// 设置内边距
convertView.setPadding(NodeVo.getLevel() * 30, 3, 3, 3);
if (!NodeVo.isHideChecked()) {
//获取各个节点所在的父布局
RelativeLayout myView = (RelativeLayout) convertView;
//父布局下的CheckBox
myView.getChildAt(1);
}
return convertView;
}
public abstract View getConvertView(NodeVo NodeVo, int position, View convertView, ViewGroup parent);
}
3.9 创建一个属于自己自定义的MyTreeListViewAdapter适配器类
package top.xiewenwen.alerttreeview.Adapter;
import java.util.List;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import top.xiewenwen.alerttreeview.R;
import top.xiewenwen.alerttreeview.vo.NodeVo;
public class MyTreeListViewAdapter<T> extends TreeListViewAdapter<T> {
public MyTreeListViewAdapter(ListView mTree, Context context,
List<T> datas, int defaultExpandLevel, boolean isHide)
throws IllegalArgumentException, IllegalAccessException {
super(mTree, context, datas, defaultExpandLevel, isHide);
}
@Override
public View getConvertView(NodeVo node, int position, View convertView,
ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.tree_item, parent, false);
viewHolder = new ViewHolder();
viewHolder.icon = convertView.findViewById(R.id.id_treenode_icon);
viewHolder.label = convertView.findViewById(R.id.id_treenode_name);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
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 static final class ViewHolder {
ImageView icon;
TextView label;
}
}
3.10 修改AlertTreectivity类里代码
package top.xiewenwen.alerttreeview;
import androidx.appcompat.app.AppCompatActivity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import top.xiewenwen.alerttreeview.Adapter.MyTreeListViewAdapter;
import top.xiewenwen.alerttreeview.vo.MyNodeVo;
public class AlertTreectivity extends AppCompatActivity {
private final List<MyNodeVo> mDatas = new ArrayList<>();
private AlertDialog mAlert;
private Button choice;
private MyTreeListViewAdapter<MyNodeVo> adapter;
private TextView getTreeIdName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alert_treectivity);
getTreeIdName=findViewById(R.id.getTreeIdName);
choice=findViewById(R.id.choice);
initTreeListView();
}
private void initTreeListView() {
// 从布局文件中加载 AlertDialog 需要显示的 view
LayoutInflater inflater = this.getLayoutInflater();
View customView = inflater.inflate(R.layout.view_tree, null, false);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// 指定 AlertDialog 需要显示的 view
builder.setView(customView);
// 点击空白处是否自动隐藏对话框(默认值为 true)
builder.setCancelable(true);
// 创建 AlertDialog 对象
mAlert = builder.create();
// 弹出自定义对话框
choice.setOnClickListener(v -> mAlert.show());
// 设置自定义 view 中的显示内容
ListView treeLv = customView.findViewById(R.id.tree_lv);
initDatas();
try {
adapter = new MyTreeListViewAdapter<>(treeLv, this,
mDatas, 0, true);
adapter.setOnTreeNodeClickListener((node, position) -> {
if (node.isLeaf()) {
getTreeIdName.setText(node.getName());
mAlert.dismiss();
}
});
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
treeLv.setAdapter(adapter);
// 自定义 view 中的关闭按钮的点击事件
customView.findViewById(R.id.cancel).setOnClickListener(v -> mAlert.dismiss());
}
private void initDatas() {
mDatas.add(new MyNodeVo(1, 0, "中国"));
mDatas.add(new MyNodeVo(2, 1, "湖南"));
mDatas.add(new MyNodeVo(3, 1, "广东"));
mDatas.add(new MyNodeVo(4, 3, "深圳"));
}
}
3.11 更改AndroidManifest配置文件,使运行的时候访问AlertTreectivity类
<activity
android:name=".AlertTreectivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
四. 总结
学习了3种数据可以创建可视化的树状结构,用递归来循环判断他们的子集关系,学会了怎么通过A获取到B的View视图,学会了怎么重写Adapter适配器类。
五. 代码开源
Android TreeListView: 安卓点击按钮弹出树状下拉框,选择后显示在EditText上 (gitee.com)
六. 参考文献
TreeListView: TreeListView (gitee.com)
一手遮天 Android - view(弹出类): PopupWindow 基础 - webabcd - 博客园 (cnblogs.com)