您的位置:首页 > 移动开发 > Android开发

Android ExpandableListView 中完美嵌套 GridView

2016-09-15 12:52 519 查看
转载请注明出处: http://blog.csdn.net/like_program/article/details/52549435

在做项目的时候,想在 ExpandableListView 中嵌套一个 GridView,在实现的过程中,遇到了不少坑,所以写篇博客记录一下,也顺便帮助下和我一样的新手。

我一直觉得,再多的文字,再多的代码片段,都不如写个小 Demo 更直观,所以在以后的博客中,我都尽量用小 Demo 来讲解,也给出源码。

先上一张最终效果图:



打开 Android Studio,我们新建一个 ExpandableListViewTest 项目,在项目中新建两个包(package), 分别命名为 activity 和 adapter,然后将 Android Studio 默认创建的 MainActivity 拖入 activity 包中。

为了让项目运行起来看上去更美观,我们用 Theme 主题,修改 AndroidManifest.xml 中的 application 下的 android:theme 属性,修改为
@android:style/Theme


修改后的 AndroidManifest.xml 代码如下所示:

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme" >
<activity android:name=".activity.MainActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>


修改了主题后,我们再修改下 MainActivity.java 文件,MainActivity 默认继承自 AppCompatActivity ,我们把 AppCompatActivity 修改为 Activity,让 MainActivity 继承 Activity,这样等会项目才能正常运行。

我们先来写一下布局文件,既然我们想在 ExpandableListView 中嵌套一个 GridView,那么首先我们应该写一个 ExpandableListView

activity_main.xml 代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">

<ExpandableListView
android:id="@+id/expandableList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:childDivider="@drawable/separator"
android:divider="@drawable/separator"/>
</RelativeLayout>


布局文件非常简单,在布局中只放置了一个 ExpandableListView,并设置了各组之间的分割线和各组子项之间的分割线,分割线是一张图片。接下来我们就要在 MainActivity 中实例化 ExpandableListView 的布局,并准备好 ExpandableListView 要显示的数据,然后把数据传入 ExpandableListView 的适配器,这样才能把 ExpandableListView 显示出来。

MainActivity.java 代码如下所示:

package com.example.expandablelistviewtest.activity;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ExpandableListView;

import com.example.expandablelistviewtest.R;
import com.example.expandablelistviewtest.adapter.MyExpandableListViewAdapter;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends Activity {

private ExpandableListView expandableListView;

/**
* 每个分组的名字的集合
*/
private List<String> groupList;

/**
* 每个分组下的每个子项的 GridView 数据集合
*/
private List<String> itemGridList;

/**
* 所有分组的所有子项的 GridView 数据集合
*/
private List<List<String>> itemList;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

expandableListView = (ExpandableListView) findViewById(R.id.expandableList);

// 分组
groupList = new ArrayList<>();
groupList.add("分组1");
groupList.add("分组2");

// 每个分组下的每个子项的 GridView 数据集合
itemGridList = new ArrayList<>();
for (int i = 0; i < 4; i++) {
itemGridList.add("电脑" + (i + 1));
}

// 所有分组的所有子项的 GridView 数据集合
itemList = new ArrayList<>();
itemList.add(itemGridList);
itemList.add(itemGridList);
// 创建适配器
MyExpandableListViewAdapter adapter = new MyExpandableListViewAdapter(MainActivity.this,
groupList, itemList);
expandableListView.setAdapter(adapter);
// 隐藏分组指示器
expandableListView.setGroupIndicator(null);
// 默认展开第一组
expandableListView.expandGroup(0);
}
}


既然我们要设置 ExpandableListView 适配器,那首先我们要定义一个 ExpandableListView 适配器,在 adapter 包下新建一个 MyExpandableListViewAdapter.java 类,在 ExpandableListView 适配器中,我们将把在 MainActivity 中准备好的数据传入这个适配器,并定义这些数据该怎么显示,因此我们还要创建一个 ExpandableListView 的分组布局文件 和 子项布局文件。不过不要着急,我们先定义好适配器。

MyExpandableListViewAdapter.java 代码如下所示:

package com.example.expandablelistviewtest.adapter;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseExpandableListAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import com.example.expandablelistviewtest.R;

import java.util.List;

/**
* ExpandableListView 适配器
*/
public class MyExpandableListViewAdapter extends BaseExpandableListAdapter {

private Context mContext;

/**
* 每个分组的名字的集合
*/
private List<String> groupList;

/**
* 所有分组的所有子项的 GridView 数据集合
*/
private List<List<String>> itemList;

private GridView gridView;

public MyExpandableListViewAdapter(Context context, List<String> groupList,
List<List<String>> itemList) {
mContext = context;
this.groupList = groupList;
this.itemList = itemList;
}

@Override
public int getGroupCount() {
return groupList.size();
}

@Override
public int getChildrenCount(int groupPosition) {
return itemList.get(groupPosition).size();
}

@Override
public Object getGroup(int groupPosition) {
return groupList.get(groupPosition);
}

@Override
public Object getChild(int groupPosition, int childPosition) {
return itemList.get(groupPosition).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 false;
}

@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup
parent) {
if (null == convertView) {
convertView = View.inflate(mContext, R.layout.expandablelist_group, null);
}
ImageView ivGroup = (ImageView) convertView.findViewById(R.id.iv_group);
TextView tvGroup = (TextView) convertView.findViewById(R.id.tv_group);
// 如果是展开状态,就显示展开的箭头,否则,显示折叠的箭头
if (isExpanded) {
ivGroup.setImageResource(R.drawable.ic_open);
} else {
ivGroup.setImageResource(R.drawable.ic_close);
}
// 设置分组组名
tvGroup.setText(groupList.get(groupPosition));
return convertView;
}

@Override
public View getChildView(final int groupPosition, final int childPosition, boolean isLastChild, View
convertView, ViewGroup parent) {
if (null == convertView) {
convertView = View.inflate(mContext, R.layout.expandablelist_item, null);
}
// 因为 convertView 的布局就是一个 GridView,
// 所以可以向下转型为 GridView
gridView = (GridView) convertView;
// 创建 GridView 适配器
MyGridViewAdapter gridViewAdapter = new MyGridViewAdapter(mContext, itemList.get
(groupPosition));
gridView.setAdapter(gridViewAdapter);
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(mContext, "点击了第" + (groupPosition + 1) + "组,第" +
(position + 1) + "项", Toast.LENGTH_SHORT).show();
}
});
return convertView;
}

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


我们可以看到,ExpandableListView 的适配器代码看上去很多,但实际上并不复杂,因为大部分代码都是重写父类的方法,这里对 getChildView 的某些代码解释一下:convertView 的布局就是一个 GridView,但是实例化布局后,convertView 是 View 类型,所以 convertView 没有 setAdapter() 方法,需要向下转型为 GridView ,才能调用 setAdapter() 方法,设置适配器。

定义好了 ExpandableListView 的适配器,我们来写一下 ExpandableListView 的分组布局文件 和 子项布局文件。我们先写 ExpandableListView 的分组布局文件,在 layout 文件夹下,新建 expandablelist_group.xml ,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">

<ImageView
android:id="@+id/iv_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@drawable/ic_open"/>

<TextView
android:id="@+id/tv_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:gravity="center"
android:text="分组"
android:textSize="18sp"/>

</LinearLayout>


我们可以看到,分组布局非常简单,就是一个横向的线性布局,然后依次排列了一个 指示箭头 图片 和一个 分组 名字,我们继续写 ExpandableListView 的子项布局文件,在 layout 文件夹下,新建 expandablelist_item.xml ,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:numColumns="3">

</GridView>


我们可以看到,子项布局更简单,就是一个有 3 列数据的 GridView。

对了,刚才我们在 ExpandableListView 的适配器中创建了 GridView 的适配器,那我们赶紧去定义一下 GridView 的适配器吧。

在 adapter 包下,新建 MyGridViewAdapter.java ,代码如下所示:

package com.example.expandablelistviewtest.adapter;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.example.expandablelistviewtest.R;

import java.util.List;

/**
* GridView 适配器
*/
public class MyGridViewAdapter extends BaseAdapter {

private Context mContext;

/**
* 每个分组下的每个子项的 GridView 数据集合
*/
private List<String> itemGridList;

public MyGridViewAdapter(Context mContext, List<String> itemGridList) {
this.mContext = mContext;
this.itemGridList = itemGridList;
}

@Override
public int getCount() {
return itemGridList.size();
}

@Override
public Object getItem(int position) {
return itemGridList.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (null == convertView) {
convertView = View.inflate(mContext, R.layout.gridview_item, null);
}
TextView tvGridView = (TextView) convertView.findViewById(R.id.tv_gridview);
tvGridView.setText(itemGridList.get(position));
return convertView;
}
}


GridView 的适配器和 ExpandableListView 的适配器一样,逻辑并不复杂,主要就是重写父类的方法,在 getView() 方法中,我们实例化了 GridView 的子项布局,所以,我们也要写一下 GridView 的子项布局文件,在 layout 文件夹中,新建 gridview_item.xml ,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_gridview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTop="@drawable/icon_computer"
android:gravity="center"
android:text="电脑"/>

</LinearLayout>


GridView 子项布局只定义了一个带有图片的 TextView。

好了,我们来运行一下,看看效果。



好像已经成功了,不过貌似有些问题:

它的每个分组的每个子项里都有一个 GridView,但我们只想在每个分组里只显示一个 GridView 。

在点击 GridView 中的子项时,我们发现,在同一个分组里,点击不同子项里 GridView 的第一个子项时,都显示的第 N 组第 1 项。

我们定义的 GridView 有 4 个数据,分别是 电脑 1 ,电脑 2 ,电脑 3 ,电脑 4 。但是它只显示了 3 个。

我们逐一解决这些问题。

首先,我们可以发现,每组都有 4 行数据,也就是说,每组都有 4 个子项,那么如果我们想要在每个分组里只显示一个 GridView ,也就是一个子项,那么我们要先找到我们之前定义过的和 4 相关的数据,经过查找,我们发现,和 4 相关的数据只有一个,就是 itemGridList ,它里面有 4 个数据,找到了它之后,我们再找一下它在哪里被调用了,经过查找,我们发现,在 ExpandableListView 适配器中的 getChildrenCount() 方法中调用了它,有些同学了可能会疑惑,这里调用的是
itemList.get(groupPosition).size()
,关 itemGridList 什么事,其实我们之前在 MainActivity 中是这样创建 itemList 的:
itemList.add(itemGridList)
,在这行代码中,itemGridList 被添加到了 itemList 中,那么在 getChildrenCount() 方法中,调用
itemList.get(groupPosition)
,得到的就是 itemGridList ,再调用它的 size() 方法,得到的就是 itemGridList 的大小了,也就是 4,知道了这个,我们想在每个分组里只显示一个 GridView,也就好办了,在 getChildrenCount() 方法中,我们直接返回 1,即

@Override
public int getChildrenCount(int groupPosition) {
return 1;
}


再运行一下程序,我们发现,果然每个分组里只显示了一个 GridView 。



我们再来看第二个问题,在同一个分组里,点击不同子项里 GridView 的第一个子项时,都显示的第 N 组第 1 项。

我们查看下 ExpandableListView 适配器代码中的 getChildView() 代码中的设置子项监听器代码:

Toast.makeText(mContext, "点击了第" + (groupPosition + 1) + "组,第" +
(position + 1) + "项", Toast.LENGTH_SHORT).show();


这里使用了两个变量, groupPosition 很容易理解,就是 分组的 id ,position 可能有些同学会迷惑,position 并不是分组的子项 id ,而是 GridView 的子项 id ,弄清了这个,第二个问题也就不难懂了,我们不管点击的是分组下的哪一个子项中的 GridView 的第一项,都显示是 第一项,是因为 position 表示的 GridView 的子项 id ,和 分组下的子项 id 并没有关系。

我们再来看第三个问题,我们定义的 GridView 有 4 个数据,但是它只显示了 3 个。

首先,它只显示 3 个的部分原因是因为我们定义的 GridView 一行只能显示 3 个数据,我们可以查看 expandablelist_item.xml 的代码:
android:numColumns="3"


更重要的原因是因为 GridView 的父容器测量模式为 UNSPECIFIED 的时候,GridView 的高度默认为一个 item 的高度,GridView 中源码如下:

if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}


既然是这样的话,我们就重写 GridView 的 onMeasure 方法,自定义高度,在 activity 包中,新建 MyGridView.java ,代码如下:

package com.example.expandablelistviewtest.activity;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.GridView;

/**
* 自定义 GridView
*/
public class MyGridView extends GridView {

public MyGridView(Context context) {
super(context);
}

public MyGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public MyGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

/**
* 重写该方法,达到使 GridView 适应 ExpandableListView 的效果
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}


关于重写 GridView 的 onMeasure 方法的原理分析,可以查看这两篇文章。

android开发游记:ScrollView嵌套ListView,ListView完全展开及makeMeasureSpec测量机制原理分析

详解嵌套ListView、ScrollView布局显示不全的问题

写好了 MyGridView 后,在 MyGridView 上点击右键,选择
Copy Reference
,复制 MyGridView 的全路径名,替换 expandablelist_item.xml 中的 GridView 标签。



替换后的 expandablelist_item.xml 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<com.example.expandablelistviewtest.activity.MyGridView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:numColumns="3">

</com.example.expandablelistviewtest.activity.MyGridView>


重新运行程序,我们发现,GridView 已经完美嵌套在 ExpandableListView 中了。



源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: