Android列表控件

8,133 阅读20分钟

在Android中可滑动的列表是常见的UI布局效果. 所以熟练掌握列表控件是肯定的; RecyclerView是列表控件中最重要最复杂的所以将在另一篇文章中详细讲解, 本文不涉及;

我平时项目开发必备框架

  1. Android上最强网络请求 Net
  2. Android上最强列表(包含StateLayout) BRV
  3. Android最强缺省页 StateLayout
  4. JSON和长文本日志打印工具 LogCat
  5. 支持异步和全局自定义的吐司工具 Tooltip
  6. 开发调试窗口工具 DebugKit
  7. 一行代码创建透明状态栏 StatusBar

主要控件

  • GridView (网格视图)
  • GridLayout (网格布局)
  • ListView (列表视图)
  • Gallery (被废弃, 由HorizontalScrollView以及ViewPager替代)
  • ExpandableListView (可扩展列表视图)
  • Spinner (下拉视图)

ListView

主要功能使列表复用. 可以处理大量的列表控件组合. 在android21以前属于最常用控件之一. 之后引入RecyclerView控件. 其作用比ListView更加强大可定制性更高.

但是某些时候用ListView比RecyclerView更加方便. 而且官方并没有说应该被废弃.

布局属性

在布局文件中可以使用的xml属性

属性 描述
android:entries 引用一个数组资源来构成列表
android:divider 分割线(android:divider="@null" 可以去除分割线)
android:dividerHeight 分割线高度(即使设为0也会有1dp高度)
android:footerDividersEnabled 是否开启脚部分割线
android:headerDividersEnabled 会否开启头部分割线

预览item

 tools:listitem="@layout/demo_item"

函数

头布局和脚布局

添加布局

void addHeaderView (View v, 
                Object data, 
                boolean isSelectable)

void addHeaderView (View v)

void addFooterView (View v, 
                Object data, 
                boolean isSelectable)

void addFooterView (View v)

删除布局

boolean removeHeaderView (View v)

boolean removeFooterView (View v)

分割线

// 分割线是否启用
boolean areHeaderDividersEnabled ()
boolean areFooterDividersEnabled () 

// 是否启用分割线
void setHeaderDividersEnabled (boolean headerDividersEnabled)
void setFooterDividersEnabled (boolean footerDividersEnabled)

// 头部和脚部的越界显示图片, 默认情况ListView是无法越界, 故默认没有效果
void setOverscrollFooter (Drawable footer)
void setOverscrollHeader (Drawable header)

Drawable getOverscrollHeader ()
Drawable getOverscrollFooter ()

得到布局数量

int getFooterViewsCount ()

int getHeaderViewsCount ()

条目选择

void setSelection (int position)

平滑滚动

smoothScroll代表平滑滚动, By代表相对距离移动, To只会滚动到指定位置后无变化

// 相对滚动位置
void smoothScrollToPosition (int position)

void smoothScrollByOffset (int offset)

除此之外还继承了AbsListView的滚动方法

// 取消快速滚动条(快速滚动时依旧有个无法拖动的小滚动条)
void setSmoothScrollbarEnabled (boolean enabled)
boolean isSmoothScrollbarEnabled ()

  
// 平滑滚动的同时限定了最大滚动范围
void smoothScrollToPosition (int position, 
                int boundPosition) // 范围单位px


// 滚动像素单位, 并且可以控制滚动持续时间
void smoothScrollBy (int distance, 
                int duration)

// 指定的滚动位置会向上偏移一段距离 
void smoothScrollToPositionFromTop (int position, 
                int offset) // 偏移距离
  
// 增加控制滚动持续时间
void smoothScrollToPositionFromTop (int position, 
                int offset, 
                int duration)

滚动

void scrollListBy (int y)

适配器

学习适配器就要区分方法的作用:

  • 用于重写的方法. 这类方法是给ListView来调用的(适配器通过setAdapter()传入ListView)

  • 用于调用的方法. 暴露给用户来控制Item的

ListView采用MVC的架构, View和Data由一个Adapter控制. ListView使用的Adapter是接口ListAdapter. 使用setAdapter()方法设置.

最基础的适配器ListAdapter属于接口. 需要实现的方法很多. 为了方便提供了继承ListAdapter的抽象适配器

适配器的继承关系

  • Adapter
    • ListAdapter
      • BaseAdapter
        • SimpleAdapter
        • ArrayAdapter
        • CursorAdapter
          • ResourceCursorADapter
          • SimpleCursorAdapter
      • WrapperListAdapter
        • HeaderViewListAdapter
    • SpinnerAdapter

以上讲的适配器适用于ListView和GridView以及Spinner.

ListAdapter

ListAdapter属于接口, 一般情况并不直接使用, 因为没必要重写全部方法. 一般使用其子类.

// 是否启用item. 如果fasle则不启用. item处于无法选择和点击的状态
boolean isEnabled(int position); 

// 可以看到没有position参数. 所以如果返回fasle则全部item都处于不启用状态
public boolean areAllItemsEnabled(); 

继承父类的方法

int getCount () // 决定ListView的Item数量

Object getItem (int position) // 得到item 数据. 这里返回的值会在ListView中使用到

long getItemId (int position)  // 得到item 的 id. 这里返回的值会在ListView中使用到

// 返回Item类型, 类型是否相同决定是否复用item
int getItemViewType (int position) 

// 返回Item视图内容
View getView (int position,   // 位置
                View convertView, // 复用视图 
                ViewGroup parent) // 父容器

// 返回Item类型数量
int getViewTypeCount () 
  
// id是否唯一
boolean hasStableIds () 

boolean isEmpty () // 是否为空

void registerDataSetObserver (DataSetObserver observer) // 注册数据观察者

void unregisterDataSetObserver (DataSetObserver observer) // 取消数据观察者

是否唯一

hasStableIds()这个方法是判断id是否是有效. 返回true有效false无效.

  • 有效的情况下会通过getItemId()的返回id值来判断item是否是相同
  • 无效的情况下会默认使用item的position来当作id

BaseAdapter

首先我讲讲最常用适配器 BaseAdapter.

特点:

  • ListView支持高度自定义的Item

  • 需要自己重写该适配器来使用

重写方法

        final String[] title = {"用户", "首页", "设置", "关于", "反馈"};

// 这是写了个匿名类
        mListView.setAdapter(new BaseAdapter() {
            /**
             * 控制ListView的Item的数量
             * @return
             */
            @Override
            public int getCount() {
                return title.length;
            }

            /**
             * 控制ListView的某些方法返回的Object数据.
             * 例如ListView的getItemAtPosition()方法. 通过位置索引得到数据对象, 即该方法返回的Object对象
             *
             * @param position
             * @return
             */
            @Override
            public Object getItem(int position) {
                return null;
            }

            /**
             * 每次点击item都会回调该方法. 同样是为了ListView的getItemIdAtPosition()方法能够得到item的id
             *
             * @param position
             * @return
             */
            @Override
            public long getItemId(int position) {
                return 0;
            }

            /**
             * 控制ListView的Item的视图显示
             *
             * @param position 当前显示的视图位置
             * @param convertView 缓存的视图. 用于复用item
             * @param parent 父容器布局
             * @return
             */
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
              
                View view = View.inflate(MainActivity.this, R.layout.item_list, null);
                
                // 根据传入的数据进行修改
                TextView text = ButterKnife.findById(view, R.id.text);
                text.setText(title[position]);
                
                return view;
            }
        });

在主布局中添加控件

<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="com.liangjingkanji.listview.MainActivity">

    <ListView
        android:id="@+id/list"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

注意inflate item 视图的时候是否启用parent.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  
  <TextView
    android:id="@+id/text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
  
</LinearLayout>

后面的布局文件我不会再写出来了. 多余代码影响阅读性.

介绍下BaseAdapter相对于父类ListAdapter增加的方法. 这些方法都不是必须的

// 判断适配器是否存在item
boolean isEmpty () 

// **DropDownView**等方法是重写的SpinnerAdapter的. 所以会在讲解Spinner的时候详细讲. ListView和GridView用不到
View getDropDownView (int position, 
                View convertView, 
                ViewGroup parent)
 
boolean hasStableIds ()

void notifyDataSetChanged () // 数据如果发生变化通知ListView局部更新

void notifyDataSetInvalidated () //数据如果发生变化通知ListView整个更新

ArrayAdapter

ArrayAdapter是BaseAdapter的子类, 进行了进一步的封装, 能够快速实现最简单的字符串列表(同时限制了数据只能是单一的字符串). 注意这不是抽象类. 可以直接创建对象.

特点:

  • 只需要构造方法就可以构造出一个ListView出来
  • 自定义很弱, ListView的数据只能是字符串.

创建ArrayAdapter的时候需要指定泛型ArrayAdapter<T>. 泛型决定了构造方法能接受的数据类型

构造方法

ArrayAdapter (Context context, // 上下文
                int resource) // 布局id. 只支持根布局是TextView的布局

ArrayAdapter (Context context, 
                int resource,  // 这个构造方法就支持任意布局了
                int textViewResourceId)   // 指定一个Textview的id来设置数据. 

ArrayAdapter (Context context, 
                int resource, 
                T[] objects) // 直接在构造方法添加数据, 必须是字符串的数组

ArrayAdapter (Context context, 
                int resource, 
                int textViewResourceId, 
                T[] objects) // 同上

ArrayAdapter (Context context, 
                int resource, 
                List<T> objects)// 添加数据集合, 同样必须是字符串

ArrayAdapter (Context context, 
                int resource, 
                int textViewResourceId, // 同上
                List<T> objects) // 添加数据集合

示例


String[] array = {"用户", "首页", "设置", "关于", "反馈"};

// 这里的android.R.layout.simple_list_item_1是系统提供的TextView布局文件推荐直接拿来用. 可以点开看下源码.
listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, array));

看上去是不是很方便就实现了ListView.

注意

  1. ArrayAdapter内部已经对ListView进行了Item复用

  2. 传入的数据必须是字符串. 源码中进行了判断

  3. ArrayAdapter并没有进行ViewHolder的复用.

方法

// 添加一个数据
void add (T object)

// 添加多个数据
void addAll (T... items)

// 添加集合
void addAll (Collection<? extends T> collection)

// 删除全部数据
void clear ()
  
// 删除指定数据
void remove (T object)

// 对应位置插入数据
void insert (T object, 
                int index)

// 默认为true. 所以每次你对数据修改的时候都会调用notifyOnChange方法. 多次调用影响效率(频繁刷新UI). 可以用该方法设置false.然后自己调用notifyChange等方法来更新Item的UI.
void setNotifyOnChange (boolean notifyOnChange)

// 这是一个静态方法直接创建ListView. 简单直接
ArrayAdapter<CharSequence> createFromResource (Context context, 
                int textArrayResId,  // 文本数据
                int textViewResId)  // 文本控件id

// 得到传入的上下文
Context getContext ()

// 通过数据得到索引
int getPosition (T item)

再介绍两个用于重写支持ListView的筛选Item功能的方法

// 过滤器. 例如联系人的联想筛选
Filter getFilter ()

// 对数据使用标准的比较器排序操作
void sort (Comparator<? super T> comparator)

SimpleAdapter

SimpleAdapter是这三种中最复杂的适配器, 但是数据填充ListView的item很方便. 同样非抽象类可以直接创建对象使用.

同样先介绍构造方法

SimpleAdapter (Context context,  
                List<? extends Map<String, ?>> data,  // 数据. 每个Item对应一个Map集合
                int resource,  // Item的布局
                String[] from, // Map集合是无序的. 所以需要一个key数组来控制顺序
                int[] to) // 数据填充到该数组对应的View id

示例

创建一个ListView作为数据

// List集合存储Map集合代表数据
List<Map<String, Object>> list = new ArrayList<>();

Map<String, Object> map = new HashMap<>(); 
map.put("icon", R.mipmap.ic_launcher);
map.put("name", "设置");

Map<String, Object> map2 = new HashMap<>();
map2.put("icon", R.mipmap.ic_launcher);
map2.put("name", "关于");

list.add(map);
list.add(map2);

// String数据的值是Map集合中的键, 对应int数组中的控件id. 将键对应的值填充到对应的id控件上
listView.setAdapter(new SimpleAdapter(this,list, R.layout.list_item, new String[]{"icon", "name"}, new int[]{R.id.icon, R.id.name}));

查看源码可以看出来其实SimpleAdapter只支持TextView和Checkable以及ImageView三种控件的属性值设定, 如果非该三种将抛出语法异常信息.

如果传入ViewBinder接口可以在回调方法内手动处理数据填充

SimpleAdapter.ViewBinder getViewBinder ()
void setViewBinder (SimpleAdapter.ViewBinder viewBinder)

WrapperListAdapter

之前就提过这个适配器和BaseAdapter一样继承自ListAdapter. 不过这是接口. 内部就一个方法:

// 返回适配器对象. 等同于getAdapter
ListAdapter getWrappedAdapter ()

HeaderViewListAdapter

支持头布局和脚布局的ListAdapter

构造方法

// 可以看出主要就是加入两个包含头布局和脚布局的集合外加一个普通ListAdapter即可
HeaderViewListAdapter (ArrayList<ListView.FixedViewInfo> headerViewInfos, 
                ArrayList<ListView.FixedViewInfo> footerViewInfos, 
                ListAdapter adapter)

其实ListView支持直接添加头布局和脚布局, 这个之前提过. addHeaderViewaddFootView内部实现就是将原有适配器包裹成HeaderViewListAdapter.

FixedViewInfo

public class FixedViewInfo {
  // 视图
  public View view;
  // 数据
  public Object data;
  // 是否可选择. Selectable状态
  public boolean isSelectable;
}

点击事件

// 普通点击事件
void setOnClickListener (View.OnClickListener l)

// item点击事件
void setOnItemClickListener (AdapterView.OnItemClickListener listener)

// item长按点击事件
void setOnItemLongClickListener (AdapterView.OnItemLongClickListener listener)

// item选择事件
void setOnItemSelectedListener (AdapterView.OnItemSelectedListener listener)

多条目

常常一个列表中不可能条目都是一模一样的, 需要掺杂一些不同类型的Item.

下面介绍两个方法getViewTypeCountgetItemViewType

默认情况下两个方法实现

我们通过重写BaseAdapter中的这两个方法实现多条目

Tip: 如果直接在getView方法中通过position判断的话会导致视图复用机制(convertView)无法正常运行. 会导致视图错乱.

内存优化

划动屏幕时ListView从屏幕消失的条目会被销毁, 而新出现在屏幕的条目会被创建. 如果在大量的且显示内容过多的条目快速划动会造成回收内存的速度赶不上创建条目对象的速度, 而造成oom内存溢出. 为了避免这种情况ListView需要进行内存复用优化.

复用条目

条目在每次被显示在屏幕上的时候都会调用getView()方法, 如果每次在该方法中都创建一个View对象的情况下有大量的条目会导致OOM内存溢出, 所以使用的时候需要复用这个View对象, 而getView方法中已经缓存了这个对象即convertView参数,只需要调用即可.

/**
* 返回条目显示内容, 就是一个View对象

* @param position 条目所在位置
* @param convertView 条目复用对象
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View view = null;
  
  // 如果convertView不为空则复用
    if (convertView == null) {
        view = View.inflate(MainActivity.this, R.layout.list_item, null);
    }else {
        view = convertView;
    }
  
    TextView text = (TextView) view.findViewById(R.id.text);
    text.setText("当前条目" + position);
    return view;
}

ViewHolder

每次findViewById都会进行整个布局的遍历, 容易影响程序的运行效率, 所以可以创建ViewHolder类进行控件引用的存储. 然后每个Item对该ViewHolder进行存取操作;

关键方法setTag()

//在外面先定义,ViewHolder静态类
static class ViewHolder
{
    public ImageView img;
    public TextView title;
    public TextView info;
}

//然后重写getView
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if(convertView == null)
    {
        holder = new ViewHolder();
      
        convertView = mInflater.inflate(R.layout.list_item, null);
      
        holder.img = (ImageView)item.findViewById(R.id.img)
        holder.title = (TextView)item.findViewById(R.id.title);
        holder.info = (TextView)item.findViewById(R.id.info);
      
        convertView.setTag(holder);
    }else
    {
        holder = (ViewHolder)convertView.getTag();
    }
        holder.img.setImageResource(R.drawable.ic_launcher);
        holder.title.setText("Hello");
        holder.info.setText("World");
    }
                                                                                                
    return convertView;
}

RecyclerView都是强制使用ViewHolder;

选择模式

ListView其实支持多种选择模式(ChoiceModel)的设置

/**
 * 没有选择模式
 */
public static final int CHOICE_MODE_NONE = 0;
/**
 * 单选模式
 */
public static final int CHOICE_MODE_SINGLE = 1;
/**
 * 多选模式
 */
public static final int CHOICE_MODE_MULTIPLE = 2;
/**
 * The list allows multiple choices in a modal selection mode
 */
public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;

通过布局属性设置选择模式

android:choiceMode
Constant Value Description
multipleChoice 2 多选模式
multipleChoiceModal 3 屏蔽点击事件的多选模式
none 0 非选模式
singleChoice 1 单选模式

关于选择模式的方法都在AbsListView抽象类中

boolean isItemChecked (int position)
// 判断指定位置的item是否被选中

void setItemChecked (int position, 
                boolean value)
// 设置item选中

int getCheckedItemCount ()
// 被选中的item数量

long[] getCheckedItemIds ()
// 如果非非选模式并且(hasStableIds() == true) 才有效

int getCheckedItemPosition ()
// 选择的位置索引(只在单选模式有效)

SparseBooleanArray getCheckedItemPositions ()
// 得到被选中的所有item的位置

void clearChoices ()
// 清理所有被选择

int getChoiceMode ()
// 选择模式

void setChoiceMode (int choiceMode)
// 设置选择模式
    
void setMultiChoiceModeListener (AbsListView.MultiChoiceModeListener listener)
// 多选监听器

选择模式生效必须满足以下两点:

  1. 设置选择模式(默认情况是单选模式)

  2. Item没有子控件拦截焦点

    android:focusable="false"
    

屏蔽点击事件的多选模式

该模式下要想进入多选模式有以下两种方式:

  1. 长按item
  2. 通过函数调用setItemChecked

GridView

翻译即"网格视图"的意思. 和ListView的区别就是网格列表.

但是注意GridView并不是水平滑动布局, 只不过是增加列数的ListView(即满足列数限制就换行, 默认一行即ListView). 需要水平滑动布局可以使用HorizontalScrollView以及RecyclerView的GridLayoutManager布局

GridView兼容ListView的所有适配器.

布局属性

每个属性都有对应的方法. 详细介绍看方法.

属性 描述
android:columnWidth 列宽(默认无效)需要配合拉伸模式使用
android:numColumns 列数(默认为1)
android:stretchMode 拉伸模式
android:horizontalSpacing 水平间距
android:verticalSpacing 垂直间距
android:gravity 对齐方式

示例

用法和ListView一样. 支持之前讲过的所有的ListAdapter适配器;

mList.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, title));

mList.setNumColumns(2); // 设置列数. 默认1. 即和ListView没区别

列宽和间距

GridView就像一个网格列表. 学会如何排列网格是非常重要的.

上图标注了GridView的列宽以及水平和垂直间距

网格宽度

// 格子宽度. 像素单位
int getColumnWidth ()
void setColumnWidth (int columnWidth)

Tip: 如果在item的布局中设置了固定大小, 会导致裁剪效果.

拉伸模式

拉伸模式我认为是GridView最重要也是最难理解的地方. 注意理解我的分析

// 设置拉伸模式
void setStretchMode (int stretchMode)

支持四种拉伸模式

  1. NO_STRETCH

    不拉伸, 尺寸自己控制. 该模式下必须设置网格宽度否则什么都不显示

  2. STRETCH_COLUMN_WIDTH (拉伸列宽)

    默认模式, 列宽由屏幕拉伸决定. 所以之前介绍的setColumnWidth无效. 网格内容会根据屏幕的大小来比例缩放控制

  3. STRETCH_SPACING (拉伸间距)

    该模式必须制定列宽否则不显示, 同时自己指定的间距(包括水平间距和垂直间距)无效

  4. STRETCH_SPACING_UNIFORM

    此模式下列宽和间距都是有效设置值, 并且水平方向最左边也会有间距.

    但是由于都是有效值所以无法做到屏幕均布的效果

除了STRETCH_COLUMN_WIDTH其他模式都需要指定网格宽度(setColumnWidth).

总结:

  1. 拉伸间距或者列宽就无法设置其值.
  2. 如果列宽和间距都非拉伸模式就无法均布
  3. 如果不是拉伸列宽的情况下就必须制定列宽值否则不显示内容.

间距

// 设置水平间隔
int getHorizontalSpacing ()
void setHorizontalSpacing (int horizontalSpacing)

// 设置垂直间隔
void setVerticalSpacing (int verticalSpacing)
int getVerticalSpacing ()

列数

void setNumColumns (int numColumns)
int getNumColumns ()

对齐方式

int getGravity ()
void setGravity (int gravity)

方法介绍

// 返回适配器
ListAdapter getAdapter ()

列表嵌套

这里介绍GridView和ListView之间或者两种相同列表的相互嵌套.

列表嵌套的问题分为两种情况:

  1. 显示不完整
  2. 被嵌套的列表无法滑动

第一种

如果存在列表嵌套了一个高度为wrap_content|match_parent的列表时会发现被嵌套的列表无法完全显示, 但是如果固定的高度就不会发生这种情况, 但是很多数据都并不是固定的而是通过数据的数量动态加载.

通过自定义onMeasure方法给被嵌套的ListView一个无限的高度最大值

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
				MeasureSpec.AT_MOST);
		super.onMeasure(widthMeasureSpec, expandSpec);
	}

integer的最大值是32位, 之所以右移两位是因为在MeasureSpec中前两位表示模式

// 移位位数 30 private static final int MODE_SHIFT = 30;

// int 型占 32 位,左移 30 位,该属性表示掩码值,用来与 size 和 mode 进行 "&" 运算,获取对应值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

// 左移 30 位,其值为 00...(此处省略 30 个0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

// 左移 30 位,其值为 01...(此处省略 30 个0)
public static final int EXACTLY = 1 << MODE_SHIFT;

// 左移 30 位,其值为 10...(此处省略 30 个0)
public static final int AT_MOST = 2 << MODE_SHIFT;

如果不想重写就通过ListView的Item数量来动态的设置ListView的固定高度

private void setListViewHeight(ListView listView) {

    ListAdapter listAdapter = listView.getAdapter();

    if (listAdapter == null) {    
            return;
    }

    int totalHeight = 0;

    for (int i = 0; i < listAdapter.getCount(); i++) {

            View listItem = listAdapter.getView(i, null, listView);
            listItem.measure(0, 0);
            totalHeight += listItem.getMeasuredHeight();
    }


    ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight
                    + (listView.getDividerHeight() * (listAdapter.getCount() - 1));

    listView.setLayoutParams(params);

}

或者你可以在getView方法中设置每个View对象的layoutParams

第二种

上面介绍的两种方法只是针对ListView被嵌套时不显示的问题. 但是如果ListView里面嵌套的要是一个可滑动的ListView就需要另外解决了.

其实Google已经考虑到这种问题了, 提供方法可以直接生效

ViewCompat.setNestedScrollingEnabled(mList,true); // api21以上可以直接使用View而不是ViewCompat

关于NestedScrollView以及ScrollView中嵌套ListView或GridView不会出现第二种情况, 但是也会出现显示不完全. 同样解决方法.

对于RecyclerView中嵌套GridView和ListView第二种方法的解决办法就失效了.

ExpandableListView

ExpandableListView 是支持分组展开的ListView.

主要分为两部分:组和子列表

Attributes

指示器图标

android:groupIndicator

android:childIndicator

指示器间隔

android:indicatorEnd	
android:indicatorLeft	
android:indicatorRight	
android:indicatorStart

android:childIndicatorEnd	
android:childIndicatorLeft	
android:childIndicatorRight	
android:childIndicatorStart

指示器图标会和你的getView()视图内容重叠. 建议给item设置一个padding

子列表分割线

android:childDivider

分割线可以是图片或者颜色. 但是无论如何都是一个高度为1dp的全屏宽度的分割线. 且必须适配器isChildSelectable()方法返回true才会显示.

去除默认的指示器和分割线

android:divider="@null"
android:groupIndicator="@null"

ExpandableListAdapter

ExpandaleListAdapter属于接口. 一般情况直接使用其子类.

abstract boolean	areAllItemsEnabled()
  
// 该方法用于适配器返回视图数据
abstract Object	getChild(int groupPosition, int childPosition)

  
// 子列表item id
abstract long	getChildId(int groupPosition, int childPosition)

// 子列表item 视图
abstract View	getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent)

// 子列表
abstract int	getChildrenCount(int groupPosition)


abstract long	getCombinedChildId(long groupId, long childId)


abstract long	getCombinedGroupId(long groupId)

// 组对象
abstract Object	getGroup(int groupPosition)

// 组数量
abstract int	getGroupCount()

// 组id
abstract long	getGroupId(int groupPosition)

// 返回组视图
abstract View	getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent)

// id是否稳定, 该方法等同于ListView
abstract boolean	hasStableIds()

// 子列表是否可选
abstract boolean	isChildSelectable(int groupPosition, int childPosition)

abstract boolean	isEmpty()

// 组折叠回调
abstract void	onGroupCollapsed(int groupPosition)

// 组展开回调
abstract void	onGroupExpanded(int groupPosition)

BaseExpandableListAdapter

类似BaseAdapter, 需要实现的方法上面已经介绍过了. 下面直接示例;

public class CustomExpandableListAdapter extends BaseExpandableListAdapter {

    private  Context mContext;
    private  List<String> mGroupData;
    private  List<ArrayList<String>> mChildData;

    public CustomExpandableListAdapter(Context context, List<String> groupData, List<ArrayList<String>> childData) {
        mContext = context;
        mGroupData = groupData;
        mChildData = childData;
    }

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

    @Override public int getChildrenCount(int i) {
        return mChildData.size();
    }

    @Override public Object getGroup(int i) {
        return mGroupData.get(i);
    }

    @Override public Object getChild(int i, int i1) {
        return mChildData.get(i).get(i1);
    }

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

    @Override public long getChildId(int i, int i1) {
        return i+i1;
    }

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

    @Override public View getGroupView(int i, boolean b, View view, ViewGroup viewGroup) {

        if(view == null) {
        }

        View groupView = LayoutInflater.from(mContext).inflate(R.layout.item_group_text, viewGroup, false);
        TextView tvTitle = ButterKnife.findById(groupView, R.id.tv_title);
        tvTitle.setText(mGroupData.get(i));
        return groupView;
    }

    @Override public View getChildView(int i, int i1, boolean b, View view, ViewGroup viewGroup) {
        View childView = LayoutInflater.from(mContext).inflate(R.layout.item_group_text, viewGroup, false);
        TextView tvTitle = ButterKnife.findById(childView, R.id.tv_title);
        tvTitle.setText(mChildData.get(i).get(i1));
        return childView;
    }

    @Override public boolean isChildSelectable(int i, int i1) {
        return true;
    }
}

SimpleExpandableListAdapter

SimpleExpandableListAdapter和SimpleAdapter差不多不属于抽象类, 只需要使用构造方法创建实例即可.

SimpleExpandableListAdapter (Context context, 
                List<? extends Map<String, ?>> groupData, // 数据 
                int groupLayout,  // 组视图布局
                String[] groupFrom, // 数据键
                int[] groupTo,   // 布局控件id
                List<? extends List<? extends Map<String, ?>>> childData, 
                int childLayout,  // 子列表视图
                String[] childFrom, 
                int[] childTo)

SimpleExpandableListAdapter (Context context, 
                List<? extends Map<String, ?>> groupData, 
                int expandedGroupLayout, // 租展开布局
                int collapsedGroupLayout, // 组折叠布局
                String[] groupFrom, 
                int[] groupTo, 
                List<? extends List<? extends Map<String, ?>>> childData, 
                int childLayout, 
                String[] childFrom, 
                int[] childTo)

SimpleExpandableListAdapter (Context context, 
                List<? extends Map<String, ?>> groupData, 
                int expandedGroupLayout, 
                int collapsedGroupLayout, 
                String[] groupFrom, 
                int[] groupTo, 
                List<? extends List<? extends Map<String, ?>>> childData, 
                int childLayout, 
                int lastChildLayout, // 租最后一个子列表的视图
                String[] childFrom, 
                int[] childTo)

从构造方法可以看到SimpleExpandableListAdapter还支持两种固定的多类型布局. 不过需要注意的是多类型布局的groupTo/childTo控件id还是必须包括在内的.

监听器

子列表点击事件

void setOnChildClickListener (ExpandableListView.OnChildClickListener onChildClickListener)

组点击事件

void setOnGroupClickListener (ExpandableListView.OnGroupClickListener onGroupClickListener)

组收缩和展开事件

void setOnGroupCollapseListener (ExpandableListView.OnGroupCollapseListener onGroupCollapseListener)

void setOnGroupExpandListener (ExpandableListView.OnGroupExpandListener onGroupExpandListener)

取消组的折叠和收缩只需要在组点击事件的回调中返回true即可

        mExpand.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
            @Override
            public boolean onGroupClick(ExpandableListView expandableListView, View view, int i,
                    long l) {
                return true;
            }
        });

Spinner

虽然Spinner是容器布局不过并不支持子控件.因为其继承了AdapterView;

简单实现

布局中创建控件

<Spinner
         android:id="@+id/spinner"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:entries="@array/entries"
         />

values/strings 创建数组实体

<resources>
    <string-array name="entries">
        <item>Mercury</item>
        <item>Venus</item>
        <item>Earth</item>
        <item>Mars</item>
        <item>Jupiter</item>
        <item>Saturn</item>
        <item>Uranus</item>
        <item>Neptune</item>
    </string-array>
</resources>

Attributes

// 水平和垂直偏移. 只有垂直是有效的
android:dropDownHorizontalOffset

android:dropDownVerticalOffset

android:dropDownWidth // 下拉弹窗宽度

android:dropDownSelector // 下拉颜色选择器

android:gravity 

android:popupBackground // 下拉弹窗背景颜色

android:spinnerMode // 对话框/下拉弹窗

android:prompt // 对话框模式的标题, 注意需要引用string

选择监听

mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
  @Override
  public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {

  }

  @Override public void onNothingSelected(AdapterView<?> parent) {

  }
});

如果只是想使用下拉列表可以看看ListPopupWindow; 单纯的下拉列表, 提供依附功能和自定义宽高;