RecyclerView(二)—— RecyclerView的使用

RecyclerView

ListView由于强大的功能,在过去的Android开发当中可以说是贡献卓越,直到今天仍然还有不计其数的程序在使用ListView。不过ListView并不是完美无缺的,比如如果不使用一些技巧来提升它的运行效率,那么ListView的性能就会非常差。还有,ListView的扩展性也不够好, 它只能实现数据纵向滚动的效果,如果我们想实现横向滚动的话,ListView是做不到的。

为此,Android提供了一个更强大的滚动控件——RecyclerView。它可以说是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView存在的各种不足之处。 目前Android官方更加推荐使用RecyclerView,未来也会有更多的程序逐渐从ListView转向RecyclerView

1 RecyclerView的基本用法

和之前我们所学的所有控件不同,RecyclerView属于新增控件,那么怎样才能让新增的控件在所有Android系统版本上都能使用呢?为此,GoogleRecyclerView控件定义在了AndroidX当中,我们只需要在项目的build.gradle中添加RecyclerView库的依赖,就能保证在所有Android系统版本上都可以使用RecyclerView控件了。

打开app/build.gradle文件,在dependencies闭包中添加如下内容:

dependencies {

    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
}

接下来在activity_main.xml中:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

在布局中加入RecyclerView控件。需要注意的是,由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。

接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。其中,ViewHolder是我们在FruitAdapter中定义的一个内部类,代码如下所示:

class FruitAdapter(private val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size
}

这是RecyclerView适配器标准的写法,虽然看上去好像多了好几个方法,但其实它比ListView的适配器要更容易理解。这里我们首先定义了一个内部类ViewHolder,它要继承自RecyclerView.ViewHolder。然后ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewById()方法来获取布局中ImageViewTextView的实例了。

FruitAdapter中也有一个主构造函数,它用于把要展示的数据源传进来,我们后续的操作都将在这个数据源的基础上进行。

由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()onBindViewHolder()getItemCount()3个方法。 onCreateViewHolder()方法是用于创建ViewHolder实例的,我们在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入构造函数当中,最后将ViewHolder的实例返回。onBindViewHolder()方法用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里我们通过 position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder.ImageViewTextView当中即可。getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。

适配器准备好了之后,我们就可以开始使用RecyclerView了,修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        recyclerView = findViewById(R.id.recyclerView)
        initFruits()
        val layoutManager = LinearLayoutManager(this)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recyclerView.adapter = adapter
    }

    ...
}

这里使用了一个同样的initFruits()方法,用于初始化所有的水果数据。接着在onCreate()方法中先创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中。**LayoutManager用于指定RecyclerView的布局方式,这里使用的 LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。**接下来我们创建了FruitAdapter的实例,并将水果数据传入FruitAdapter的构造函数中,最后调用RecyclerView.setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成了。

2 实现横向滚动和瀑布流布局

ListView的扩展性并不好,它只能实现纵向滚动的效果,如果想进行横向滚动 的话,ListView就做不到了。RecyclerView就能做得到。

首先要对fruit_item布局进行修改,因为目前这个布局里面的元素是水平排列的,适用于纵向滚动的场景,而如果我们要实现横向滚动的话,应该把fruit_item里的元素改成垂直排列才比较合理。修改fruit_item.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="80dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_marginTop="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginTop="10dp" />

</LinearLayout>

LinearLayout改成垂直方向排列,并把宽度设为80dp。这里将宽度指定为 固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView的子项就会有长有短,非常不美观,而如果用match_parent的话,就会导致宽度过长,一个子项占满整个屏幕。然后我们将ImageView和TextView都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一定距离。

接下来修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById(R.id.recyclerView)
        initFruits()

        val layoutManager = LinearLayoutManager(this)
        layoutManager.orientation = LinearLayoutManager.HORIZONTAL
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recyclerView.adapter = adapter
    }
   ...
}

MainActivity中只加入了一行代码,调用LinearLayoutManager.setOrientation()方法设置布局的排列方向。默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL表示让布局横行排列,这样RecyclerView就可以横向滚动了。

横向的ListView

ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManagerLayoutManager制定了一套可扩展的布局排列接口, 子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。

除了LinearLayoutManager之外,RecyclerView还给我们提供了GridLayoutManagerStaggeredGridLayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。 这里我们来实现 一下效果更加炫酷的瀑布流布局。

首先还是来修改一下fruit_item.xml中的代码,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginTop="10dp" />

</LinearLayout>

首先将LinearLayout的宽度由80dp改成了match_parent,因为瀑布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。其次我们使用了layout_margin属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。最后还将TextView的对齐属性改成了居左对齐,因为待会我们会将文字的长度变长,如果还是居中显示就会感觉怪怪的。

接着修改MainActivity中的代码,如下所示:

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById(R.id.recyclerView)
        initFruits()

        val layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recyclerView.adapter = adapter
    }

    private fun initFruits() {
        repeat(2) {
            fruitList.add(Fruit(getRandomLengthString("白菜"), R.drawable.cai))
            fruitList.add(Fruit(getRandomLengthString("香肠"), R.drawable.chang))
            fruitList.add(Fruit(getRandomLengthString("葱姜蒜"), R.drawable.congjiang))
            fruitList.add(Fruit(getRandomLengthString("鸡蛋"), R.drawable.dan))
            fruitList.add(Fruit(getRandomLengthString("香菇"), R.drawable.gu))
            fruitList.add(Fruit(getRandomLengthString("西红柿"), R.drawable.hongshi))
            fruitList.add(Fruit(getRandomLengthString("胡萝卜"), R.drawable.luo))
            fruitList.add(Fruit(getRandomLengthString("猕猴桃"), R.drawable.mihoutao))
            fruitList.add(Fruit(getRandomLengthString("柠檬"), R.drawable.ningmeng))
            fruitList.add(Fruit(getRandomLengthString("牛油果"), R.drawable.niuyouguo))
            fruitList.add(Fruit(getRandomLengthString("葡萄"), R.drawable.putao))
            fruitList.add(Fruit(getRandomLengthString("青椒"), R.drawable.qingjiao))
            fruitList.add(Fruit(getRandomLengthString("肉"), R.drawable.rou))
            fruitList.add(Fruit(getRandomLengthString("山竹"), R.drawable.shanzhu))
            fruitList.add(Fruit(getRandomLengthString("土豆"), R.drawable.tudou))
            fruitList.add(Fruit(getRandomLengthString("虾"), R.drawable.xia))
            fruitList.add(Fruit(getRandomLengthString("西瓜"), R.drawable.xigua))
            fruitList.add(Fruit(getRandomLengthString("玉米"), R.drawable.yumi))
        }
    }

    private fun getRandomLengthString(str: String): String {
        val n = (1..20).random()
        val builder = StringBuilder()
        repeat(n) {
            builder.append(str)
        }
        return builder.toString()
    }
}

首先,在onCreate()方法中,我们创建了一个StaggeredGridLayoutManager的实例。 StaggeredGridLayoutManager的构造函数接收两个参数:第一个参数用于指定布局的列数,传入3表示会把布局分为3列;第二个参数用于指定布局的排列方向,传入 StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列。最后把创建好的实例设置到RecyclerView当中就可以了。

不过由于瀑布流布局需要各个子项的高度不一致才能看出明显的效果,这里定义了一个getRandomLengthString()方法,这个方法中调用了Range对象的random()函数来创造一个120之间的随机数,然后将参数中传入的字符串随机重复几遍。在initFruits()方法中,每个水果的名字都改成调用getRandomLengthString()这个方法来生成,这样就能保证各水果名字的长短差距比较大,子项的高度也就各不相同了。

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

瀑布流

3 RecyclerView的点击事件

ListView一样,RecyclerView也必须能响应点击事件才可以,不然的话就没什么实际用途 了。不过不同于ListView的是,RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件。这相比于ListView来说,实现起来要复杂一些。

那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView,偏偏在点击事件上却没有处理得非常好呢?其实不是这样的,ListView在点击事件上的处理并不人性化, setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的某一个按钮呢?虽然ListView也能做到,但是实现起来就相对比较麻烦了。为此,RecyclerView干脆直接摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View去注册,就再没有这个困扰了。

下面我们来具体学习一下如何在RecyclerView中注册点击事件,修改FruitAdapter中的代 码,如下所示:

class FruitAdapter(private val fruitList: List<Fruit>) :
    RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        val viewHolder = ViewHolder(view)
        viewHolder.itemView.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "you clicked view ${fruit.name}", Toast.LENGTH_LONG)
                .show()
        }
        viewHolder.fruitImage.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context, "you clicked image ${fruit.name}", Toast.LENGTH_LONG)
                .show()
        }

        return viewHolder
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.imageId)
        holder.fruitName.text = fruit.name
    }

    override fun getItemCount() = fruitList.size
}

可以看到,这里我们是在onCreateViewHolder()方法中注册点击事件。上述代码分别为最外层布局和ImageView都注册了点击事件,itemView表示的就是最外层布局。RecyclerView的强大之处也在于此,它可以轻松实现子项中任意控件或布局的点击事件。我们在两个点击事件中先获取了用户点击的position,然后通过position拿到相应的Fruit实例,再使用Toast分别弹出两种不同的内容以示区别。

现在重新运行代码,并点击苹果的图片部分,可以看到,触发了ImageView的点击事件。然后点击文字部分,由于TextView并没有注册点击事件,因此点击文字这个事件会被子项的最外层布局捕获。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值