男人应似海  

自定义树形结构组件—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);//设置节点点击事件监听

    }

}

posted on 2011-11-30 11:50  男人应似海  阅读(19013)  评论(12编辑  收藏  举报