最近朋友公司的一个项目中用到了一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选中状态错乱》。
好库推荐
给大家推荐一下BannerViewPager。这是一个基于ViewPager实现的具有强大功能的无限轮播库。通过BannerViewPager可以实现腾讯视频、QQ音乐、酷狗音乐、支付宝、天猫、淘宝、优酷视频、喜马拉雅、网易云音乐、哔哩哔哩等APP的Banner样式以及指示器样式。
欢迎大家到github关注BannerViewPager!