ListView
ListView的使用
主要分为以下几个部分:
-
ListView本身的处理
findViewById,setAdapter
-
每个Item对应的数据和布局的处理
对数据的获取,对布局的创建和处理
-
完成Adapter
在Adapter中完成数据向界面的转换
// 1. findViewById,获取ListView控件对象
ListView list_view = findViewById(R.id.list_view);
// 2. 需要有数据。显示列表型的数据
// 一般情况下数据是需要从后台获取的,这里我们自己定义数据来模拟。
initData();
// 3. ListView中的每一个item的布局,需要我们自己定义,自己实现。
// 在layout目录下,新建一个布局文件
// 4. 完成Adapter类
// Adapter:根据适配器设计模式来设计的。功能:将数据和界面进行适配。
// ListView需要的是一个个View。而我们一般情况下有的是一条条数据。
// Adapter的作用就是将数据转化成View,然后把这些View给ListView
// 5. 将数据设置进Adapter中,然后将Adapter对象设置进ListView
FruitAdapter adapter = new FruitAdapter(initData());
list_view.setAdapter(adapter);
Adapter
自定义类,继承android.widget.BaseAdapter类。然后复写BaseAdapter中的四个方法。
- Adapter的四个覆写方法的意义
getCount():告诉ListView,一共需要展示多少条数据。写法固定,几乎不会变。
@Override
public int getCount() {
return data == null ? 0 : data.size();
}
getView(): 当某个item想要展示在屏幕上时,就会调用对应position的getView方法。
/**
* 当某个item想要展示在屏幕上时,就会调用对应position的getView方法
*
* @param position 在ListView中的索引
* @param convertView 复用的View
* @param parent 当前View的容器
* @return 要展示在界面上的view
*/
@Override
@SuppressLint("ViewHolder")
public View getView(int position, View convertView, ViewGroup parent) {
// 1. 找到布局文件,加载
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
// 2. findViewById,将数据塞进去
// 不同的Item,对数据的处理不同
return view;
}
getItem()方法: AdapterView.getItemAtPosition()方法获取的值就是这个方法的返回值。其实作用不大,可以不管它。
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// ListView的想法是这样,只是我们一般不这么做
Object item = parent.getItemAtPosition(position);
}
});
getItemId()方法: 在点击事件响应的处理方法中的id的值就是这个方法的返回值。作用不大,也可以不管它。
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
// 这个方法的最后一个参数id就是Adapter中的getItemId()方法的返回值。
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
}
});
完整代码
Activity中:
initData方法只是生成一个List。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 如何使用ListView
// 1. findViewById
ListView list_view = findViewById(R.id.list_view);
// 2. 需要有数据。显示列表型的数据
// 一般情况下数据是需要从后台获取的,这里我们自己定义数据来模拟。
initData();
// 3. ListView中的每一个item的布局,需要我们自己定义,自己实现。
// 在layout目录下,新建一个布局文件
// 4. 完成Adapter类
// Adapter:根据适配器设计模式来设计的。功能:将数据和界面进行适配。
// ListView需要的是一个个View。而我们一般情况下有的是一条条数据。
// Adapter的作用就是将数据转化成View,然后把这些View给ListView
// 5. 将数据设置进Adapter中,然后将Adapter对象设置进ListView
FruitAdapter adapter = new FruitAdapter(initData());
list_view.setAdapter(adapter);
}
}
数据类:
/**
* 要显示在ListView中的数据Bean类
*/
public class Fruit {
private String name;
private int drawableRes;
// 其它的getter/setter、构造方法、toString等方法
}
item_fruit布局文件:很简单,左边一个ImageView,紧跟着一个TextView
<?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="60dp">
<ImageView
android:id="@+id/iv_fruit"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:src="@drawable/apple_pic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_fruit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="16dp"
android:text="Apple"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@id/iv_fruit"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Adapter类:
/**
* Adapter的使用过程:
* 1. 继承BaseAdapter类
* 2. 添加4个空方法。
* 3. 添加成员变量data,并且提供从外部设置data的方式
* 4. 复写getCount()方法和getView()方法。
*/
public class FruitAdapter extends BaseAdapter {
/**
* 这个data里面存的数据就是将要显示出来的数据
*/
private ArrayList<Fruit> data;
/**
* 通过构造方法将数据传递进来
* @param data
*/
public FruitAdapter(ArrayList<Fruit> data) {
this.data = data;
}
/**
* 告诉ListView,一共需要展示多少条数据。写法固定,几乎不会变。
*
* @return
*/
@Override
public int getCount() {
return data == null ? 0 : data.size();
}
/**
* 当某个item想要展示在屏幕上时,就会调用对应position的getView方法。
*
* @param position 在ListView中的索引
* @param convertView 复用的View
* @param parent 当前View的容器
* @return 要展示在界面上的view
*/
@Override
@SuppressLint("ViewHolder")
public View getView(int position, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit, parent, false);
// 将xml布局渲染成view了以后,还需要对view内部的控件设置对应的数据。
ImageView iv = view.findViewById(R.id.iv_fruit);
TextView tv = view.findViewById(R.id.tv_fruit);
Fruit fruit = data.get(position);
iv.setImageResource(fruit.getDrawableRes());
tv.setText(fruit.getName());
return view;
}
//-------------------------------------------------------------------------------------------
// 下面这两个方法我们不需要管。
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
}
优化
为什么需要优化?
ListView并不会一次性创建所有的itemView,而是只在itemView要显示到屏幕上时再调用getView方法获取itemView对象。在滑动屏幕时,ListView会不断的调用Adapter中的getView方法获取对象,就会有两个问题。
- getView中每次都要创建新的View对象,而view的inflate过程是很耗费CPU和内存资源的。
- 每次getView时,都会将view中的控件使用findViewById()方法查找一遍,这个方法也比较耗费资源。
所以在快速滑动ListView时,就会有可能出现界面卡顿的情况。
/**
* 原来的getView方法每次都是重新加载布局
*/
@Override
@SuppressLint("ViewHolder")
public View getView(int position, View convertView, ViewGroup parent) {
// 1. 找到布局文件,加载
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
// 2. findViewById,将数据塞进去
ImageView iv = view.findViewById(R.id.iv_fruit_first);
TextView tv = view.findViewById(R.id.tv_fruit_first);
Fruit fruit = data.get(position);
iv.setImageResource(fruit.getDrawableResId());
tv.setText(fruit.getName());
return view;
}
getView()方法的优化:
第一步:复用convertView
什么是convertView?
在滑动过程中,被移除的itemView,并不会被直接GC,而是存在于ListView的一个缓冲池中,以备复用。
/**
* 当某个item想要展示在屏幕上时,就会调用对应position的getView方法
*
* @param position 在ListView中的索引
* @param convertView 复用的View
* @param parent 当前View的容器
* @return 要展示在界面上的view
*/
@Override
@SuppressLint("ViewHolder")
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView==null){
// 如果convertView是null,表明这个布局需要重新渲染
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
}else {
// 否则,我们可以复用convertView
}
ImageView iv = convertView.findViewById(R.id.iv_fruit_first);
TextView tv = convertView.findViewById(R.id.tv_fruit_first);
Fruit fruit = data.get(position);
iv.setImageResource(fruit.getDrawableResId());
tv.setText(fruit.getName());
return convertView;
}
第二步:复用convertView中的控件
借助view的tag和对象,使用ViewHolder这个内部类,来减少findViewById()方法的使用
static class ViewHolder {
ImageView iv;
TextView tv;
}
/**
* 当某个item想要展示在屏幕上时,就会调用对应position的getView方法
*
* @param position 在ListView中的索引
* @param convertView 复用的View
* @param parent 当前View的容器
* @return 要展示在界面上的view
*/
@Override
@SuppressLint("ViewHolder")
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// 如果convertView是null,表明这个布局需要重新渲染
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
// 创建ViewHolder对象,将查找的控件对象赋值给ViewHolder中的成员变量,给convertView设置tag。
// 达到else分支里面可以复用的目的。
ViewHolder holder = new ViewHolder();
holder.ivFruit = convertView.findViewById(R.id.iv_fruit_first);
holder.tvFruit = convertView.findViewById(R.id.tv_fruit_first);
convertView.setTag(holder);
// 处理完了convertView和holder。不要忘记设置数据
Fruit fruit = data.get(position);
holder.ivFruit.setImageResource(fruit.getDrawableResId());
holder.tvFruit.setText(fruit.getName());
return convertView;
} else {
// 否则,我们可以复用convertView
// getView最根本的目的是两个,一个是return一个view显示;另一个是将这个view中的控件显示正常的数据
// 要想获取控件,需要先获取ViewHolder对象,通过convertView的getTag方法获取
ViewHolder holder = (ViewHolder) convertView.getTag();
Fruit fruit = data.get(position);
holder.ivFruit.setImageResource(fruit.getDrawableResId());
holder.tvFruit.setText(fruit.getName());
return convertView;
}
}
最终版本:
在之前的基础上,将代码中重复的部分处理掉。
static class ViewHolder {
ImageView iv;
TextView tv;
}
/**
* 当某个item想要展示在屏幕上时,就会调用对应position的getView方法
*
* @param position 在ListView中的索引
* @param convertView 复用的View
* @param parent 当前View的容器
* @return 要展示在界面上的view
*/
@Override
@SuppressLint("ViewHolder")
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_fruit_first, parent, false);
holder = new ViewHolder();
holder.ivFruit = convertView.findViewById(R.id.iv_fruit_first);
holder.tvFruit = convertView.findViewById(R.id.tv_fruit_first);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Fruit fruit = data.get(position);
holder.ivFruit.setImageResource(fruit.getDrawableResId());
holder.tvFruit.setText(fruit.getName());
return convertView;
}
ListView的点击事件
// ListView中的Item的点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
/**
* ListView中的Item的点击事件的回调方法
* @param parent parent其实就是当前这个listView
* @param view 点击的那个Item对应的View,也就是Adapter的getView方法返回的view
* @param position 点击的那个Item对应的索引
* @param id 就是Adapter的getItemId()方法返回的值,但是我们一般不用
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.i(TAG, "onItemClick: position: " + position);
Log.i(TAG, "onItemClick: id: " + id);
// 点击Item之后,弹出Toast,显示对应的View上的水果的名称
// 使用parent/listView.getItemAtPosition(position)就是调用Adapter中的getItem()方法
// 我们一般不用这种方式,因为数据我们能轻易获取,有了position了,使用data.get(position)更方便
//Fruit fruit = (Fruit) parent.getItemAtPosition(position);
//String msg = fruit.getName();
// 这是直接使用data的用法
String msg = adapter.data.get(position).getName();
Toast.makeText(SecondActivity.this, msg, Toast.LENGTH_SHORT).show();
}
});