ListView是一种如今比较常见的组件,是用来显示多个可滑动项(Item)列表的的ViewGroup。它的优点在于可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。即使在ListView中加载非常非常多的数据,都不会发生崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。其他关于ListView的一些基础知识可参考郭神的文章从源码角度分析ListView。在这里我们只通过一些的demo来让大家了解如何使用这么一款强大的原生控件。
一个ListView的创建需要三个元素:
- 每一列Item的View
- 填入View的数据或图片等
- 连接ListView和数据的适配器Adapter
我们首先把布局工作完成。ListView的布局其实并不复杂,可以理解为有两层布局。第一是整体布局,第二是每个Item的布局。我们首先创建整体的布局:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.listviewdemo.MainActivity">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list_view"/>
</LinearLayout>
创建完该xml布局后我们可以发现预览中已经出现了我们想要的ListView的效果:
这也是系统的布局,但目前我们只需显示一行String,所以最终不考虑这样的布局。当然我们自定义这样的布局也不困难。
那么接下来再为我们的Item创建布局:item.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:orientation="vertical">
<TextView
android:id="@+id/tx_view"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1"
android:textSize="36dp"/>
</LinearLayout>
这里我们先简单的只在Item中添加一个TextView。text设置成什么并没有影响,之后会被绑定的数据所替换。
这样每一列Item的View就完成了。接着需要把数据写入到Item的TextView中并展现出来。在这之前我们需要一个连接ListView和数据的适配器Adapter,可以先自定义一个适配器MyAdapter:
public class MyAdapter extends ArrayAdapter{
private int resourceId;
private ArrayList<String> mData;
public MyAdapter(Context context, int textViewResourceId, List objects){
super(context,textViewResourceId,objects);
resourceId = textViewResourceId;
mData = (ArrayList<String>)objects;
}
public View getView(int position,View convertView,ViewGroup parent){
View view = LayoutInflater.from(getContext()).inflate(resourceId,null);
TextView tx = view.findViewById(R.id.tx_view);
tx.setText(mData.get(position));
return view;
}
}
可能有人不了解View view = LayoutInflater.from(getContext()).inflate(resourceId,null);的作用是啥,那咱们来拆分一下。
//加载布局管理器
LayoutInflater inflater = LayoutInflater.from(context);
//将xml布局转换为view对象
convertView = inflater.inflate();
//利用view对象找到布局中的组件
convertView.findViewById();
因为在TextView中我只简单的添加字符串,因此我把要接受(适配)的数据规定为String类型。如果你想要实现Item中有很多控件(如按钮、图片、文字等)可以先自定义一个类,然后再规定适配器所要适配的数据类型(下面会有实例)。getView方法我们可以把它认为是把数据绑定View并返回,即之前所说的填入View的数据或图片等。在getView中我们通过每个Item的位置position来确定他所要加载的数据。
最后我们需要完成MainActivity并写入我们所要呈现的数据,设置ListView与我们的适配器相关联。MainActivity:
public class MainActivity extends Activity {
private ListView listView;
private MyAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.list_view);
mAdapter = new MyAdapter(MainActivity.this,R.layout.item,getData());
listView.setAdapter(mAdapter);
}
private ArrayList<String> getData(){
ArrayList<String> mData = new ArrayList<>();
String temp = "item";
for(int i=1;i<41;i++)
mData.add(temp + i);
return mData;
}
}
核心代码就是定义适配器并setAdapter。这里MyAdapter()传入的三个数据分别为:相关上下文context、子Item的布局、所要适配的数据。最后效果:(这里由于录屏软件的问题并没有把所有Item都录进去)
上面应该能说最简单的ListView实现了吧。接下来稍微增加一点难度,Item里面实现多个组件。那么就需要定义一个类来“容纳它们”,并设置它为适配器类型,同时需要改变Item的布局。
每个Item中包含学生的照片,学号和姓名,定义类MyClass:
public class MyClass{
private String name;
private String id;
private int imageId;
public MyClass(String name,String id,int imageId){
this.id = id;
this.imageId = imageId;
this.name = name;
}
public String getName(){
return name;
}
public String getId(){
return id;
}
public int getImageId(){
return imageId;
}
}
同时修改我们的布局。此时如果继续采用LinearLayout布局可能会出现嵌套,所以我们采用相对布局RelativeLayout:
<?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/image"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:adjustViewBounds="true"
android:padding="2dp" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/image"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:gravity="center_vertical"
android:layout_marginTop="10dp"
android:textSize="25dp"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_toRightOf="@id/image"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:layout_below="@id/title"
android:textSize="40dp"/>
</RelativeLayout>
这里通过layout_alignParent来设置控件相对于父容器的位置。通过layout_toRightOf和layout_below来确定相互之间的位置关系。这里需要注意如何设置第一个TextView layout_above第二个TextView,最后第一个TextView可能会显示不出来。具体原因目前也不太清楚,按照网上的方法试验下来也不起效果,如果哪位同学知道原因可直接在评论区留言。
布局完成之后我们就需要自定义一个类来“包含”这些内容。MyClass:
public class MyClass{
private String name;
private String id;
private int imageId;
public MyClass(String name,String id,int imageId){
this.id = id;
this.imageId = imageId;
this.name = name;
}
public String getName(){
return name;
}
public String getStId(){
return id;
}
public int getImageId(){
return imageId;
}
}
比较简单,就定义了三个值。三个返回函数也是为了等会能在getView方法中绑定数据使用。
接着我们需要改变我们的适配器所适配的数据对象类型,注意接收对象改为MyClass类型。MyAdapter:
public class MyAdapter extends ArrayAdapter<MyClass>{
private int resourceId;
public MyAdapter(Context context, int textViewResourceId, List<MyClass> objects){
super(context,textViewResourceId,objects);
resourceId = textViewResourceId;
}
public View getView(int position,View converView,ViewGroup parent){
MyClass stu = getItem(position); //直接通过getItem方法获取当前实例
View view = LayoutInflater.from(getContext()).inflate(resourceId,null);
TextView title = view.findViewById(R.id.title);
TextView text = view.findViewById(R.id.text);
ImageView image = view.findViewById(R.id.image);
title.setText(stu.getStId());
text.setText(stu.getName());
image.setImageResource(stu.getImageId());
return view;
}
}
最后在MainActivity中同步修改接受数据的类型。MainAcitvity:
public class MainActivity extends Activity {
private ListView listView;
private MyAdapter mAdapter;
private String[] names = {"曹一","孙二","张三","李四","王五","赵六","广七","刘八","夏九","付十"};
private String[] ids = {"10001","10002","10003","10004","10005","10006","10007","10008","10009","10010"};
private MyClass[] students = new MyClass[10];
private List<MyClass> list = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.list_view);
for (int i=0;i<10;i++){
students[i] = new MyClass(names[i],ids[i],R.drawable.ic_launcher_background);
list.add(students[i]);
}
mAdapter = new MyAdapter(MainActivity.this,R.layout.item,list);//最后的数据类型发生改变
listView.setAdapter(mAdapter);
}
}
放上最后的效果图(这里偷懒我就直接用了系统自带的背景图啦~):
那么稍复杂的ListView我们也就完成啦。同理我们可以继续添加其他如Button等的控件来实现需求。
ListView基本使用相信大家都已掌握,接下来咱们将进一步学习ListView的优化以及它的点击事件处理。
相信大家都已经注意到了,在适配器的getView中我们每次都需要通过.inflate方法来加载一个布局,即使是已加载过但被划出屏幕的布局也需重新加载,这显然是十分不合理的。细看该方法,发现有个参数我们从来没有使用过,对就是convertView,简单来说他的作用是缓存了ListView中已经加载好的View。这样就可以用它来优化加载布局问题。修改getView方法:
public View getView(int position,View convertView,ViewGroup parent){
MyClass stu = getItem(position);
View view;
if(convertView==null) //如果未加载过,则加载。否则直接使用convertView对象
view = LayoutInflater.from(getContext()).inflate(resourceId,null);
else
view = convertView;
TextView title = view.findViewById(R.id.title);
TextView text = view.findViewById(R.id.text);
ImageView image = view.findViewById(R.id.image);
title.setText(stu.getStId());
text.setText(stu.getName());
image.setImageResource(stu.getImageId());
return view;
}
同时我们发现每次我们都需要调用findViewById方法来获取一次控件的代码,这同样也是不合理的。那是否有办法可以对此进行优化呢?答案是有。我们可以新增内部类ViewHolder来缓存控件的实例。convertView为空时,会将控件的实例存放在ViewHolder里,然后用setTag方法将ViewHolder对象存储在view里。convertView不为空时,用getTag方法获取viewHolder对象。我们再次修改getView,并增加内部类ViewHolder:
public View getView(int position,View convertView,ViewGroup parent){
MyClass stu = getItem(position);
View view;
ViewHolder viewHolder;
if(convertView==null){
view = LayoutInflater.from(getContext()).inflate(resourceId,null);
viewHolder = new ViewHolder();
viewHolder.image = view.findViewById(R.id.image);
viewHolder.text = view.findViewById(R.id.text);
viewHolder.title = view.findViewById(R.id.title);
view.setTag(viewHolder);
}
else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
viewHolder.title.setText(stu.getStId());
viewHolder.text.setText(stu.getName());
viewHolder.image.setImageResource(stu.getImageId());
return view;
}
class ViewHolder{
TextView title;
TextView text;
ImageView image;
}
- 判断convertView是否为空优化加载布局
- 设置ViewHolder优化加载控件
这样ListView的优化工作也就完成了。最后我们来说一下ListView中点击Item的事件处理。和普通的监听事件相类似,我们只需重写onItemClick方法即可,这里我们就直接贴出代码:
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MyClass stud = list.get(position);
Toast.makeText(MainActivity.this,"你点击了学号为"+stud.getStId()+"的"+stud.getName()+"同学",Toast.LENGTH_SHORT).show();
}
});
最后效果图(最后的鼠标点击gif上看的可能有点问题但是实际操作是OK的):
到这里ListView我们算是基本都了解了一遍,之后我们将会开始了解RecyclerView。希望大家都有所收获一起进步。