黑马程序员技术交流社区

标题: Android 开发必备 : ListView(下) [打印本页]

作者: 张老师    时间: 2017-1-11 14:56
标题: Android 开发必备 : ListView(下)
本帖最后由 就业部_安卓组 于 2017-1-11 14:56 编辑

上篇。首先我们要先做数据,这里我创建了一个 CustomBean.java
[Java] 纯文本查看 复制代码
package com.mesmerize.listviewapp;

/**
* Created by mesmerize on 2017/1/5.
*
* 数据
*/
public class CustomBean {

    // 昵称
    public String nick;

    // 最后一条聊天信息
    public String chatMessage;

    // 头像
    public int headImage;

    // 最后聊天的时间
    public String time;

    public CustomBean(String nick, String chatMessage, int headImage, String time) {

        this.nick = nick;
        this.chatMessage = chatMessage;
        this.headImage = headImage;
        this.time = time;
    }
}

让我们看看 activity_main.xml
[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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.mesmerize.listviewapp.MainActivity">

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

</RelativeLayout>

很简单,就是一个单纯的 ListView,其他没什么控件了,然后呢?没有然后了,就这么多就可以了,接下来再创建一个 CustomActivity.java
[Java] 纯文本查看 复制代码
package com.mesmerize.listviewapp;

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

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

/**
* Created by mesmerize on 2017/1/5.
*
* 自定义 ListView
*/

public class CustomActivity extends Activity {

    private List<CustomBean> customBeanList;

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

        customBeanList = new ArrayList<>();

        ListView mListView = (ListView) findViewById(R.id.lv_listview);

    }
}

写到这里你可能会有疑问了,我们找到了 ListView 这个控件,也有了 List 集合,接下来怎么办呢?如何将数据与视图做关联呢?
不知道你还是否记得前面咱们说过 ListView 是通过 Adapter 来将视图与数据进行关联的,在 Adapter 对象构造的时候将数据传递过去,并且在 Adapter 中定义 ListView 每一个 item 条目的视图,然后通过 ListView 的 setAdapter() 方法进行关联。那接下来咱们来创建一个 Adapter,看看这个 Adapter 是如何编写的。
[Java] 纯文本查看 复制代码
package com.mesmerize.listviewapp;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

/**
* Created by mesmerize on 2017/1/5.
*/

public class CustomAdapter extends BaseAdapter {


    private final CustomActivity context;
    private final List<CustomBean> list;

    public CustomAdapter(CustomActivity customActivity, List<CustomBean> customBeanList) {

        this.context = customActivity;
        this.list = customBeanList;
    }

    @Override
    public int getCount() {

        return list.size();
    }

    @Override
    public Object getItem(int position) {

        return null;
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = View.inflate(context, R.layout.adapter_custom, null);

        ImageView head = (ImageView) view.findViewById(R.id.head);
        TextView nick = (TextView) view.findViewById(R.id.nick);
        TextView message = (TextView) view.findViewById(R.id.message);
        TextView time = (TextView) view.findViewById(R.id.time);


        head.setImageResource(list.get(position).headImage);
        nick.setText(list.get(position).nick);
        message.setText(list.get(position).chatMessage);
        time.setText(list.get(position).time);

        return view;
    }
}


在这里,我们的 Adapter 继承的是 BaseAdapter,之后重写四个方法,在这里我们重点关注两个方法就够了,分别是 getCount() 以及 getView() 这两个方法。可以看到,在这里,我们的 CustomAdapter 构造方法里面接收了两个参数,分别是 Context 上下文以及 List这两个参数。上下文是需要的,在填充一个 view 视图的时候需要用到,在这里我们传进来的上下文是 CustomActivity,而第二个参数的意思是 view 需要显示的数据是哪些。这两个缺一不可。接下来看 getCount() 和 getView()。
首先 getCount() 很简单,就是指 ListView 一共有多少条,返回值是一个 int,我们这里只需要看传进来的 List 的 size 是多少就可以了,所以直接 return list.size() 就可以了。这样,系统就知道要绘制多少次了。
其次,getView(),顾名思义,就是视图的意思,他的返回值是一个 View。里面的参数暂时先不用管,待会会讲到他们的意思。
在这里,我们首先要告诉系统我们的 item 的视图长什么样子,所以第一行代码的意义在此:
[Java] 纯文本查看 复制代码
View view = View.inflate(context, R.layout.adapter_custom, null);

这个可以理解为,将 R.layout.adapter_custom 这个布局填充成了一个 view 对象,我们可以通过这个对象查找其中的 view 控件。这个就相当于咱们在 activity 中 findViewById 一样,只不过在这之前要加上 view,我们需要通过 view.findViewById 的形式来查找。其实在 Activity 中,是通过 Activity.this.findViewById 的形式来查找的,只不过一般咱们都省略了 this 罢了。
我们看一看 adapter_custom.xml 布局文件:
[Java] 纯文本查看 复制代码
<?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">

    <ImageView
        android:id="@+id/head"
        android:layout_margin="5dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/head_jinx"/>

    <TextView
        android:id="@+id/nick"
        android:layout_toRightOf="@id/head"
        android:textSize="18sp"
        android:layout_marginLeft="9dp"
        android:layout_marginTop="12dp"
        android:textColor="#000000"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="金克斯"/>

    <TextView
        android:id="@+id/message"
        android:layout_marginLeft="7dp"
        android:layout_marginTop="15dp"
        android:layout_below="@id/nick"
        android:layout_toRightOf="@id/head"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="啊哈哈哈哈啊哈哈哈啊哈哈哈"/>

    <TextView
        android:id="@+id/time"
        android:text="15:33"
        android:layout_alignParentRight="true"
        android:layout_marginTop="15dp"
        android:layout_marginRight="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

布局显示如下:




也就是说,咱们的每一个 item 都是长这个样子。在这里,为了显示方便,我写了一些死数据,真实开发中不建议写死数据,这一点切记。
然后我们将这个 xml 布局填充成一个 view 对象之后,要将此 view 返回出去,告知系统咱们的 view 视图。接下来,咱们就要做些事情了,先是通过 findViewById 将 xml 布局中的控件一个个的查找出来:

[Java] 纯文本查看 复制代码
ImageView head = (ImageView) view.findViewById(R.id.head);
        TextView nick = (TextView) view.findViewById(R.id.nick);
        TextView message = (TextView) view.findViewById(R.id.message);
        TextView time = (TextView) view.findViewById(R.id.time);



查找出来之后就好办了,接下来就开始操作这些控件了,咱们只需要将对应的数据一个个的给相应的控件 set 上就可以了,代码如下:
[Java] 纯文本查看 复制代码

head.setImageResource(list.get(position).headImage);
nick.setText(list.get(position).nick);
message.setText(list.get(position).chatMessage);
time.setText(list.get(position).time);

给头像设置上图片,昵称、聊天内容、最近聊天时间一个个的都填充上。这样,咱们的 adapter 就写好了。接下来只差最后一步了,我们需要将 adapter 与 ListView 做关联,打开咱们的 CustomActivity,加上这句代码:
[Java] 纯文本查看 复制代码
CustomAdapter mAdapter = new CustomAdapter(this, customBeanList);

mListView.setAdapter(mAdapter);

意思是,首先我们创建了 Adapter 对象,并且将上下文以及 List 传递进去,接下来调用 ListView 的 setAdapter 方法关联,这样就可以了。但是这个时候我们运行会发现程序一片空白,什么也没有。这是为什么呢?因为我们没有数据啊!
我们创建了 List 的确不假,但是我们没有给它添加数据啊,那这个 List 的size 不就是 0吗?如果是 0 的话,系统在绘制的时候肯定绘制 0 次啊!所以显示一片空白是很正常的。那现在我们要造一点数据了。我这里是这么写的:
[Java] 纯文本查看 复制代码
 /**
     * 初始化数据
     */
    private void initCustomList(){

       CustomBean vn = new CustomBean("薇恩", "走,开黑去,我带你秀起来", R.mipmap.head_vn, "20:23");
         customBeanList.add(vn);

         CustomBean lks = new CustomBean("拉克丝", "我刚买的大天使元素,来来来,带你上分", R.mipmap.head_lakesi, "17:33");
         customBeanList.add(lks);

         CustomBean ey = new CustomBean("好运", "啊哈哈,啊哈哈哈哈,哈啊啊哈哈啊", R.mipmap.head_eyun, "16:53");
         customBeanList.add(ey);

         CustomBean xz = new CustomBean("瞎子", "我用双手成就你的梦想,一库", R.mipmap.head_liqing, "16:15");
         customBeanList.add(xz);

         CustomBean ylst = new CustomBean("伊利斯坦", "我的滑板鞋时尚时尚最时尚", R.mipmap.head_ylst, "15:25");
         customBeanList.add(ylst);

         CustomBean irly = new CustomBean("艾瑞莉娅", "不要再削我了,呜呜~~~~(>_<)~~~~", R.mipmap.head_irly, "14:22");
         customBeanList.add(irly);

         CustomBean ali = new CustomBean("阿狸", "你渴望什么呢?", R.mipmap.head_ali, "13:55");
         customBeanList.add(ali);

         CustomBean jinx = new CustomBean("金克斯", "我数到三......  3", R.mipmap.head_jinx, "13:53");
         customBeanList.add(jinx);


    }

创建了一个 initCustomList 方法,然后里面造了一大堆数据,并且将他们都添加到 customBeanList 集合中去。接下来,咱们只需要调用一下这个方法执行就可以了,因此,最终 CustomActivity . java 的代码如下:
[Java] 纯文本查看 复制代码
package com.mesmerize.listviewapp;

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

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

/**
* Created by mesmerize on 2017/1/5.
*
* 自定义 ListView
*/

public class CustomActivity extends Activity {

    private List<CustomBean> customBeanList;

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

        customBeanList = new ArrayList<>();

        initCustomList();

        ListView mListView = (ListView) findViewById(R.id.lv_listview);

        CustomAdapter mAdapter = new CustomAdapter(this, customBeanList);

        mListView.setAdapter(mAdapter);

    }

    /**
     * 初始化数据
     */
    private void initCustomList(){
        CustomBean vn = new CustomBean("薇恩", "走,开黑去,我带你秀起来", R.mipmap.head_vn, "20:23");
        customBeanList.add(vn);

        CustomBean lks = new CustomBean("拉克丝", "我刚买的大天使元素,来来来,带你上分", R.mipmap.head_lakesi, "17:33");
        customBeanList.add(lks);

        CustomBean ey = new CustomBean("好运", "啊哈哈,啊哈哈哈哈,哈啊啊哈哈啊", R.mipmap.head_eyun, "16:53");
        customBeanList.add(ey);

        CustomBean xz = new CustomBean("瞎子", "我用双手成就你的梦想,一库", R.mipmap.head_liqing, "16:15");
        customBeanList.add(xz);

        CustomBean ylst = new CustomBean("伊利斯坦", "我的滑板鞋时尚时尚最时尚", R.mipmap.head_ylst, "15:25");
        customBeanList.add(ylst);

        CustomBean irly = new CustomBean("艾瑞莉娅", "不要再削我了,呜呜~~~~(>_<)~~~~", R.mipmap.head_irly, "14:22");
        customBeanList.add(irly);

        CustomBean ali = new CustomBean("阿狸", "你渴望什么呢?", R.mipmap.head_ali, "13:55");
        customBeanList.add(ali);

        CustomBean jinx = new CustomBean("金克斯", "我数到三......  3", R.mipmap.head_jinx, "13:53");
        customBeanList.add(jinx);

    }
}

数据还是有点少,这个时候我们可以多添加几次,for 循环一下就可以了,然后运行一下,看看效果。


滑动一下看看:


ok 了,以上就是一个简单的用法。我们可以自定义修改 xml 的布局,就可以定制出各种复杂的界面了,简单吧?快去试试吧!

ListView 的效率

前面说到 ListView 很难用,这么说是有原因的,不知道各位在实现了上述效果的时候是否有疑惑,“就是这样吗?”,“就这么简单吗”,如果你这么想了,那就对了。因为我们还有很多的细节可以优化,而且咱们的代码也有个隐患,这也关乎到运行效率。这样的代码,如果加载的数据量过多的话,程序直接就蹦了。当前我们的这个程序的运行效率也是很低的,问题就出在 CustomAdapter 这个类中的 getView() 方法中。在这里,由于篇幅的原因,我就不演示了,直接告诉大家测试的方法,大家可以自行去测一下。大家可以在 getView() 方法中打印一下,会发现每次滑动出一个新条目的时候都会创建一个新的对象,每次 getView() 这个方法都会执行一次的。
这个现象是很严重的,这相当于每次都将布局重新加载了一遍,可以想象,当我们快速滑动 ListView 的话会有什么样的后果,这很有可能会成为性能的瓶颈,那该怎么办?
别担心,我走这条路已经很多年了,稳的狠,上车,我带你。
让我们先把焦点放在 getView() 这个方法上面,我们发现getView() 方法中有一个 convertView 的参数,这个参数用于将之前加载好的布局进行缓存,方便我们之后进行重用。这就很好的解决了我们上面所提到的重复构建对象、每次重复绘制布局的问题了,该怎么使用呢?看代码:
[Java] 纯文本查看 复制代码
public class CustomAdapter extends BaseAdapter {


    private final CustomActivity context;
    private final List<CustomBean> list;

    ...

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        CustomBean item = list.get(position);

        View view;

        if (convertView == null) {

            view =  View.inflate(context, R.layout.adapter_custom, null);
        } else {

            view = convertView;
        }

        ImageView head = (ImageView) view.findViewById(R.id.head);
        TextView nick = (TextView) view.findViewById(R.id.nick);
        TextView message = (TextView) view.findViewById(R.id.message);
        TextView time = (TextView) view.findViewById(R.id.time);


        head.setImageResource(item.headImage);
        nick.setText(item.nick);
        message.setText(item.chatMessage);
        time.setText(item.time);

        return view;
    }
}

在这里,我们修改了一些代码,在 getView 方法中对 convertView 做了判断,如果为 null,则去填充一个布局,否则直接重用 convertView。这样就大大提高了 ListView 的运行效率,哪怕我们快速滑动也不会崩溃了。同时还对 CustomBean 做一层抽取,方便后续代码的简化,做到这一步,就完成了咱们的优化。但是仅仅如此吗?
肯定不止,我们还可以继续做优化的,为什么还要做优化呢?因为我们现在的代码虽然不会重复去加载布局,但是每次调用 getView() 方法还是会调用 View 的findViewById() 方法来获取控件的实例,那么我们下一步的优化就是对它下手,在这里我们需要使用 ViewHolder 的方法来对这部分做优化了,代码如下:

[Java] 纯文本查看 复制代码
package com.mesmerize.listviewapp;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

/**
* Created by mesmerize on 2017/1/5.
*/

public class CustomAdapter extends BaseAdapter {


    private final CustomActivity context;
    private final List<CustomBean> list;

    ...

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        CustomBean item = list.get(position);

        ViewHolder holder;

        if (convertView == null) {

            convertView =  View.inflate(context, R.layout.adapter_custom, null);
            holder = new ViewHolder();

            holder.head = (ImageView) convertView.findViewById(R.id.head);
            holder.nick = (TextView) convertView.findViewById(R.id.nick);
            holder.message = (TextView) convertView.findViewById(R.id.message);
            holder.time = (TextView) convertView.findViewById(R.id.time);

            convertView.setTag(holder);
        } else {

            holder = (ViewHolder) convertView.getTag();
        }


        holder.head.setImageResource(item.headImage);
        holder.nick.setText(item.nick);
        holder.message.setText(item.chatMessage);
        holder.time.setText(item.time);

        return convertView;
    }

    class ViewHolder {

        ImageView head;
        TextView nick,message,time;
    }
}

在这里修改的代码比较多,我们新创建了一个内部类 ViewHolder,这个是为了对每个控件的实例进行缓存。然后判断 convertView 为 null 的时候,我们创建一个 ViewHolder 对象,并且每次 findViewById 查找出来的控件实例统一由 ViewHolder 进行管理,最后调用了 View 的 setTag() 方法。可以将之理解为打上一个标记,将 ViewHolder 对象存储在了 View 中。当 convertView 不为 null 的时候通过 getTag() 的方法将 ViewHolder 取出。这样所有控件的实例都缓存在了 ViewHolder 中,这样就节省了 findViewById 的次数。
简而言之,我们所做的事就是把控件的实例存放在 ViewHolder 中,又通过 setTag() 将其放入了 view 中。
做了以上两步优化之后,运行效率就可以了。

ListView 的 Item 点击事件

我们仔细观察 QQ ,甚至压根不用仔细观察就知道,每个 Item 都是可以点击的,而不是单纯的展示。现在我们所写的这个 ListView 还不能点击,如果加上点击事件,该怎么做呢?
其实仔细翻阅 ListView 的 API 会发现,ListView 有一个 API 叫 setOnItemClickListener(),从名字上来看好像是给条目设置一个点击事件,那我们试试,修改 Activity 的代码,如下:
[Java] 纯文本查看 复制代码
package com.mesmerize.listviewapp;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;

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

/**
* Created by mesmerize on 2017/1/5.
*
* 自定义 ListView
*/

public class CustomActivity extends Activity {

    private List<CustomBean> customBeanList;

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

        customBeanList = new ArrayList<>();

        initCustomList();

        ListView mListView = (ListView) findViewById(R.id.lv_listview);

        CustomAdapter mAdapter = new CustomAdapter(this, customBeanList);

        mListView.setAdapter(mAdapter);

        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

                Toast.makeText(CustomActivity.this, "哈哈哈 我是 " + customBeanList.get(position).nick, Toast.LENGTH_SHORT).show();
            }
        });
    }

    /**
     * 初始化数据
     */
    private void initCustomList(){

        for (int i = 0; i < 3; i++) {

            CustomBean vn = new CustomBean("薇恩", "走,开黑去,我带你秀起来", R.mipmap.head_vn, "20:23");
            customBeanList.add(vn);

            CustomBean lks = new CustomBean("拉克丝", "我刚买的大天使元素,来来来,带你上分", R.mipmap.head_lakesi, "17:33");
            customBeanList.add(lks);

            CustomBean ey = new CustomBean("好运", "啊哈哈,啊哈哈哈哈,哈啊啊哈哈啊", R.mipmap.head_eyun, "16:53");
            customBeanList.add(ey);

            CustomBean xz = new CustomBean("瞎子", "我用双手成就你的梦想,一库", R.mipmap.head_liqing, "16:15");
            customBeanList.add(xz);

            CustomBean ylst = new CustomBean("伊利斯坦", "我的滑板鞋时尚时尚最时尚", R.mipmap.head_ylst, "15:25");
            customBeanList.add(ylst);

            CustomBean irly = new CustomBean("艾瑞莉娅", "不要再削我了,呜呜~~~~(>_<)~~~~", R.mipmap.head_irly, "14:22");
            customBeanList.add(irly);

            CustomBean ali = new CustomBean("阿狸", "你渴望什么呢?", R.mipmap.head_ali, "13:55");
            customBeanList.add(ali);

            CustomBean jinx = new CustomBean("金克斯", "我数到三......  3", R.mipmap.head_jinx, "13:53");
            customBeanList.add(jinx);
        }

    }
}

我们为其 set 了一个监听器,当点击其中任何一个 item 时都会回调 onItemClick() 方法,我们在这个方法里面弹了一个 Toast,点击的时候弹出该项 item 的 nick,通过 position 参数判断点击的是哪一个子项,然后获取相应的 nick 并提示。
以上就是 ListView 的一些基本用法,收获了这么多,赶紧去试试吧!
源码在底部,敬请下载查阅。






       Android 人事+技术总贴
       Android 基础篇总贴
       Android 进阶篇总贴
       Android 讨论区

       以上言论,如有错误或者表达不准确或者有纰漏的地方还请指正,同时欢迎大家在评论区留言给予一定的建议,我会根据大家的意见进行相应的改正,谢谢各位!


ListViewApp.zip

1.25 MB, 下载次数: 44






欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2