ExpandableListView实现可展开的ListView

最近朋友公司的一个项目中用到了一ListView的Item嵌套ListView的功能,朋友问我这种效果该怎么实现。ListView嵌套ListView的情况在实际开发中用到的还是比较多的。谷歌也给了我们一个这样的控件叫ExpandableListView,它可以实现item的展开效果。ExpandableListView除了比ListView多了几个方法外用法几乎和ListView用法一样,只要ListView用的溜,ExpandableListView很快就能上手。
下面将结合给朋友写的一个Demo来讲解ExpandableListView的用法,在使用ExpandableListView时候也踩到了不少坑,接下来都会作详细说明。
先来看用ExpandableListView实现的效果图吧(注意:demo中用到的数据都是假数据,项目中虽然定义了GroupBean和ChildBean,也将这两个Bean的List集合传递给了Adapter,但是实际并没有用到)
这里写图片描述
布局文件中的代码,添加一个ExpandableListView

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#EFEFEF">

    <ExpandableListView
        android:id="@+id/lv_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="#EFEFEF"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:dividerHeight="8dp"/>

</RelativeLayout>

MainActivity中只要获取到ExpandableListView,并为ExpandableListView设置Adapter即可,用法跟ListView一模一样的。代码如下:

mExpandableListView = (ExpandableListView) findViewById(R.id.lv_main);
        MyAdapter adapter = new MyAdapter(this, mGroupBeanList, mLists);
        mExpandableListView.setAdapter(adapter);
        //  隐藏自带的三角
        mExpandableListView.setGroupIndicator(null);
        Drawable drawable = getDrawable(R.drawable.shape5);
        mExpandableListView.setChildDivider(drawable);

上边代码作下说明:mExpandableListView.setGroupIndicator(null)是隐藏默认的指示图标,由于项目需要,这个Demo中的指示图标是自定义的,所以隐藏了默认的。盗张图,感受下默认的图标样式:
这里写图片描述
如果需要让指示图标显示在右边也是可以的,只需要添加下边两行代码即可:

int width = getWindowManager().getDefaultDisplay().getWidth();  
mExpandableListView.setIndicatorBounds(width-50, width); 

接下来看下GroupItem和ChildItem的布局文件
GroupItem布局效果图和代码如下:
这里写图片描述

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/rl_group_item"
                android:layout_width="match_parent"
                android:layout_height="105dp"
                android:background="@drawable/shape">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="105dp">


        <ImageView
            android:id="@+id/iv_group_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="4dp"
            android:layout_marginTop="12dp"
            android:background="@drawable/mine_card"/>
        <TextView
            android:id="@+id/tv_group_item_city"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignTop="@id/iv_group_item"
            android:layout_marginTop="10dp"
            android:layout_toRightOf="@id/iv_group_item"
            android:text="郑州-新乡"
            android:textColor="#333"
            android:textSize="15sp"
            android:textStyle="bold"/>
        <TextView
            android:id="@+id/tv_group_item_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_group_item_city"
            android:layout_marginTop="10dp"
            android:layout_toRightOf="@id/iv_group_item"
            android:text="09-25 15:21 出发"
            android:textColor="#757575"
            android:textSize="12sp"/>
        <CheckBox
            android:id="@+id/cb_group_item_flag"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="8dp"
            android:layout_marginLeft="12dp"
            android:background="@drawable/selector"
            android:button="@null"/>
        <TextView
            android:id="@+id/tv_group_item_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_marginRight="8dp"
            android:layout_marginTop="15dp"
            android:text="¥60元"
            android:textColor="#04a7dd"
            android:textSize="12dp"/>
        <TextView
            android:id="@+id/tv_group_item_state"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_below="@id/tv_group_item_price"
            android:layout_marginRight="8dp"
            android:layout_marginTop="12dp"
            android:text="进行中"
            android:textColor="#757575"
            android:textSize="12sp"/>
        <TextView
            android:id="@+id/tv_group_item_delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_alignParentRight="true"
            android:layout_marginBottom="8dp"
            android:layout_marginRight="8dp"
            android:text="退款"
            android:textColor="#fff"
            android:textSize="12sp"/>
        <TextView
            android:id="@+id/tv_group_item_judge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="8dp"
            android:layout_marginRight="8dp"
            android:layout_toLeftOf="@id/tv_group_item_delete"
            android:text="二维码"
            android:textColor="#04a7dd"
            android:textSize="12sp"/>
    </RelativeLayout>
</RelativeLayout>

如果仔细看上边的布局文件中的代码会发现布局文件中嵌套了两层RelativeLayout,这是写这个demo时踩到的一个大坑,如果不再添加一层RelativeLayout那么不管Item的高度设置为多少,item都只有固定的高度,如下图:
这里写图片描述
只有再添加一层RelativeLayout之后才能正常显示,如下:
这里写图片描述

ChildItem布局代码如下(ChildItem中同样需要多嵌套一层RelativeLayout,不然Item高度显示不正常):

<?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"
                android:background="@drawable/shape4">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="100dp">

    <TextView
        android:id="@+id/tv_child_item_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="15dp"
        android:layout_marginTop="12dp"
        android:text="姓名"
        android:textColor="#333"
        android:textSize="15sp"/>
    <TextView
        android:id="@+id/tv_child_item_person_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:layout_toRightOf="@id/tv_child_item_name"
        android:text="王先生"
        android:textColor="#757575"
        android:textSize="13sp"/>
    <TextView
        android:id="@+id/tv_child_item_touch_way"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="10dp"
        android:layout_marginTop="10dp"
        android:text="联系方式"
        android:textSize="15sp"/>
    <TextView
        android:id="@+id/tv_child_item_tel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:layout_toRightOf="@id/tv_child_item_touch_way"
        android:layout_alignParentBottom="true"
        android:text="16452135156"/>
    <TextView
        android:id="@+id/tv_child_item_state"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="8dp"
        android:layout_marginTop="12dp"
        android:text="未上车"
        android:textColor="#04a7dd"
        android:textSize="12sp"/>
    <TextView
        android:id="@+id/tv_child_item_drawback"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_marginBottom="10dp"
        android:text="退款"
        android:layout_marginRight="8dp"
        android:textColor="#fff"/>
    </RelativeLayout>
</RelativeLayout>

接下来最重要的一部分自定义ExpandableListView的适配器。

package com.example.edianzu.expandablelistviewdemo;

import android.content.Context;
import android.database.DataSetObserver;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ExpandableListAdapter;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by zhpan on 2016/9/24.
 */

public class MyAdapter implements ExpandableListAdapter,View.OnClickListener {
    private Context mContext;
    private List<GroupBean> mGroupBeanList;
    private List<List<ChildBean>> mLists;
    private Map<Integer,Boolean> map=new HashMap<>();//存储被选中的checkbox

    public MyAdapter(Context context, List<GroupBean> groupBeanList, List<List<ChildBean>> lists) {
        mContext = context;
        mGroupBeanList = groupBeanList;
        mLists = lists;
    }

    @Override
    public void registerDataSetObserver(DataSetObserver observer) {

    }

    @Override
    public void unregisterDataSetObserver(DataSetObserver observer) {

    }

    /**
     * 获取listview的组数
     * @return
     */
    @Override
    public int getGroupCount() {
        return 20;
    }

    /**
     * @param groupPosition 点击的条目位置
     * @return 子view条目总数数
     */
    @Override
    public int getChildrenCount(int groupPosition) {
        return 5;
    }

    /**
     *
     * @param groupPosition
     * @return  此处应该返回GroupBean对象
     */
    @Override
    public Object getGroup(int groupPosition) {
        return mGroupBeanList.get(groupPosition);
    }

    /**
     *
     * @param groupPosition
     * @param childPosition
     * @return  此处应该返回ChildBean对象
     */
    @Override
    public Object getChild(int groupPosition, int childPosition) {
        return mLists.get(childPosition);
    }

    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    /**
     * 返回的view作为组列表项
     * @param groupPosition
     * @param isExpanded
     * @param convertView
     * @param parent
     * @return
     */
    @Override
    public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        final GroupViewHolder holder;
        if(convertView==null){
            holder=new GroupViewHolder();

            convertView=View.inflate(mContext,R.layout.group_item,null);
            holder.mImageViewLogo= (ImageView) convertView.findViewById(R.id.iv_group_item);
            holder.mCheckBoxFlag= (CheckBox) convertView.findViewById(R.id.cb_group_item_flag);
            holder.mTextViewCity= (TextView) convertView.findViewById(R.id.tv_group_item_city);
            holder.mTextViewTime= (TextView) convertView.findViewById(R.id.tv_group_item_time);
            holder.mTextViewPrice= (TextView) convertView.findViewById(R.id.tv_group_item_price);
            holder.mTextViewState= (TextView) convertView.findViewById(R.id.tv_group_item_state);
            holder.mTextViewDrawback= (TextView) convertView.findViewById(R.id.tv_group_item_delete);
            holder.mTextViewJudge= (TextView) convertView.findViewById(R.id.tv_group_item_judge);
            holder.mRelativeLayout= (RelativeLayout) convertView.findViewById(R.id.rl_group_item);

            holder.mCheckBoxFlag.setBackgroundResource(R.drawable.selector);
            holder.mImageViewLogo.setImageResource(R.drawable.mine_card);
            holder.mTextViewCity.setText("郑州-上海");
            holder.mTextViewPrice.setText("¥60元");
            holder.mTextViewTime.setText("09-25 15:21 出发");
            holder.mTextViewState.setText("二维码");
            holder.mTextViewDrawback.setText("退款");
            holder.mTextViewJudge.setBackgroundResource(R.drawable.shape3);
            holder.mTextViewDrawback.setBackgroundResource(R.drawable.shape2);
            convertView.setTag(holder);
        }else {
            holder= (GroupViewHolder) convertView.getTag();
        }
        holder.mCheckBoxFlag.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if(holder.mCheckBoxFlag.isChecked()){
                    map.put(groupPosition,true);
                }else {
                    map.remove(groupPosition);
                }
            }
        });

        if(map!=null&&map.containsKey(groupPosition)){
            holder.mCheckBoxFlag.setChecked(true);
        }else {
            holder.mCheckBoxFlag.setChecked(false);
        }


        //  给mRelativeLayout设置点击事件拦截item点击展开子view的事件
        holder.mRelativeLayout.setOnClickListener(this);

        holder.mTextViewJudge.setOnClickListener(this);
        holder.mTextViewDrawback.setOnClickListener(this);
        /**
         * 三角的点击事件,点击三角时回调MainActivity中的OnFlagClickListener()回调方法
         */
        holder.mCheckBoxFlag.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity context = (MainActivity) mContext;
                context.OnFlagClickListener(groupPosition);
            }
        });


        return convertView;
    }

    /**
     * 返回的view对象作为子列表项
     * @param groupPosition
     * @param childPosition
     * @param isLastChild
     * @param convertView
     * @param parent
     * @return
     */
    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        ChildViewHolder holder;
        if(convertView==null){
            convertView=View.inflate(mContext,R.layout.child_item,null);
            holder=new ChildViewHolder();
            holder.mTextViewName= (TextView) convertView.findViewById(R.id.tv_child_item_person_name);
            holder.mTextViewTel= (TextView) convertView.findViewById(R.id.tv_child_item_tel);
            holder.mTextViewState= (TextView) convertView.findViewById(R.id.tv_child_item_state);
            holder.mTextViewDrawback= (TextView) convertView.findViewById(R.id.tv_child_item_drawback);

            holder.mTextViewName.setText("王先生");
            holder.mTextViewTel.setText("16245895873");
            holder.mTextViewState.setText("未上车");
            holder.mTextViewDrawback.setText("退款");
            convertView.setTag(holder);
        }else {
            holder= (ChildViewHolder) convertView.getTag();
        }

        // 此处可以设置子view中控件的点击事件



        return convertView;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

    @Override
    public boolean areAllItemsEnabled() {
        return false;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public void onGroupExpanded(int groupPosition) {

    }

    @Override
    public void onGroupCollapsed(int groupPosition) {

    }

    @Override
    public long getCombinedChildId(long groupId, long childId) {
        return 0;
    }

    @Override
    public long getCombinedGroupId(long groupId) {
        return 0;
    }

    /**
     * 控件的点击事件
     * @param v
     */
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.tv_group_item_judge:
                Toast.makeText(mContext, "点击了二维码", Toast.LENGTH_SHORT).show();
                break;
            case R.id.tv_group_item_delete:
                Toast.makeText(mContext, "点击了退款", Toast.LENGTH_SHORT).show();
                break;
        }
    }

    static class GroupViewHolder{
        private ImageView mImageViewLogo;
        private CheckBox mCheckBoxFlag;
        private TextView mTextViewCity;
        private TextView mTextViewTime;
        private TextView mTextViewPrice;
        private TextView mTextViewState;
        private TextView mTextViewJudge;
        private TextView mTextViewDrawback;
        private RelativeLayout mRelativeLayout;
    }

    static class ChildViewHolder{
        private TextView mTextViewName;
        private TextView mTextViewState;
        private TextView mTextViewTel;
        private TextView mTextViewDrawback;
    }
}

ExpandableListView的Adapter相比ListView的Adapter多了不少的方法,数了下大概是二十个!但是用得到的也就十个左右,用到的方法中都加了注释,这里不做过多解释。这里只说其中最重要的两个方法getGroupView()和getChildView()
这两个方法前者返回的view作为组列表项,后者返回的view对象作为子列表项。类比于ListView的Adapter中的getView()方法。

Adapter中还有一段代码需要作下解释:

 /**
         * 三角的点击事件,点击三角时回调MainActivity中的OnFlagClickListener()回调方法
         */
        holder.mCheckBoxFlag.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity context = (MainActivity) mContext;
                context.OnFlagClickListener(groupPosition);
            }
        });

这段代码是指示箭头的监听事件,指示箭头是自定义的一个CheckBox,然后用一个selector作为背景的。
当点击指示箭头的时候出发了回调方法,回调方法在MainActivity中完成展开和关闭子view的功能。
接口如下:

/**
 * Created by zhpan on 2016/9/24.
 * 三角的回调接口
 */

public interface SetOnFlagClickListener {
    /**
     * 三角点击事件的回调方法,点击三角时展开或关闭子ListView
     * @param position
     */
    void OnFlagClickListener(int position);
}

MainActity实现该接口并重写OnFlagClickListener方法如下:

  /**
     * 三角点击事件的回调方法,点击三角时展开或关闭子ListView
     *
     * @param position
     */
    @Override
    public void OnFlagClickListener(int position) {
        if (mExpandableListView.isGroupExpanded(position)) {  //如果是打开状态则关闭
            mExpandableListView.collapseGroup(position);

        } else { //如果是关闭状态则打开
            mExpandableListView.expandGroup(position);
        }
    }

可通过ExpandableListView的collapseGroup方法和expandGroup方法关闭和展开子view。
另外一点需要注意,默认的点击item就可以展开子View,但是这个例子中要屏蔽掉点击Item展开子View,想了很多办法都没实现,后来想既然给Item其他子控件设置了点击事件后子View可以获取相应点击事件而没有展开子View。于是就给Item的布局文件加了一个监听事件,然后在onClick()方法中什么都没有做,果真拦截了展开子View的时间。如下:

     //  给mRelativeLayout设置点击事件拦截item点击展开子view的事件
        holder.mRelativeLayout.setOnClickListener(this);

最后一点还要注意ExpandableListView中的指示箭头是一个CheckBox,当CheckBox状态改变的时候由于Item复用了convertView,会使CheckBox的选中状态错乱。因此Adapter中添加了存储CheckBox选中状态的代码。
关于CheckBox选中状态错乱的详细解释请参看上篇文章《ListView嵌套CheckBox滑动时CheckBox选中状态错乱》

源码下载

猜你喜欢

转载自blog.csdn.net/qq_20521573/article/details/52665196