07.16 控件-ListView

2023/6/4 Android 开发基础

ListView 列表视图控件,显示一个可垂直滚动的视图集合,其中每个视图都位于列表中前一个视图的正下方。

目前官方已经推荐使用兼容拓展库提供的RecyclerView (opens new window)来显示列表,这是一个更现代、更灵活、更有性能的新控件。

可滚动的列表,这个在日常的软件中已经是很常见的一种控件,在大量数据需要浏览查看时,滚动列表是一个很好的交互方式。如购物网站滑动列表刷宝贝,外卖软件滑动列表选择美食,服务软件滑动列表选择服务等等...

列表视图是一个适配器视图,它不知道它包含的视图的详细信息,比如类型和内容。相反,列表视图会根据需要从 ListAdapter 中请求视图,比如在用户向上或向下滚动时显示对应的新视图。

要为数据集中的每个项显示更自定义的视图,需要实现一个 ListAdapter。例如,扩展 BaseAdapter 并为getView(…) 中的每个数据项创建和配置对应的视图。

# 基本属性和方法

常用属性:

  • android:divider:设置列表的分隔条,可以用颜色分割,也可以用drawable资源分割
  • android:dividerHeight:设置列表分隔条的高度
  • android:footerDividersEnabled:当设置为false时,ListView将不会在每个页脚视图之前绘制分割线,默认值为true。
  • android:headerDividersEnabled:当设置为false时,ListView将不会在每个标题视图后绘制分割线,默认值为true。

常用方法:

  • addFooterView(View v, Object data, boolean isSelectable):添加一个固定视图以显示在列表的底部,并指定这个视图的数据以及视图是否可以被选中
  • addFooterView(View v):添加一个固定视图以显示在列表的底部
  • addHeaderView(View v, Object data, boolean isSelectable):添加一个固定视图以显示在列表的顶部,并指定这个视图的数据以及视图是否可以被选中
  • addHeaderView(View v):添加一个固定视图以显示在列表的顶部
  • removeFooterView(View v):移除先前添加的页脚视图
  • removeHeaderView(View v):删除先前添加的标题视图
  • setAdapter(ListAdapter adapter):设置列表适配器
  • setSelection(int position):设置当前选择的项
  • smoothScrollByOffset(int offset):平滑地滚动到指定的适配器偏移位置
  • smoothScrollToPosition(int position):平滑地滚动到指定的适配器位置

# 基本使用示例

ListView 最常见的使用方法就是使用自定义 BaseAdapter,绑定自定义列表布局的方式。下面就来看看这种常见的实现方式。

  1. 定义列表的数据项对象:DataBean
/**
* 定义列表数据对象.
*/
public class DataBean {
    // 图标
    private int icon;
    // 标题
    private String title;
    // 内容
    private String tips;

    public DataBean(int icon, String title, String tips) {
        this.icon = icon;
        this.title = title;
        this.tips = tips;
    }

    public int getIcon() {
        return icon;
    }

    public void setIcon(int icon) {
        this.icon = icon;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getTips() {
        return tips;
    }

    public void setTips(String tips) {
        this.tips = tips;
    }
}
  1. 实现自定义实现 BaseAdapter 列表适配器:ListViewAdapter
/**
* ListView 列表适配器.
*/
public class ListViewAdapter extends BaseAdapter {
    private Context mContext;
    private List<DataBean> mDataBeans;

    public ListViewAdapter(Context context, List<DataBean> dataBeans) {
        mContext = context;
        mDataBeans = dataBeans;
    }

    @Override
    public int getCount() {
        return mDataBeans == null ? 0 : mDataBeans.size();
    }

    @Override
    public Object getItem(int position) {
        return mDataBeans == null ? null : mDataBeans.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext)
                    .inflate(R.layout.layout_listview_item, parent, false);
        }
        ImageView ivIcon = convertView.findViewById(R.id.iv_icon);
        TextView tvTitle = convertView.findViewById(R.id.tv_title);
        TextView tvTips = convertView.findViewById(R.id.tv_tips);
        DataBean dataBean = mDataBeans.get(position);
        ivIcon.setBackgroundResource(dataBean.getIcon());
        tvTitle.setText(dataBean.getTitle());
        tvTips.setText(dataBean.getTips());
        return convertView;
    }
}
  1. 自定义列表布局:layout_listview_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16dp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/iv_icon"
        app:layout_constraintTop_toTopOf="@id/iv_icon" />

    <TextView
        android:id="@+id/tv_tips"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@id/iv_icon"
        app:layout_constraintTop_toBottomOf="@id/tv_title" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 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="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

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

</LinearLayout>
  1. 示例代码
// 定义图标类型
private static final int ICON_SPRING = 0;
private static final int ICON_SUMMER = 1;
private static final int ICON_AUTUMN = 2;
private static final int ICON_WINTER = 3;

// 初始化列表模拟数据
ArrayList<DataBean> list = new ArrayList<>();
for (int i = 0; i < 36; i++) {
    int iconType = i % 4;
    int iconRes;
    String title;
    String tips;
    switch (iconType) {
        case ICON_SPRING:
            iconRes = R.drawable.icon_1;
            title = "春天";
            tips = "细听来,句句是乡音。";
            break;
        case ICON_SUMMER:
            iconRes = R.drawable.icon_2;
            title = "夏天";
            tips = "午饭后,纳凉大树下。";
            break;
        case ICON_AUTUMN:
            iconRes = R.drawable.icon_3;
            title = "秋天";
            tips = "东海岸,相约看海鸥。";
            break;
        case ICON_WINTER:
            iconRes = R.drawable.icon_4;
            title = "冬天";
            tips = "大街上,传来爆竹声。";
            break;
        default:
            iconRes = 0;
            title = "";
            tips = "";
    }
    DataBean dataBean = new DataBean(iconRes, title, tips);
    list.add(dataBean);
}

ListView listView = findViewById(R.id.lv);
// 设置列表适配器
ListViewAdapter adapter = new ListViewAdapter(this, list);
listView.setAdapter(adapter);
// 设置item点击监听
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Toast.makeText(getApplicationContext(),
                String.format("点击了第%d项:%s", position, list.get(position).getTitle()),
                Toast.LENGTH_SHORT).show();
    }
});
// 设置item长按监听
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        Toast.makeText(getApplicationContext(),
                String.format("长按了第%d项:%s", position, list.get(position).getTitle()),
                Toast.LENGTH_SHORT).show();
        return true;
    }
});
  1. 效果图

ListView 基本使用示例效果图

# 添加表头表尾

Listview 作为一个列表控件,可以自己设置表头与表尾。通过 addHeaderViewaddFooterView方法即可实现。

  1. 定义一个表头和表尾的自定义布局文件(这里表头和表尾使用同一个布局):layout_listview_header.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:background="#4CAF50">

    <TextView
        android:id="@+id/tv_header"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textStyle="bold"
        android:textColor="#fff"
        android:textSize="25dp" />

</LinearLayout>
  1. 代码设置表头和表尾(在设置adapter前设置))
ListView listView = findViewById(R.id.lv);
ListViewAdapter adapter = new ListViewAdapter(this, list);

// 加载表头布局
View headerView = LayoutInflater.from(this).inflate(R.layout.layout_listview_header, null);
TextView tvHeader = headerView.findViewById(R.id.tv_header);
tvHeader.setText("表头");
// 设置列表的表头(设置adapter前设置)
listView.addHeaderView(headerView);

// 加载表尾布局
View footView = LayoutInflater.from(this).inflate(R.layout.layout_listview_header, null);
TextView tvFoot = footView.findViewById(R.id.tv_header);
tvFoot.setText("表尾");
// 设置列表的表尾(设置adapter前设置)
listView.addFooterView(footView);

// 设置列表适配器
listView.setAdapter(adapter);
  1. 效果图

ListView 添加表头表尾效果图

# 列表显示方向

默认情况下,列表是从上到下方向显示的,如需要列表从下到上方向显示,可以设置android:stackFromBottom="true"属性或者调用setStackFromBottom(true)方法即可实现底部显示方向效果。

效果如下:

ListView 底部方向显示效果图

还有一种效果,底部显示方向效果在动态添加数据时,具有如下效果:

ListView 底部方向动态添加数据显示效果图

# 滚动条配置

当列表数据显示数量超过 ListView 本身的大小时,默认会显示一个滚动条(scrollbars),指示当前显示的数据在整个列表中的相对位置。

常用的滚动条配置属性:

  • android:scrollbars:它的取值可以是vertical,horizontal或none。 对ListView来说,它只能垂直滚动,将scrollbars设置成horizontal或者none效果都是一样的,也就是不会出现滚动条。所以如果不希望ListView显示滚动条,就将scrollbars设置成none。此外,如果scrollbars设置成none,那么其他的滚动条相关的配置都不会有效果。
  • android:scrollbarThumbVertical:滚动条滑块资源设置,这也是美化滚动条时最重要的一项配置。它可以设置为一个颜色值,或者是一个Drawable资源。对Drawable资源可以使用.9的png图片,也可以使用XML来配置。
  • android:scrollbarTrackVertical:滚动条的滑动轨道资源设置,可以设置为一个颜色值,或者是一个Drawable资源。
  • android:scrollbarSize:滚动条的大小,对ListView来说就是滚动条的宽度大小。如果scrollbarThumbVertical配置的是一个具有宽度的资源,此配置会被忽略。只有scrollbarThumbVertica配置的是颜色值或者xml时(自身宽度为0的Drawable),此项配置才会有效。
  • android:verticalScrollbarPosition:设置滚动条的位置,它可以是right,left或者是defaultPosition。如果不设置,默认是defaultPosition。如果是defaultPosition,则滚动条的位置受到布局RTL配置的影响,如果布局是从右往左,则滚动条显示在left侧,如果布局是从左往右,则滚动条显示在right侧。 滚动条没有上下位置的设置。对于可水平滚动的View(如HorizontalScrollView),滚动条始终在下方。不能设置到上方。
  • android:scrollbarStyle:此项配置也是用来设置滚动条的位置,不过不是左右位置,而是滚动条和ListView内容之间的相对位置,它的取值范围是insideoverlay(默认),insideInset,outsideOverlay,outsideInset。
  • android:fadeScrollbars:是否在ListView不滚动时隐藏滚动条,可以选择true或false,默认为true,也就是不滚动时隐藏。如果将其设置为false,那么只要ListView能够滚动,滚动条就会一直显示,不会隐藏。但如果ListView不足一页,不能滚动,则不会显示。
  • android:scrollbarDefaultDelayBeforeFade:设置滚动条从显示到隐藏的间隔时间,单位为毫秒,如果不设置,默认为300毫秒。需要注意的是,ListView初次加载完成时,会自动显示出滚动条。这时如果没有去做滑动操作,滚动条也会自动隐藏,不过和滑动后隐藏不同的是,这里需要经过4倍间隔时间后才会开始隐藏。
  • android:scrollbarFadeDuration:设置滚动条开始隐藏到完全隐藏的间隔时间,单位为毫秒,如果不设置,默认为250毫秒。
  • android:scrollbarAlwaysDrawVerticalTrack:设置是否总是显示垂直滚动条的Track,可以选择true或false,默认为false。通常,如果设置了滚动条的Track,那么Track会随着滚动条一起显示和隐藏。但如果设置了scrollbarAlwaysDrawVerticalTrack为true,则滚动条的Track将一直显示,不会隐藏。当然,如果没有配置scrollbarTrackVertical,即使设置了scrollbarAlwaysDrawVerticalTrack为true,也不会有Track显示。此外,fadeScrollbars配置为false,则无论scrollbarAlwaysDrawVerticalTrack配置为true还是false,Track都不会隐藏。
  • android:fastScrollEnabled:是否启用快速滚动条,可以选择true或false,默认为false,也就是不使用快速滚动条。如果启用了快速滚动条,当ListView页数小于4页时,仍然会显示普通滚动条,当ListView页数超过4页时,才会显示快速滚动条。
  • android:fastScrollStyle:设置快速滚动条的样式,其可配置的值,及含义和android:scrollbarStyle一样。不过此项配置是从Android5.0才开始有的,所以只有Android5 .0以上的系统中,此项配置才会有效。
  • android:fastScrollAlwaysVisible: 设置是否始终显示快速滚动条,可以选择true或false,默认为false,如果将其设置为true,则快速滚动条将始终显示,不会隐藏,且不受4页的约束,也就是说ListView即使不足4页,快速滚动条也会显示,甚至即使ListView不足一页,快速滚动条都会显示。此外,android:fadeScrollbars设置将不会生效,也就是即使android:fadeScrollbars设置为false,也不会显示普通滚动条。

滚动条的效果相关配置还是挺多的,这里列出的都是一般用的比较多的,这里也不再对这些属性进行逐个进行详细展开了。

设置滚动条位置android:scrollbarStyle

android:scrollbarStyle用于控制滚动条的与列表内容之间的相对位置,主要有如下设置:

  1. insideoverlay:默认属性,表示滚动条右侧和ListView可用区域右侧对其,且覆盖在列表的Item之上。

ListView insideoverlay滚动条样式效果图

  1. insideInset:表示滚动条右侧和ListView可用区域右侧对其,但不覆盖在Item之上,而是将Item挤到滚动条的左边。

ListView insideInset滚动条样式效果图

  1. outsideOverlay:表示滚动条右侧和ListView右侧对其,且覆盖在右侧padding之上。

ListView outsideOverlay滚动条样式效果图

  1. outsideInset:表示滚动条右侧和ListView可用区域右侧对其,但不覆盖在padding之上,而是将padding挤到滚动条的左边。

ListView outsideInset滚动条样式效果图

滚动条的滑块和滑轨自定义实现

  1. 定义滚动条滑块xml样式:listview_scrollbar_thumb.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 红蓝色上下方向渐变 -->
    <gradient
        android:angle="90"
        android:endColor="#FF0000"
        android:startColor="#03A9F4" />
    <!-- 圆角 -->
    <corners android:radius="6dp" />
    <!-- 描边 -->
    <stroke
        android:width="1dp"
        android:color="#A4000000" />
</shape>
  1. 定义滚动条滑动轨道xml样式:listview_scrollbar_track.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <!-- 颜色 -->
    <solid
        android:color="#DFF1C1" />
    <!-- 圆角 -->
    <corners android:radius="6dp" />
    <!-- 描边 -->
    <stroke
        android:width="1dp"
        android:color="#A2000000" />
</shape>
  1. 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="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <ListView
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:fadeScrollbars="false"
        android:paddingEnd="5dp"
        android:scrollbarSize="10dp"
        android:scrollbarStyle="outsideInset"
        android:scrollbarThumbVertical="@drawable/listview_scrollbar_thumb"
        android:scrollbarTrackVertical="@drawable/listview_scrollbar_track"
        android:stackFromBottom="true" />

</LinearLayout>
  1. 效果图

ListView 自定义滚动条样式效果图

# ConvertView复用

ListView 中有 ConvertView 复用机制,试想一下,列表控件主要用于大数据展示的场景,如果数据很庞大,成千万条的时候,会创建相应数量的View项么?显然不是,在 AdaptergetView 方法中,有一个ConvertView 参数,这个参数不为 null 的时候,就是 ListView 已经触发了 View 的复用机制了。

ConvertView的复用机制,简单电概括就是:ListView会创建列表显示区域大小所需数量的View,后面滑动时会复用不可见的View来代替新可见的显示项,然后复用这个View将界面元素进行重新刷新即可。当然是否复用这个View,取决与Adapter中getView返回的view,如果不希望复用,那么在getView中总是返回一个新创建的View即可(有内存和性能消耗风险)。

理解示意图:

ListView 列表项View复用机制示意图

  1. 正常ConvertView复用使用方法。
public View getView(int position, View convertView, ViewGroup parent) {
    Log.e(TAG, "getView: position = " + position + "convertView = " + convertView);
    // 当convertView为null时,需要创建一个新的列表项的View
    if (convertView == null) {
        convertView = LayoutInflater.from(mContext)
            .inflate(R.layout.layout_listview_item, parent, false);
    }
    // 当convertView不为null时,直接使用这个复用的View,重新刷新数据即可
    ImageView ivIcon = convertView.findViewById(R.id.iv_icon);
    TextView tvTitle = convertView.findViewById(R.id.tv_title);
    TextView tvTips = convertView.findViewById(R.id.tv_tips);
    DataBean dataBean = mDataBeans.get(position);
    ivIcon.setBackgroundResource(dataBean.getIcon());
    tvTitle.setText(dataBean.getTitle());
    tvTips.setText(dataBean.getTips());
    return convertView;
}
  1. 使用ViewHolder组件,优化减少findViewById()的调用。
/**
* ListView ViewHolder优化列表适配器.
*/
public class ListViewAdapter extends BaseAdapter {
    private Context mContext;
    private List<DataBean> mDataBeans;

    public ListViewAdapter(Context context, List<DataBean> dataBeans) {
        mContext = context;
        mDataBeans = dataBeans;
    }

    @Override
    public int getCount() {
        return mDataBeans == null ? 0 : mDataBeans.size();
    }

    @Override
    public Object getItem(int position) {
        return mDataBeans == null ? null : mDataBeans.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Log.e(TAG, "getView: position = " + position + "convertView = " + convertView);
        ViewHolder viewHolder = null;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext)
                    .inflate(R.layout.layout_listview_item, parent, false);
            viewHolder = new ViewHolder(convertView);
            // 将ViewHolder保存到当前的View中
            convertView.setTag(viewHolder);
        } else {
            // 再当前的View中取出ViewHolder
            viewHolder = (ViewHolder) convertView.getTag();
        }
        DataBean dataBean = mDataBeans.get(position);
        // 直接使用处理好的ViewHolder中的控件
        viewHolder.getIvIcon().setBackgroundResource(dataBean.getIcon());
        viewHolder.getTvTitle().setText(dataBean.getTitle());
        viewHolder.getTvTips().setText(dataBean.getTips());
        return convertView;
    }

    // 定义一个ViewHolder,保存View中的需要处理的控件和相关数据
    private class ViewHolder {
        private ImageView ivIcon;
        private TextView tvTitle;
        private TextView tvTips;

        public ViewHolder(View root) {
            ivIcon = root.findViewById(R.id.iv_icon);
            tvTitle = root.findViewById(R.id.tv_title);
            tvTips = root.findViewById(R.id.tv_tips);
        }

        public ImageView getIvIcon() {
            return ivIcon;
        }

        public TextView getTvTitle() {
            return tvTitle;
        }

        public TextView getTvTips() {
            return tvTips;
        }
    }
}

# 列表数据更新

列表的数据更新包括新增删除修改等操作,在进行数据更新时,可以通过Adapter的notifyDataSetChanged()来通知列表数据有更新,列表会重新刷新数据,刷新数据过程就是将当前所显示的View重新刷新一次(通过调用Adapter中getView方法来刷新指定的View)。

示例:

  1. 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="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:gravity="center_horizontal"
    tools:context=".activity.DemoActivity"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="addData"
        android:text="增加数据" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="deleteData"
        android:text="删除数据" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="updateData"
        android:text="修改数据" />

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fadeScrollbars="false"
        android:paddingEnd="5dp"
        android:scrollbarSize="10dp"
        android:scrollbarStyle="outsideInset" />

</LinearLayout>
  1. 示例代码
public class DemoActivity extends AppCompatActivity {
    private static final String TAG = "DemoActivity";
    private static final int ICON_SPRING = 0;
    private static final int ICON_SUMMER = 1;
    private static final int ICON_AUTUMN = 2;
    private static final int ICON_WINTER = 3;
    private List<DataBean> mData = new ArrayList<>();
    private ListViewAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        ListView listView = findViewById(R.id.lv);
        mAdapter = new ListViewAdapter(this, mData);
        // 设置列表适配器
        listView.setAdapter(mAdapter);
    }

    // 列表中增加一条数据
    public void addData(View view) {
        int count = mData.size();
        int iconType = count % 4;
        int iconRes;
        String title;
        String tips;
        switch (iconType) {
            case ICON_SPRING:
                iconRes = R.drawable.icon_1;
                title = "春天";
                tips = "细听来,句句是乡音。";
                break;
            case ICON_SUMMER:
                iconRes = R.drawable.icon_2;
                title = "夏天";
                tips = "午饭后,纳凉大树下。";
                break;
            case ICON_AUTUMN:
                iconRes = R.drawable.icon_3;
                title = "秋天";
                tips = "东海岸,相约看海鸥。";
                break;
            case ICON_WINTER:
                iconRes = R.drawable.icon_4;
                title = "冬天";
                tips = "大街上,传来爆竹声。";
                break;
            default:
                iconRes = 0;
                title = "";
                tips = "";
        }
        DataBean dataBean = new DataBean(iconRes, title, tips);
        mData.add(dataBean);
        mAdapter.notifyDataSetChanged();
    }

    // 列表中移除数据
    public void deleteData(View view) {
        if (mData.size() > 0) {
            // 移除第一条数据
            mData.remove(mData.size() - 1);
            mAdapter.notifyDataSetChanged();
        }
    }

    // 列表中更新数据
    public void updateData(View view) {
        if (mData.size() > 0) {
            // 更新最后一条数据
            DataBean dataBean = mData.get(mData.size() - 1);
            dataBean.setTitle(dataBean.getTitle() + " 【数据已经修改】");
            mAdapter.notifyDataSetChanged();
        }
    }
}
  1. 效果图

ListView 数据更新效果图

# 多种列表布局

ListView中可以根据实际情况加载不同布局显示。实现方式也很简单:定义不同布局的类型,实现Adapter中的getItemViewType(int position),然后返回特定位置的类型,在Adapter的getView中调用这个方法获取当前Item的类型,加载和处理对应的类型的View。

这里实现一个简单的不同类型布局法人示例:奇数行显示一行文字的布局,偶数行显示图片+文字的布局。

  1. 创建一个文字的布局:layout_listview_item_type1.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="match_parent">

    <TextView
        android:id="@+id/tv_item1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold" />

</LinearLayout>
  1. 创建图片和文字的布局:layout_listview_item_type2.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:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_item2"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:padding="5dp" />

    <TextView
        android:id="@+id/tv_item2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical" />
</LinearLayout>
  1. 示例代码
public class DemoActivity extends AppCompatActivity {
    private static final String TAG = "DemoActivity";
    // 图标类型定义
    private static final int ICON_SPRING = 0;
    private static final int ICON_SUMMER = 1;
    private static final int ICON_AUTUMN = 2;
    private static final int ICON_WINTER = 3;
    // 列表类型定义
    private static final int TYPE_ODD = 0;
    private static final int TYPE_EVEN = 1;
    private List<DataBean> mData = new ArrayList<>();
    private ListViewAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        ListView listView = findViewById(R.id.lv);
        mAdapter = new ListViewAdapter(this, mData);
        // 设置列表适配器
        listView.setAdapter(mAdapter);

        // 初始化数据
        for (int i = 0; i < 16; i++) {
            int count = mData.size();
            int iconType = count % 4;
            int iconRes;
            String title;
            String tips;
            switch (iconType) {
                case ICON_SPRING:
                    iconRes = R.drawable.icon_1;
                    title = "春天";
                    tips = "细听来,句句是乡音。";
                    break;
                case ICON_SUMMER:
                    iconRes = R.drawable.icon_2;
                    title = "夏天";
                    tips = "午饭后,纳凉大树下。";
                    break;
                case ICON_AUTUMN:
                    iconRes = R.drawable.icon_3;
                    title = "秋天";
                    tips = "东海岸,相约看海鸥。";
                    break;
                case ICON_WINTER:
                    iconRes = R.drawable.icon_4;
                    title = "冬天";
                    tips = "大街上,传来爆竹声。";
                    break;
                default:
                    iconRes = 0;
                    title = "";
                    tips = "";
            }
            DataBean dataBean = new DataBean(iconRes, title, tips);
            mData.add(dataBean);
        }
        mAdapter.notifyDataSetChanged();
    }

    /**
     * ListView ViewHolder优化列表适配器.
     */
    public class ListViewAdapter extends BaseAdapter {
        private Context mContext;
        private List<DataBean> mDataBeans;

        public ListViewAdapter(Context context, List<DataBean> dataBeans) {
            mContext = context;
            mDataBeans = dataBeans;
        }

        @Override
        public int getCount() {
            return mDataBeans == null ? 0 : mDataBeans.size();
        }

        @Override
        public DataBean getItem(int position) {
            return mDataBeans == null ? null : mDataBeans.get(position);
        }

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

        @Override
        public int getItemViewType(int position) {
            // 通过position位置,区分奇偶数类型
            return position % 2 == 0 ? TYPE_EVEN : TYPE_ODD;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            int itemViewType = getItemViewType(position);
            Log.i(TAG, "getView: position = " + position + ", itemViewType = " + itemViewType);

            switch (itemViewType) {
                // 奇数选项
                case TYPE_ODD: {
                    // 加载奇数项布局
                    convertView = LayoutInflater.from(mContext)
                            .inflate(R.layout.layout_listview_item_type1, parent, false);
                    TextView tv1 = convertView.findViewById(R.id.tv_item1);
                    tv1.setText(getItem(position).getTitle());
                    break;
                }
                // 偶数选项
                case TYPE_EVEN: {
                    // 加载偶数项布局
                    convertView = LayoutInflater.from(mContext)
                            .inflate(R.layout.layout_listview_item_type2, parent, false);
                    ImageView iv2 = convertView.findViewById(R.id.iv_item2);
                    TextView tv2 = convertView.findViewById(R.id.tv_item2);
                    iv2.setBackgroundResource(getItem(position).getIcon());
                    tv2.setText(getItem(position).getTips());
                    break;
                }
                default:
                    break;
            }

            return convertView;
        }

    }

    /**
     * 定义列表数据对象.
     */
    public class DataBean {
        // 图标
        private int icon;
        // 标题
        private String title;
        // 内容
        private String tips;

        public DataBean(int icon, String title, String tips) {
            this.icon = icon;
            this.title = title;
            this.tips = tips;
        }

        public int getIcon() {
            return icon;
        }

        public void setIcon(int icon) {
            this.icon = icon;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getTips() {
            return tips;
        }

        public void setTips(String tips) {
            this.tips = tips;
        }
    }
}
  1. 效果图

ListView 不同类型列表项效果图

# 结束

本节介绍了 Android 中默认的列表控件(ListView)常用属性以及相关使用方法,自定义实现的布局和适配器的方法,更多属性和用法请参考:ListView (opens new window)