上一节学习了ListView ,我们得知了ListView 是一个垂直排列的滚动列表,如果我们想要实现横向滚动的列表,或者更加复杂的网格那么ListView 就无法实现了,而且ListView 还需要我们自行的优化内部代码,不然性能会十分的差,为此Android 提供了一个更加强大的滚动控件 RecyclerView 。它是一个增强版的ListView ,可以实现网格布局,横向滑动,垂直滑动,以及麻烦的瀑布流,ListView 有的它有,ListView 没有的它也有,我们新建个RecyClerViewTest项目准备学习RecyclerView .
4.6.1 RecyClerView 的基本用法
RecyclerView 属于新增控件,我们需要通过项目的build.gradle 中添加RecyclerView 的依赖库,才能保证RecyclerView 的使用,也就是说RecyclerView 不在SDK中,需要三方导入,在Android Studio 的project 视图中的每个Module 下都有一个 build.gradle 打开后在如下操作。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
implementation 'androidx.recyclerview:recyclerview:1.0.0' 这行就是导入了RecyclerView 。
小贴士:当鼠标放在导入的第三方库上时,Android Studio 会提示最新版本!
我们在布局文件中创建 RecyclerView :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
与ListView 相同,很简单吧。我们直接复制之前ListViewTest 项目中写的fruit_item.xml 文件,来做我们的子项布局,省的重新写代码(主要是懒~)。
接下来就是准备适配器Adapter阶段,为我们的RecyclerView 写一个适配器,创建一个类 FruitAdapter 继承 RecyclerView.Adapter 然后在这个类中 创建一个内部类ViewHolder 并且继承与RecyclerView.ViewHolder,然后重写RecyclerView.Adapter的几个方法:
class FruitAdapter(private val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
// 注意 这里是类中声明一个常量,所以要先声明常量后,后用view.findViewById ,直接使用view. 是找不到的findViewById 函数的~
inner class ViewHolder(view:View) : RecyclerView.ViewHolder(view){
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName : TextView = view.findViewById(R.id.fruitName)
}
// 创建ViewHolder
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 getItemCount(): Int = fruitList.size
// 绑定ViewHolder 后的回调,用于执行内部逻辑
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.apply {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
}
}
上面的代码我已经打上了注释,很好理解吧,在公司中有些重要的代码也要给予一定的注释。
接下来就是MainActivity 中进行初始化数据源和为RecyclerView 配置适配器以及它的数据展示样式了。
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = FruitAdapter(initFruit())
}
private fun initFruit(): List<Fruit> {
repeat(2){
fruitList.add(Fruit("Apple",R.drawable.apple_pic))
fruitList.add(Fruit("Banana",R.drawable.banana_pic))
fruitList.add(Fruit("Orange",R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon",R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear",R.drawable.pear_pic))
fruitList.add(Fruit("Grape",R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple",R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry",R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry",R.drawable.cherry_pic))
fruitList.add(Fruit("Mango",R.drawable.mango_pic))
}
return fruitList
}
}
这里我们要注意一下,因为RecyclerView 是可以设置多种不同的 布局形式所以 需要给 RecyclerView的layoutManager 进行赋值,这里赋值的是线性布局管理器,默认是垂直的,也可以设置成横向的。还有网格布局以及瀑布流布局管理器,看我们的需要而定,但是必须要进行配置layoutManager,不配置就什么都没有。
我们的RecyclerView 已经配置完毕了,可以直接启动项目。实现了和ListView 几乎一模一样的效果,但是内部代码的逻辑清晰了很多。而且RecyclerView 内部已经做完了优化,我们不需要去考虑过多的优化方式了。
4.6.2 实现横向滚动和瀑布流布局
前面我们说过RecyclerView 还可以横向滑动,这是ListView 无法做到的,那么我们来实现以下,首先调整一下子项的布局fruit_item.xml文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="wrap_content">
<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="center_horizontal"
android:layout_marginTop="10dp"
/>
</LinearLayout>
然后我们改变一下MainActivity 的线性布局管理器中的一个属性即可。其他的不需要动。
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 注意这里我们把LinearLayoutManager.orientation 设置成了横向的,默认是垂直的。
recyclerView.layoutManager = LinearLayoutManager(this).apply { orientation = LinearLayoutManager.HORIZONTAL }
recyclerView.adapter = FruitAdapter(initFruit())
}
private fun initFruit(): List<Fruit> {
repeat(2){
fruitList.add(Fruit("Apple",R.drawable.apple_pic))
fruitList.add(Fruit("Banana",R.drawable.banana_pic))
fruitList.add(Fruit("Orange",R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon",R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear",R.drawable.pear_pic))
fruitList.add(Fruit("Grape",R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple",R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry",R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry",R.drawable.cherry_pic))
fruitList.add(Fruit("Mango",R.drawable.mango_pic))
}
return fruitList
}
}
再次运行项目可以发现已经变成横向滑动的了。由于RecyclerView 的出色设计,将布局排列管理工作交给了 LayoutManager ,LayoutManager 制定了一套可拓展的布局排列接口,子类只需要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。RecyclerView 还给我们提供了 GridLayoutManager 和StaggeredGridLayoutManager ,这两个都是网格布局一个是标准网格布局,一个是错列网格布局。 接下来开始用 StaggeredGridLayoutManager 错列网格布局 来实现瀑布流效果吧。
首先还是得修改一下fruit_item.xml 布局文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
>
<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="center_horizontal"
android:gravity="left"
android:layout_marginTop="10dp"
/>
</LinearLayout>
我们可以看到宽度让我们设置成了与父容器的最大宽度相同,为什么这么设置呢,因为 瀑布流的宽度是由 布局的列数自动适配的,也就是类似于权重。
然后我们修改一下MainActivity 的代码。
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 表示3列 垂直布局。
recyclerView.layoutManager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
recyclerView.adapter = FruitAdapter(initFruit())
}
private fun initFruit(): List<Fruit> {
repeat(2){
fruitList.add(Fruit(getRandomLengthString("Apple"),R.drawable.apple_pic))
fruitList.add(Fruit(getRandomLengthString("Banana"),R.drawable.banana_pic))
fruitList.add(Fruit(getRandomLengthString("Orange"),R.drawable.orange_pic))
fruitList.add(Fruit(getRandomLengthString("Watermelon"),R.drawable.watermelon_pic))
fruitList.add(Fruit(getRandomLengthString("Pear"),R.drawable.pear_pic))
fruitList.add(Fruit(getRandomLengthString("Grape"),R.drawable.grape_pic))
fruitList.add(Fruit(getRandomLengthString("Pineapple"),R.drawable.pineapple_pic))
fruitList.add(Fruit(getRandomLengthString("Strawberry"),R.drawable.strawberry_pic))
fruitList.add(Fruit(getRandomLengthString("Cherry"),R.drawable.cherry_pic))
fruitList.add(Fruit(getRandomLengthString("Mango"),R.drawable.mango_pic))
}
return fruitList
}
private fun getRandomLengthString(str:String):String{
// IntRange(1,20).random() 区间的完整写法 IntRange(1,20) == (1..20)
val n = (1..20).random()
val builder = StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}
}
展示一下效果:
在MainActivity 的代码中,我们改动了 布局管理器 ,并且我已经给了注释,这里就不解释了,还有一处改动是初始化数据那里,我们在区间中生成一个随机数,并且用随机数执行repeat 标准函数,来给StringBuilder 多次添加,并且返回数据,就这么简单。为的就是让数据的长度不相同,更能提现瀑布流的效果。
4.6.3 RecyclerView 的点击事件
RecyclerView 没有setOnItemClickListener 这个回调方法,为什么这么优秀的控件会没有这个回调方法呢,因为有些时候我的功能并不是点击整个子项触发点击事件,而是点击子项的某个控件来触发点击事件,那么 setOnItemClickListener 就不是那么实用了,我们还需要重写子项中控件的点击事件,所以 RecyclerView 没有提供这个回调方法,而是让我们自己来实现。
实现起来很简单,就是在我们定义的适配中直接调用子控件的点击事件回调方法就行了。如果需要使用Activity中的数据该怎么办,或者改变Activity 中的某些状态,我们完全可以写一个接口回调来实现。
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.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition // 拿到ViewHolder 当前所在的子项下标位置。
Toast.makeText(parent.context,"you clicked ImageView ${fruitList[position].name}",Toast.LENGTH_SHORT).show()
}
viewHolder.itemView.fruitName.setOnClickListener {
val position = viewHolder.adapterPosition // 拿到ViewHolder 当前所在的子项下标位置。
Toast.makeText(parent.context,"you clicked TextView ${fruitList[position].name}",Toast.LENGTH_SHORT).show()
}
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition // 拿到ViewHolder 当前所在的子项下标位置。
Toast.makeText(parent.context,"you clicked itemView ${fruitList[position].name}",Toast.LENGTH_SHORT).show()
}
return viewHolder
}
我们在创建ViewHolder 的时候就实现注册点击事件。这里说明一下为什么 viewHolder.adapterPosition 放在了点击事件里面,因为ViewHolder 只创建一次。也就是onCreateViewHolder 只调用一次。放在点击事件外面那么就变成固定的下标了。始终是0。现在我们可以运行项目查看结果了。
注意,点击事件优先传递到我们点击的那个View ,如果这个View 拦截了点击事件,那么他的父View 的点击事件就不会生效了。