自定义树形结构组件—TreeView
注:该文章为(男人应似海)原创,如需转载请注明出处!
组件功能
实现树形结构目录效果
效果图
实现方式
用两个树形节点类集合分别去存储所有节点(List<TreeElement> treeElements)和当前显示节点(List<TreeElement> currentElements),当前显示节点集合currentElements中的数据显示在ListView中。当点击含有子节点的节点时(如上图中的A、B、C、CC11)会把相应的子节点从所有节点集合treeElements中找到并添加当前显示节点集合currentElements中在ListView上显示或从currentElements中删除并从ListView刷新数据。
关于数据获取
这里获取树节点数据的方式是在工程资源中assets文件下创建一个本地文件(先在assets文件下创建textRes文件夹,然后在textRes文件夹下创建一个记录数据的文件,这里是treeview_elements.properties)如下图所示。
这个文件以某种格式记录数据参数(和树节点类属性对应),具体请查看下面的代码解释。
treeview_elements.properties文件中数据如下:
#id(s)-level(int)-title(s)-fold(b)-hasChild(b)-hasParent(b)-parentId(s)
1011-1-A-false-true-false-null
1021-2-AA11-false-false-true-1011
1022-2-AA22-false-false-true-1011
1023-2-AA33-false-false-true-1011
1024-2-AA44-false-false-true-1011
2011-1-B-false-true-false-null
2021-2-BB11-false-false-true-2011
2022-2-BB22-false-false-true-2011
2023-2-BB33-false-false-true-2011
2024-2-BB44-false-false-true-2011
3011-1-C-false-true-false-null
3021-2-CC11-false-true-true-3011
3031-3-CCC111-false-false-true-3021
3032-3-CCC222-false-false-true-3021
3033-3-CCC333-false-false-true-3021
以#开始的行为注释行,用于说明数据的含义和属性,其他的每行数据都代表了一个树节点对象实例TreeElement。如1021-2-AA11-false-false-true-1011解释如下:
数据 |
含义 |
1021 |
表示该节点id为1021 |
2 |
表示该节点为2级节点 |
AA11 |
表示该节点的标题或内容为AA11 |
false |
表示该节点当前是处于为展开状态,即子节点不可见(只针对有子节点的节点) |
false |
表示该节点没有子节点 |
true |
表示该节点有父节点 |
1011 |
表示该节点的父节点是1011 |
这种方式适合于数据量大且数据固定的情况,当数据较少时我们也可以通过直接new树节点对象来得到树节点集合。如果需要获取网络数据,也可以指定服务器返回特定格式的json数据,然后解析。
代码
TreeElement.java
/**
* 类名:TreeElement.java
* 类描述:树形结构节点类
* @author wader
* 创建时间:2011-11-03 16:32
*/
public class TreeElement {
String id = null;// 当前节点id
String title = null;// 当前节点文字
boolean hasParent = false;// 当前节点是否有父节点
boolean hasChild = false;// 当前节点是否有子节点
boolean childShow = false;// 如果子节点,字节当当前是否已显示
String parentId = null;// 父节点id
int level = -1;// 当前界面层级
boolean fold = false;// 是否处于展开状态
public boolean isChildShow() {
return childShow;
}
public void setChildShow(boolean childShow) {
this.childShow = childShow;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isHasParent() {
return hasParent;
}
public void setHasParent(boolean hasParent) {
this.hasParent = hasParent;
}
public boolean isHasChild() {
return hasChild;
}
public void setHasChild(boolean hasChild) {
this.hasChild = hasChild;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public boolean isFold() {
return fold;
}
public void setFold(boolean fold) {
this.fold = fold;
}
@Override
public String toString() {
return "id:" + this.id + "-level:" + this.level + "-title:"
+ this.title + "-fold:" + this.fold + "-hasChidl:"
+ this.hasChild + "-hasParent:" + this.hasParent + "-parentId:"+ this.parentId;
}
}
TreeViewAdapter.java
/**
* 类名:TreeViewAdapter.java
* 类描述:用于填充数据的类
* @author wader
* 创建时间:2011-11-03 16:32
*/
public class TreeViewAdapter extends BaseAdapter {
class ViewHolder {
ImageView icon;
TextView title;
}
Context context;
ViewHolder holder;
LayoutInflater inflater;
List<TreeElement> elements;
public TreeViewAdapter(Context context, List<TreeElement> elements) {
this.context = context;
this.elements = elements;
}
@Override
public int getCount() {
return elements.size();
}
@Override
public Object getItem(int position) {
return elements.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
/**
* ---------------------- get holder------------------------
*/
if (convertView == null) {
if (inflater == null) {
inflater = LayoutInflater.from(context);
}
holder = new ViewHolder();
convertView = inflater
.inflate(R.layout.tree_view_item_layout, null);
holder.icon = (ImageView) convertView
.findViewById(R.id.tree_view_item_icon);
holder.title = (TextView) convertView
.findViewById(R.id.tree_view_item_title);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
/**
* ---------------------- set holder--------------------------
*/
if (elements.get(position).isHasChild()) {// 有子节点,要显示图标
if (elements.get(position).isFold()) {
holder.icon.setImageResource(R.drawable.tree_view_icon_fold);
} else if (!elements.get(position).isFold()) {
holder.icon.setImageResource(R.drawable.tree_view_icon_unfold);
}
holder.icon.setVisibility(View.VISIBLE);
} else {// 没有子节点,要隐藏图标
holder.icon.setImageResource(R.drawable.tree_view_icon_fold);
holder.icon.setVisibility(View.INVISIBLE);
}
holder.icon.setPadding(25 * (elements.get(position).getLevel()), 0, 0, 0);// 根据层级设置缩进
holder.title.setText(elements.get(position).getTitle());
holder.title.setTextSize(40 - elements.get(position).getLevel() * 5); // 根据层级设置字体大小
return convertView;
}
}
TreeView.java
/**
* 类名:TreeView.java
* 类描述:实现树形结构的view
* @author wader
* 创建时间:2011-11-03 16:32
*/
public class TreeView extends ListView implements OnItemClickListener {
String TAG = "TreeView";
List<TreeElement> treeElements = null;// 所有节点集合
List<TreeElement> currentElements = null;// 当前显示的节点集合
List<TreeElement> tempElements = null;// 用于临时存储
List<TreeElement> treeElementsToDel; // 临时存储待删除的节点
TreeViewAdapter adapter = null;// 用于数据填充
LastLevelItemClickListener itemClickCallBack;// 用户点击事件回调
public TreeView(final Context context, AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "create with TreeView(Context context, AttributeSet attrs)");
treeElements = new ArrayList<TreeElement>();
currentElements = new ArrayList<TreeElement>();
adapter = new TreeViewAdapter(context, currentElements);
this.setAdapter(adapter);
itemClickCallBack = new LastLevelItemClickListener() {
@Override
public void onLastLevelItemClick(int position,TreeViewAdapter adapter) {
Log.d(TAG, "last level element "
+ currentElements.get(position).getTitle()
+ " is clicked");
Toast.makeText(context,
currentElements.get(position).getTitle(), 200).show();
}
};
this.setOnItemClickListener(this);
}
public void initData(Context context, List<TreeElement> treeElements) {
this.treeElements = treeElements;
getFirstLevelElements(context);
adapter.notifyDataSetChanged();
}
/**
* 设置点击事件回调接口
*
* @param itemClickCallBack
*/
public void setLastLevelItemClickCallBack(LastLevelItemClickListener itemClickCallBack) {
this.itemClickCallBack = itemClickCallBack;
}
/**
* 初始化树形结构列表数据,把第一层级的数据添加到currentElements中
*/
public void getFirstLevelElements(Context context) {
Log.d(TAG, "initCurrentElements");
int size = treeElements.size();
Log.d(TAG, "tree elements num is: " + size);
if (currentElements == null) {
currentElements = new ArrayList<TreeElement>();
}
currentElements.clear();
for (int i = 0; i < size; i++) {
if (treeElements.get(i).getLevel() == 1) {
currentElements.add(treeElements.get(i));
Log.d(TAG, "find a first level element: " + treeElements.get(i));
}
}
}
/**
* 从所有节点集合中获取某父节点的子节点集合
*
* @param parentId
* @return
*/
private List<TreeElement> getChildElementsFromAllById(String parentId) {
tempElements = new ArrayList<TreeElement>();
int size = treeElements.size();
for (int i = 0; i < size; i++) {
if (treeElements.get(i).getParentId().equalsIgnoreCase(parentId)) {
tempElements.add(treeElements.get(i));
Log.d(TAG, "find a child element: " + treeElements.get(i));
}
}
return tempElements;
}
/**
* 从当前显示的节点集合中获取某父节点的子节点集合
*
* @param parentId
* @return
*/
private List<TreeElement> getChildElementsFromCurrentById(String parentId) {
Log.d(TAG, "getChildElementsFromCurrentById parentId: " + parentId);
if (tempElements == null) {
tempElements = new ArrayList<TreeElement>();
} else {
tempElements.clear();
}
int size = currentElements.size();
for (int i = 0; i < size; i++) {
if (currentElements.get(i).getParentId().equalsIgnoreCase(parentId)) {
tempElements.add(currentElements.get(i));
Log.d(TAG,
"find a child element to delete: "
+ currentElements.get(i));
}
}
return tempElements;
}
/**
* 删除某父节点的所有子节点集合
*
* @param parentId
* @return
*/
private synchronized boolean delAllChildElementsByParentId(String parentId) {
Log.e(TAG, "delAllChildElementsByParentId: " + parentId);
int size;
TreeElement tempElement = currentElements
.get(getElementIndexById(parentId));
List<TreeElement> childElments = getChildElementsFromCurrentById(parentId);
if (treeElementsToDel == null) {
treeElementsToDel = new ArrayList<TreeElement>();
} else {
treeElementsToDel.clear();
}
size = childElments.size();
Log.e(TAG, "childElments size : " + size);
for (int i = 0; i < size; i++) {
tempElement = childElments.get(i);
if (tempElement.hasChild && tempElement.fold) {
treeElementsToDel.add(tempElement);
}
}
size = treeElementsToDel.size();
Log.e(TAG, "treeElementsToDel size : " + size);
for (int i = size - 1; i >= 0; i--) {
delAllChildElementsByParentId(treeElementsToDel.get(i).getId());
}
delDirectChildElementsByParentId(parentId);
return true;
}
/**
* 删除某父节点的直接子节点集合
*
* @param parentId
* @return
*/
private synchronized boolean delDirectChildElementsByParentId(
String parentId) {
Log.d(TAG, "delDirectChildElementsByParentId(): " + parentId);
boolean success = false;
if (currentElements == null || currentElements.size() == 0) {
Log.d(TAG,
"delChildElementsById() failed,currentElements is null or it's size is 0");
return success;
}
synchronized (currentElements) {
int size = currentElements.size();
Log.d(TAG, "begin delete");
for (int i = size - 1; i >= 0; i--) {
if (currentElements.get(i).getParentId()
.equalsIgnoreCase(parentId)) {
currentElements.get(i).fold = false;// 记得隐藏子节点时把展开状态设为false
currentElements.remove(i);
}
}
}
success = true;
return success;
}
/**
* 根据id查下标
*
* @param id
* @return
*/
private int getElementIndexById(String id) {
int num = currentElements.size();
for (int i = 0; i < num; i++) {
if (currentElements.get(i).getId().equalsIgnoreCase(id)) {
return i;
}
}
return -1;
}
@Override
public void onItemClick(AdapterView<?> arg0, View convertView,
int position, long id) {
TreeElement element = currentElements.get(position);
if (element.isHasChild()) {// 当前节点有子节点时只进行数据显示或隐藏等操作
if (!element.isFold()) {// 当前父节点为未展开状态
currentElements.addAll(position + 1,
this.getChildElementsFromAllById(element.getId()));
} else if (element.fold) {// 当前父节点为展开状态
boolean success = this.delAllChildElementsByParentId(element
.getId());
// boolean success =
// this.delDirectChildElementsByParentId(element
// .getId());
Log.d(TAG, "delete child state: " + success);
if (!success) {
return;
}
}
// 调试信息
// Log.d(TAG, "elements in currentElements:\n");
// for (int i = 0; i < currentElements.size(); i++) {
// Log.d(TAG + i, currentElements.get(i) + "\n");
// }
element.setFold(!element.isFold());// 设置反状态
adapter.notifyDataSetChanged();// 刷新数据显示
} else {// 当前节点有子节点时只进行用户自定义操作
itemClickCallBack.onLastLevelItemClick(position,adapter);
}
}
/**
* 自定义内部接口,用于用户点击最终节点时的事件回调
*/
public interface LastLevelItemClickListener {
public void onLastLevelItemClick(int position,TreeViewAdapter adapter);
}
}
ResManager.java
/**
* 类名:ResManager.java
* @author wader
* 类描述:获取工程中assets目下的文字、图片等资源
* 创建时间:2011-11-29 16:07
*/
public class ResManager {
/**
* 图片格式
*/
private static final String IMAGE_FILE_FORMAT = ".png";
/**
* 文本文件格式
*/
private static final String TEXT_FILE_FORMAT = ".properties";
/**
* 图片存放的路径
*/
public final static String IMAGES_DIR = "images/";
// public final static String IMAGES_DIR_480 = "images_480/";
public final static String TEXTS_DIR = "textRes/";
/**
* 文件路径
*/
private static String filePath = "";
/**
* 从工程资源加载图片资源(路径是assets/images/**.png)
*
* @param fileName
* 图片资源路径
*/
public static Bitmap loadImageRes(Activity activity, int screenWidth,
String fileName) {
Bitmap bitmap = null;
InputStream is = null;
FileInputStream fis = null;
filePath = IMAGES_DIR;
// 这里可以根据分辨率等进行路径区分判断
// if (screenWidth >= 480) {
// filePath = IMAGES_DIR_480;
// }
try {
is = activity.getAssets().open(
filePath + fileName + IMAGE_FILE_FORMAT);
if (is != null) {
bitmap = BitmapFactory.decodeStream(is);
}
} catch (Exception e) {
} finally {
try {
if (is != null) {
is.close();
}
if (fis != null) {
fis.close();
}
} catch (Exception e) {
} finally {
is = null;
fis = null;
}
}
return bitmap;
}
/**
* 从工程资源加载文字资源(路径是:assets/textRes/**.properties)
*
* @param fileName
*/
public static ArrayList<String> loadTextRes(String fileName, Context context) {
filePath = TEXTS_DIR;
return loadProperties(filePath + fileName + TEXT_FILE_FORMAT, context);
}
/**
* 读取配置文件读取配置信息
*
* @param filename
* 配置文件路径
* @return 包含配置信息的hashmap键值对
*/
private static ArrayList<String> loadProperties(String filename,
Context context) {
Log.d("loadProperties", "loadProperties");
ArrayList<String> properties = new ArrayList<String>();
InputStream is = null;
FileInputStream fis = null;
InputStreamReader rin = null;
// 将配置文件放到res/raw/目录下,可以通过以下的方法获取
// is = context.getResources().openRawResource(R.raw.system);
// 这是读取配置文件的第二种方法
// 将配置文件放到assets目录下,可以通过以下的方法获取
// is = context.getAssets().open("system.properties");
// 用来提取键值对的临时字符串
StringBuffer tempStr = new StringBuffer();
// 用来存放读取的每个字符
int ch = 0;
// 用来保存读取的配置文件一行的信息
String line = null;
try {
Log.d("loadProperties", "the filename is: " + filename);
is = context.getAssets().open(filename);
rin = new InputStreamReader(is, "UTF-8");
while (ch != -1) {
tempStr.delete(0, tempStr.length());
while ((ch = rin.read()) != -1) {
if (ch != '\n') {
tempStr.append((char) ch);
} else {
break;
}
}
line = tempStr.toString().trim();
Log.d("loadProperties", "line: " + line);
// 判断读出的那行数据是否有效,#开头的代表注释,如果是注释行那么跳过下面,继续上面操作
if (line.length() == 0 || line.startsWith("#")) {
continue;
}
properties.add(line);
}
} catch (IOException e) {
// LogX.trace("read property file", e.getMessage() + "fail");
} finally {
try {
if (is != null) {
is.close();
}
if (fis != null) {
fis.close();
}
if (null != rin) {
rin.close();
}
} catch (IOException e) {
// LogX.trace("read property file", e.getMessage() + "fail");
} finally {
is = null;
fis = null;
rin = null;
}
}
return properties;
}
}
TreeElementParser.java
/**
* 类名称:TreeElementParser.java
* @author wader
* 类描述:树形组件节点解析类,将树节点类(TreeElement)字符串信息集合解析为属性节点类集合
* 创建时间:2011—11-29 17:36
*/
public class TreeElementParser {
private static final String TAG = "TreeElementParser";
/**
* TreeElement的属性个数,可根据实际情况进行改动
*/
private static final int TREE_ELEMENT_ATTRIBUTE_NUM = 7;
/**
* 把节点字符串信息集合解析成节点集合 这里的解析可根据实际情况进行改动
*
* @param list
* @return
*/
public static List<TreeElement> getTreeElements(List<String> list) {
if (list == null) {
Log.d(TAG,
"the string list getted from solarterm.properties by ResManager.loadTextRes is null");
return null;
}
int elementNum = list.size();
List<TreeElement> treeElements = new ArrayList<TreeElement>();
String info[] = new String[TREE_ELEMENT_ATTRIBUTE_NUM];
for (int i = 0; i < elementNum; i++) {// 读取树形结构节点数
if (treeElements == null) {
treeElements = new ArrayList<TreeElement>();
}
info = list.get(i).split("-");
TreeElement element = new TreeElement();
element.setId(info[0]);
element.setLevel(Integer.valueOf(info[1]));
element.setTitle(info[2]);
element.setFold(Boolean.valueOf(info[3]));
element.setHasChild(Boolean.valueOf(info[4]));
element.setHasParent(Boolean.valueOf(info[5]));
element.setParentId(info[6]);
Log.d(TAG, "add a TreeElement: " + element.toString());
treeElements.add(element);
}
return treeElements;
}
}
Activity布局文件tree_view_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#669977"
android:orientation="vertical" >
<!-- 这里要写自定义组件的全路径,若果出现类解析错误请查看是否是这里路径问题-->
<com.dev.widget.treeview.view.TreeView
android:id="@+id/tree_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="#669977"
android:divider="#006600"
android:dividerHeight="1dip"
android:padding="10dip" >
</com.dev.widget.treeview.view.TreeView>
</LinearLayout>
ListView的Item布局文件tree_view_item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_vertical"
android:orientation="horizontal" >
<ImageView
android:id="@+id/tree_view_item_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tree_view_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="15dip"
android:text="title" />
</LinearLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dev.widget.treeview"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="4" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:label="@string/activity_name"
android:name=".TreeViewActivity" >
<intent-filter >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Activity界面代码 TreeViewActivity.java
public class TreeViewActivity extends Activity {
private TreeView treeView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tree_view_layout);
treeView = (TreeView) findViewById(R.id.tree_view);
/**
* 这部分可根据实际需要更换树形节点信息获取方式,如获取特定格式的json字符串
*/
List<String> treeElementsString = ResManager.loadTextRes(
"treeview_elements", this);// 读存放在工程assets/textRes目录下得文件资源
/**
* 我们也可以直接new出很多TreeElement实例然后得到树形节点的集合,但如果节点很多的话很不方便
*/
List<TreeElement> treeElements = TreeElementParser
.getTreeElements(treeElementsString);// 解析读出的文件资源内容
LastLevelItemClickListener itemClickCallBack = new LastLevelItemClickListener() {//创建节点点击事件监听
@Override
public void onLastLevelItemClick(int position,
TreeViewAdapter adapter) {
TreeElement element = (TreeElement) adapter.getItem(position);
Toast.makeText(getApplicationContext(), element.getTitle(), 300)
.show();
}
};
treeView.initData(this, treeElements);// 初始化数据
treeView.setLastLevelItemClickCallBack(itemClickCallBack);//设置节点点击事件监听
}
}