下面我们来学习一下 ListView 和 RecyclerView 这两种控件的用法
最常用和最难用的控件--ListView
ListView绝对可以称得上是Android应用程序中最常用的控件之一,比起前面介绍的几种控件,ListView也是相对比较复杂的。
ListView的简单用法
首先来创建一个ListViewTest项目,让Android Studio自动为我们创建好活动,然后修改activity_main.xml中的代码,如下所示
这里设置ListView的宽度和高度都是match_parent,这样ListView就可以占满整个布局的空间。接下来修改MainActivity中的代码,如下所示
public class MainActivity extends AppCompatActivity {
private String[] data={"张三","李四","王五","赵六","陈浮生","陈富贵","竹叶青","陈龙象","陈半仙","王虎胜","张三千"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapteradapter=new ArrayAdapter(
MainActivity.this,android.R.layout.simple_list_item_1,data);
ListView listView = findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
ListView是用来展示大量的数据的,所以我们先将数据提供好,这里用一个data数组来测试,里面随便写了一些人名。
不过数据中的数据是无法直接传递给ListView的,我们需要借助适配器来完成。Android中提供了许多的适配器的实现类,不过我认为最好用的就是 ArrayAdapter。它可以通过泛型来指定来适配的数据类型,然后在构造函数中把要适配的数据传入。ArrayAdapter有多个构造函数的重载,应根据实际情况选择最适合的一种。这里由于我们提供的数据是都是,因此将ArrayAdapter的泛型指定为String。ArrayAdapter的构造函数中依次传入当前上下文,ListView子项布局的id,以及要适配的数据。注意,我们使用了 R.layout.simple_list_item_1,data 作为ListView子项布局的id,这是Android内置的一个布局文件,里面只有一个TextView,只用于简单的显示一段文本。这样适配器对象就构建好了。
最后还需要调用ListView的 setAdapter() 方法将适配器对象传递进去,这样ListView和数据之间的关联就建立好了。
现在运行一下程序,效果如下图所示
定制ListView界面
现在我们来定制更加丰富的ListView。首先准备一组图片,等会我们要让水果名称旁边都有一个图片
接着定义一个实体类,作为适配器的适配类型。新建类Fruit,代码如下所示
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
这个类中只有两个字段,name是水果的名称,imageId是水果对应图片的id。然后再新建一个布局fruit_item.xml作为ListView的子项布局,代码如下所示
我们这里定义了一个ImageView用于显示水果对应的图片,TextView用于显示水果的名称,并让TextView在垂直方向上剧中显示
接下来创建一个自定义适配器,这个适配器继承 ArrayAdapter ,泛型指定为Fruit类。代码如下所示
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(@NonNull Context context, int resource, @NonNull List objects) {
super(context, resource, objects);
resourceId=resource;
}
/**
* 每个子项滚动到屏幕内的时候getView()都会被调用
*/
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
// 获取当前项的Fruit实例
Fruit fruit = getItem(position);
// 加载我们传入的布局
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
// 获取ImageView实例
ImageView fruitImage = view.findViewById(R.id.fruit_image);
// 获取TextView实例
TextView fruitName = view.findViewById(R.id.fruit_name);
// 为ImageView设置要显示的图片
fruitImage.setImageResource(fruit.getImageId());
// 为TextView设置要显示的名字
fruitName.setText(fruit.getName());
// 将布局返回
return view;
}
}
自定义适配器完成了,接下来修改MainActivity中代码,如下所示
public class MainActivity extends AppCompatActivity {
private String[] data = {"张三", "李四", "王五", "赵六", "陈浮生", "陈富贵", "竹叶青", "陈龙象", "陈半仙", "王虎胜", "张三千"};
private ListfruitList=new ArrayList<>();
public void initFruits()
{
for (int i=0;i<2;i++)
{
Fruit zs = new Fruit("张三", R.drawable.apple_pic);
fruitList.add(zs);
Fruit ls = new Fruit("李四", R.drawable.banana_pic);
fruitList.add(ls);
Fruit ww = new Fruit("王五", R.drawable.orange_pic);
fruitList.add(ww);
Fruit cl = new Fruit("赵六", R.drawable.watermelon_pic);
fruitList.add(cl);
Fruit cfs = new Fruit("陈浮生", R.drawable.pear_pic);
fruitList.add(cfs);
Fruit clx = new Fruit("陈龙象", R.drawable.grape_pic);
fruitList.add(clx);
Fruit cbx = new Fruit("陈半仙", R.drawable.pineapple_pic);
fruitList.add(cbx);
Fruit zsq = new Fruit("张三千", R.drawable.strawberry_pic);
fruitList.add(zsq);
Fruit cys = new Fruit("陈圆殊", R.drawable.cherry_pic);
fruitList.add(cys);
Fruit whs = new Fruit("王虎胜", R.drawable.mango_pic);
fruitList.add(whs);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化水果数据
initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listView = findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
现在重新运行程序,效果如图所示
提升ListView的运行效率
目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView()方法中,每次都要重新加载一次布局,当ListView快速滚动的时候,这就成为了性能瓶颈。
getView()方法中有一个convertView参数,这个参数是用来缓存加载好的布局,以便之后可以进行重用。修改FruitAdapter中的代码,如下所示
public class FruitAdapter extends ArrayAdapter {
private int resourceId;
public FruitAdapter(@NonNull Context context, int resource, @NonNull List objects) {
super(context, resource, objects);
resourceId=resource;
}
/**
* 每个子项滚动到屏幕内的时候getView()都会被调用
*/
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
// 获取当前项的Fruit实例
Fruit fruit = getItem(position);
// 加载我们传入的布局
View view;
ViewHolder viewHolder;
// convertView是将我们加载好的布局进行缓存
if (convertView==null)
{
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
viewHolder=new ViewHolder();
viewHolder.imageView = view.findViewById(R.id.fruit_image);
viewHolder.textView = view.findViewById(R.id.fruit_name);
view.setTag(viewHolder);
}else {
view=convertView;
viewHolder =(ViewHolder) view.getTag();
}
viewHolder.imageView.setImageResource(fruit.getImageId());
viewHolder.textView.setText(fruit.getName());
// 将视图返回
return view;
}
// 定义内部类
class ViewHolder{
ImageView imageView;
TextView textView;
}
}
我们在getView()中对convertView进行了判断,如果convertView为null,则用LayoutInflater去加载布局,如果不为null,则直接对convertView进行重用。这就可以大大提高了ListView的运行效率,不过还可以进一步优化,虽然不用再重复加载布局,但是每次在getView()方法中还是会调用View的findViewById()方法来获取一次控件的实例。我们可以借助一个ViewHolder来对这一部分进行性能优化。
我们新增了一个内部类 ViewHolder,用于对控件的实例进行缓存,当 convertView 为null的时候,创建一个viewHolder实例,并将控件实例都存放到viewHolder里,再调用setTag()方法将 ViewHolder存储到View当中,当convertView不为null的时候,调用View的getTag()方法重新取出ViewHolder,这样就不用每次都需要通过findViewById()方法来重新获取控件实例了。
通过这两步优化之后,我们的ListView运行效率就非常不错了。
ListView的点击事件
下面来学习一下ListView如何才能实现用户的点击事件
在MainActivity的onCreate()方法中添加如下代码
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
Fruit fruit = fruitList.get(i);
Toast.makeText(MainActivity.this, fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
当用户点击了ListView中的任何一个子项时,就会回调onItemClick()方法。这个方法中可以通过i参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果名称显示出来。
重新运行程序,效果如下所示
更强大的滚动控件
ListView并不是没有缺点,如果我们不使用一些技巧来对ListView进行一些优化的话,那么ListView的运行效率是很差的。还有,ListView的扩展性也不好,它只可以实现纵向滚动,如果要想实现横向滚动,ListView是做不到的。
为此Android提供了一个更强大的滚动控件--RecyclerView,它可以说是增强版的ListView。它不仅可以轻松实现ListView相同的效果,还优化好ListView存在的各种不足。目前官方更加推荐使用RecyclerView。下面我们就来学习一下RecyclerView的用法。
首先创建好一个RecyclerViewTest项目,并让Android Studio自动为我们创建好活动。
RecyclerView的基本用法
和百分比布局类似,RecyclerView也是新增布局,因此需要在项目的build.gradle添加相应的依赖库才行。
打开app/build.gradle文件,在dependencies闭包中添加如下内容
implementation 'androidx.recyclerview:recyclerview:1.0.0'
添加完后记得点击Sync Now来进行同步,然后修改activity_main.xml中的代码,如下所示
让RecyclerView占满整个屏幕,因为RecyclerView并不是内置在系统SDK当中,所以需要写完整的包路径。
下面我们来实现和ListView相同的效果,为简单起见,我们直接从ListViewTest项目中将图片、Fruit类、fruit_item.xml文件复制过来。
首先为RecyclerView准备一个适配器,新建FruitAdapter类,让它继承RecyclerView.Adapter,其中泛型指定为FruitAdapter.ViewHolder,ViewHolder是FruitAdapter中定义的一个内部类,代码如下所示
public class FruitAdapter extends RecyclerView.Adapter {
private List mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView fruitImage;
TextView fruitName;
public ViewHolder(@NonNull View itemView) {
super(itemView);
// 分别获取到布局中的ImageView和TextView实例
fruitImage = itemView.findViewById(R.id.fruit_image);
fruitName = itemView.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List mFruitList) {
this.mFruitList = mFruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 把fruit_item布局加载进来
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
// 创建ViewHolder实例,并把加载进来的fruit_item传到构造函数
ViewHolder holder = new ViewHolder(view);
// 返回ViewHolder实例
return holder;
}
/**
* onBindViewHolder()会给RecyclerView的子项数据赋值,在每一个子项滚到屏幕内会被执行
*/
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
// 通过position参数得到当前项的Fruit实例
Fruit fruit = mFruitList.get(position);
// 将fruit数据设置到ViewHolder的ImageView和TextView当中
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
// 返回数据源的长度,告诉RecyclerView共有多少子项
return mFruitList.size();
}
}
这段代码看上去有点长,但是却比ListView更容易理解。这里我们定义了一个内部类ViewHolder,它继承自 RecyclerView.ViewHolder ,在ViewHolder的构造函数当中传入一个View参数,这个参数通过就是RecyclerView的最外层布局,那么我们就可以通过findViewById()方法获取到ImageView和TextView的实例了。
接着向下看,FruitAdapter中也有一个构造函数。这个方法用于要展示的数据源传进来,然后赋值给一个全局变量mFruitList,我们后继的操作都将在这个数据源上进行。
继续往下看,由于FruitAdapter继承自RecyclerView.Adapter,所以必须重写 onCreateViewHolder()、 onBindViewHolder()、 getItemCount() 这3个方法。
onCreateViewHolder() 方法是用来创建ViewHolder实例的。我们先把fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载进来的布局传递到ViewHolder构造函数当中,最后把ViewHolder实例返回。
onBindViewHolder() 方法是对RecyclerView的子项数据进行赋值的。会在每个子项滚动到屏幕内的时候执行,通过position参数得到当前项的Fruit实例,然后将数据设置到ViewHolder的ImageView和TextView当中
getItemCount() 方法就非常简单了,用来告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
适配器准备好之后,就可以使用RecyclerView了,修改MainActivity代码,如下所示
public class MainActivity extends AppCompatActivity {
private List fruitList = new ArrayList<>();
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit(("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit(("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit(("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit(("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化水果数据
initFruits();
// 先获得到RecyclerView的实例
RecyclerView recyclerView = findViewById(R.id.recycler_view);
// 指定RecyclerView的布局方式,这里LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
// 创建FruitAdapter实例,并把水果的数据传递到FruitAdapter的构造函数当中
FruitAdapter adapter = new FruitAdapter(fruitList);
// 完成适配器设置,RecyclerView和数据间的关联就建立完成了
recyclerView.setAdapter(adapter);
}
}
代码中的注释已经解释得很清楚了,现在可以运行一下程序了,效果如图所示
实现横向滚动
首先要对fruit_item布局进行修改,将里面的元素改成垂直排列才比较合理
,修改fruit_item.xml中的代码,如下所示
接下来修改MainActivity中的代码,如下所示
package com.example.recyclerviewtest;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List fruitList = new ArrayList<>();
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit(("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit(("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit(("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit(("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化水果数据
initFruits();
// 先获得到RecyclerView的实例
RecyclerView recyclerView = findViewById(R.id.recycler_view);
// 指定RecyclerView的布局方式,这里LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
// 设置布局的排列方式,默认是纵向的,下面设置成横向的
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
// 创建FruitAdapter实例,并把水果的数据传递到FruitAdapter的构造函数当中
FruitAdapter adapter = new FruitAdapter(fruitList);
// 完成适配器设置,RecyclerView和数据间的关联就建立完成了
recyclerView.setAdapter(adapter);
}
}
调用LinearLayoutManager的 setOrientation()方法来设置布局的排列方向,默认是纵向的,传入 LinearLayoutManager.HORIZONTAL 来设置布局的排列方式为横向,这样RecyclerView就可以横向滚动了。
重新运行一下程序,效果如下所示
实现瀑布流布局
StaggeredGridLayoutManager 可以用于实现瀑布流布局
首先来修改一下fruit_item.xml中的代码,如下所示
这里我们将LinearLayout的宽度由100dp改成match_parent,因为瀑布流布局的宽度是根据布局的列数来自动适配的,而不是一个固定值。
接下来修改MainActivity中的代码,如下所示
public class MainActivity extends AppCompatActivity {
private List fruitList = new ArrayList<>();
private String getRandomLengthName(String name)
{
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i=0;i
由于瀑布流布局需要各个子项的高度不一致才能看出明显的效果,所以这里使用了getRandomLengthName()这个方法来取得不同长度的水果名字。
重新运行程序,效果如下所示
RecyclerView点击事件
RecyclerView摒弃了子项点击事件的监听器,所有点击事件都由具体的View去实现。下面我们来具体学习一下在RecyclerView中如何注册点击事件,修改FruitAdapter中的代码,如下所示
public class FruitAdapter extends RecyclerView.Adapter {
private List mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView fruitImage;
TextView fruitName;
View fruitView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
// 分别获取到布局中的ImageView和TextView实例
fruitView=itemView;
fruitImage = itemView.findViewById(R.id.fruit_image);
fruitName = itemView.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List mFruitList) {
this.mFruitList = mFruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 把fruit_item布局加载进来
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
// 创建ViewHolder实例,并把加载进来的fruit_item传到构造函数
final ViewHolder holder = new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(),"You clicked view"+fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(),"You clicked Image"+fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
// 返回ViewHolder实例
return holder;
}
/**
* onBindViewHolder()会给RecyclerView的子项数据赋值,在每一个子项滚到屏幕内会被执行
*/
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
// 通过position参数得到当前项的Fruit实例
Fruit fruit = mFruitList.get(position);
// 将fruit数据设置到ViewHolder的ImageView和TextView当中
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
// 返回数据源的长度,告诉RecyclerView共有多少子项
return mFruitList.size();
}
}
我们先修改ViewHolder,在ViewHolder中添加fruitView变量来保存子项最外层布局的实例(View就是子项最外层布局),然后在onCreateViewHolder()注册点击事件就可以了。这里分别为最外层布局和ImageView都注册了点击事件,RecyclerView中的强大之处就在这里,它可以轻松实现子项中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的position,然后通过position拿到相应的Fruit实例,再分别用Toast弹出不同的内容以示区别。
重新运行程序,效果如下所示
内容参考自《第一行代码》