ListView

2022-04-15  本文已影响0人  _戏_梦

ListView的使用

主要分为以下几个部分:

  1. ListView本身的处理

    findViewById,setAdapter

  2. 每个Item对应的数据和布局的处理

    对数据的获取,对布局的创建和处理

  3. 完成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中的四个方法。

  1. 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方法获取对象,就会有两个问题。

  1. getView中每次都要创建新的View对象,而view的inflate过程是很耗费CPU和内存资源的。
  2. 每次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();
    }
});

参考资料

上一篇下一篇

猜你喜欢

热点阅读