Android--ListView和RecyclerView控件的使用

下面我们来学习一下 ListViewRecyclerView 这两种控件的用法

最常用和最难用的控件--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和数据之间的关联就建立好了。

现在运行一下程序,效果如下图所示

image

定制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);
    }
}

现在重新运行程序,效果如图所示

image

提升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();
            }
        });
Android--ListView和RecyclerView控件的使用_第1张图片
image

当用户点击了ListView中的任何一个子项时,就会回调onItemClick()方法。这个方法中可以通过i参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果名称显示出来。

重新运行程序,效果如下所示

image

更强大的滚动控件

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);
    }
}

代码中的注释已经解释得很清楚了,现在可以运行一下程序了,效果如图所示

image

实现横向滚动

首先要对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就可以横向滚动了。

重新运行一下程序,效果如下所示

image

实现瀑布流布局

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()这个方法来取得不同长度的水果名字。

重新运行程序,效果如下所示

image

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弹出不同的内容以示区别。

重新运行程序,效果如下所示

image

内容参考自《第一行代码》

github代码

码云代码

个人网站:www.panbingwen.cn

你可能感兴趣的:(Android--ListView和RecyclerView控件的使用)