【Android】ListView与RecyclerView的基础使用
一、ListView
1. ListView 的基本用法
1.1 在布局中声明 ListView
<?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" >
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
1.2 准备数据源(通常是一个字符串列表)
String[] data = {"Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango"};
1.3 创建 Adapter(适配器)
ArrayAdapter<String> adapter = new ArrayAdapter<>(
MainActivity.this, // 当前上下文
android.R.layout.simple_list_item_1, // 内置的 item 布局
data // 数据源
);
数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android 中提供了很多适配器的实现类,其中 ArrayAdapter 可以通过泛型来指定。
android.R.layout.simple_list_item_1
:这是一个预定义的Android系统布局资源ID,用于显示单行文本的列表项。它是一个非常简单的布局,通常用于显示文本内容的列表项。在这里,我们将使用这个布局来显示data
数组中的每个图片名称。
1.4 绑定 Adapter 到 ListView
ListView listView = findViewById(R.id.listView);
listView.setAdapter(adapter);
完整代码如下:
private String[] data = {"Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
运行程序,效果如下,可通过滚动的方式来查看屏幕外的数据:
2. 定制 ListView 的界面
2.1 定义一个实体类
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;
}
}
2.2 新建一个自定义布局
<?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">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>
在这个布局中,我们定义了一个 ImageView 用于显示水果的图片,又定义了一个TextView 用于显示水果的名称,并让TextView在垂直方向上居中显示。
2.3 创建一个自定义适配器
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前Fruit实例
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}
FruitAdapter 重写了父类的一组构造函数,用于将上下文、ListView 子项布局的 id 和数据都传递进来。另外又重写了 getView() 方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。在 getView() 方法中,首先通过 getItem() 方法得到当前项的 Fruit 实例,然后使用 LayoutInflater 来为这个子项加载我们传入的布局。
修改 MainActivity 中的代码如下所示:
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 = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
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);
}
}
这里添加了一个 initFruits() 方法,用于初始化所有水果数据。运行程序,效果如下图所示:
3. ListView 的性能优化
目前我们 ListView 的运行效率是很低的,因为在 FruitAdapter的getView() 方法中,每次都将布局重新加载了一遍,当 ListView 快速滚动的时候,这就会成为性能的瓶颈。仔细观察会发现,getview() 方法中还有一个 convertView 参数,这个参数用于将之前加载好的布局进行缓存,以便之后可以进行重用。修改FruitAdapter 中的代码,如下所示:
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前Fruit实例
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
}
可以看到,现在我们在 getView() 方法中进行了判断,如果 convertView 为 null ,则使用 LayoutInflater 去加载布局,如果不为 null 则直接对 convertView 进行重用。这样就大大提高了 ListView 的运行效率,在快速滚动的时候也可以表现出更好的性能。
不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是每次在 getView() 方法中还是会调用 View 的 findViewById() 方法来获取一次控件的实例。 我们可以借助一个 ViewHolder 来对这部分性能进行优化,修改 FruitAdapter 中的代码,如下所示:
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前Fruit实例
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
viewHolder = new ViewHolder();
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder); // 将 ViewHolder 存储在 View 中
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag(); // 重新获取 ViewHolder
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder {
ImageView fruitImage;
TextView fruitName;
}
}
这里新增了一个内部类 ViewHolder,用于对控件的实例进行缓存。当 convertView 为 null 的时候,创建一个 ViewHolder 对象,并将控件的实例都存放在ViewHolder 里,然后调用 View 的 setTag() 方法,将 ViewHolder 对象存储在 View 中。当 convertView 不为 null 的时候,则调用 View 的 getTag() 方法,把 ViewHolder 重新取出。这样所有控件的实例都缓存在了 ViewHolder 里,就没有必要每次都通过 findViewById() 方法来获取控件实例了。 通过这两步优化之后,ListView 的运行效率就已经非常不错了。
4. ListView 的点击事件
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@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 = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this, fruit.getName(), Toast.LENGTH_SHORT).show();
}
});
}
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);
}
}
}
可以看到,我们使用 setonItemClickListener() 方法为 ListView 注册了一个监听器,当用户点击了 ListView 中的任何一个子项时,就会回调 onItemClick() 方法。在这个方法中可以通过 position 参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过 Toast 将水果的名字显示出来。
运行程序,效果如下:
二、RecyclerView
ListView
在 Android 中 仍然可以使用,但在现代开发中 已经不推荐 首选它了。Google 自 Android 5.0(API 21)起推荐使用 RecyclerView
替代 ListView
。
ListView
的局限性:
- 没有 ViewHolder 自动复用机制(需要手动写);
- 缺乏灵活的布局管理(只能垂直排列);
- 不支持分隔线样式定制、滑动动画、复杂多类型 item;
- 性能和扩展性较差。
1. RecyclerView 的基本用法
1.1 准备一个适配器
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
虽然这段代码看上去好像有点长,但其实它比 ListView 的适配器要更容易理解。这里我们首先定义了一个内部类 ViewHolder,ViewHolder 要继承自 RecyclerView.ViewHolder。然后 ViewHolder 的构造函数中要传入一个 View 参数,这个参数通常就是 Recycler View子项的最外层布局,那么我们就可以通过 findViewById() 方法来获取到布局中的 ImageView 和 TextView 的实例了。
FruitAdapter 中也有一个构造函数,这个方法用于把要展示的数据源传进来, 并赋值给一个全局变量mFruitList,我们后续的操作都将在这个数据源的基础上进行。
由于 FruitAdapter
继承自 RecyclerView.Adapter
,必须重写以下三个核心方法:
onCreateViewHolder()
:创建并返回一个ViewHolder
对象,负责加载每个子项的布局。onBindViewHolder()
:将数据绑定到ViewHolder
上,在子项显示时调用,用于设置具体内容。getItemCount()
:返回数据项的总数,告诉 RecyclerView 有多少个子项要显示。
这三个方法共同完成了列表项的创建、复用和数据绑定,是实现 RecyclerView 的基础。
1.2 使用这个适配器
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits(); // 初始化水果数据
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
}
}
}
在 initFruits()
方法中初始化水果数据。onCreate()
中先获取 RecyclerView
实例,然后创建 LinearLayoutManager
并设置给 RecyclerView
,实现类似 ListView
的线性布局。接着创建 FruitAdapter
并传入数据,最后调用 setAdapter()
设置适配器,完成数据与列表的绑定。
运行程序,效果如下:
2. 实现横向滚动和瀑布流
2.1 横向滚动
只需加一行代码layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits(); // 初始化水果数据
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
效果如下,可横向滚动:
2.2 瀑布流暴布局
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits(); // 初始化水果数据
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
StaggeredGridLayoutManager layoutManager = new
StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
}
}
private String getRandomLengthName(String name) {
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(name);
}
return builder.toString();
}
}
构造 StaggeredGridLayoutManager 对象,构造函数的两个参数,第一个参数表示指定布局的列数,第二个参数指定布局的排列方式。
注意 fruit_item.xml 中,LinearLayout 的宽度应该是 macth_parent,因为瀑布流的宽度应该是根据布局的列数自动适配的。
效果如下:
3. RecyclerView 的点击事件
RecycleView需要我们为子项具体的View注册点击事件,修改FruitAdapter中代码如下:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
View fruitView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitView = view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
final ViewHolder holder = new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit =mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked view" + fruit.getName(),
Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
Fruit fruit =mFruitList.get(position);
Toast.makeText(v.getContext(), "you clicked image" + fruit.getName(),
Toast.LENGTH_SHORT).show();
}
});
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
我们先是修改了 ViewHolder,在 ViewHolder 中添加了 fruitView 变量来保存子项最外层布局的实例,然后在 onCreateviewHolder() 方法中注册点击事件就可以了。这里分别为最外层布局和 ImageView 都注册了点击事件,RecyclerView 的强大之处也在这里,它可以轻松实现子项中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的 positio,然后通过 position 拿到相应的 Fruit 实例,再使用 Toast 分别弹出两种不同的内容以示区别。由于 TextView 并没有注册点击事件,因此点击文字这个事件会被子项的最外层布局捕获到。
int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
我们先是修改了 ViewHolder,在 ViewHolder 中添加了 fruitView 变量来保存子项最外层布局的实例,然后在 onCreateviewHolder() 方法中注册点击事件就可以了。这里分别为最外层布局和 ImageView 都注册了点击事件,RecyclerView 的强大之处也在这里,它可以轻松实现子项中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的 positio,然后通过 position 拿到相应的 Fruit 实例,再使用 Toast 分别弹出两种不同的内容以示区别。由于 TextView 并没有注册点击事件,因此点击文字这个事件会被子项的最外层布局捕获到。