提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。
一、常用控件
Android的TextView、Button、EditText、ImageView组件对你来说都太容易啦,此处就不再赘述了。我们只简单讲一下AlertDialog。
1.1 AlertDialog
在 Kotlin 中,setPositiveButton和setNegativeButton分别用于设置对话框中确认按钮和取消按钮的点击逻辑。
①setPositiveButton()用于设置对话框中的确认按钮,该方法接受一个字符串参数,用于显示确认按钮的文本。以下是一个使用setPositiveButton()的示例代码:
val mDialog = AlertDialog.Builder(context)
.setTitle("提示")
.setMessage("确定要执行此操作吗?")
.setPositiveButton("确定") { dialog, which ->
// dialog是当前的AlertDialog实例(mDialog) which是被按下按钮的ID(在这个例子中是"OK"按钮)
// 当用户点击"OK"按钮时 lambda表达式中的逻辑会被执行
}
.create()
mDialog.show()
在上面的示例中,我们创建了一个 AlertDialog 对话框。然后设置了标题、提示消息和确认按钮。当用户点击确定按钮时,会执行相应的操作。
②setNegativeButton()用于设置对话框中的取消按钮,setNegativeButton的使用方式与setPositiveButton类似,接收一个字符串参数用于显示按钮的文本,并使用重载函数来指定一个点击事件监听器。
以下是使用setNegativeButton的示例代码:
val dialog = AlertDialog.Builder(context)
· · ·
.setNegativeButton("取消") { _, _ ->
// 取消按钮点击事件
}
.create()
dialog.show()
确认按钮和取消按钮可以同时设置再AlertDialog上,以提供对话框中的两个按钮及它们的点击逻辑。这里你可能会有疑问,按钮点击事件里的下划线是什么意思呢?Kotlin允许我们将没有用到的参数使用下划线来替代。在这个例子中,setPositiveButton()方法有两个下划线,表示这个Lambda表达式接受两个参数。这两个下划线被命名为’_',表示我们在这个特定的Lambda表达式中并没有使用到这两个参数。
不要忘记调用create()方法来构建Dialog,并且通过show()方法将其显示出来!
二、基本布局
布局是一种可用于放置很多控件的容器,他可以按照一定的规律调整内部控件的位置。布局的内部除了可以放置控件外,还可以放置布局。通过布局的多层嵌套,就能够实现一些比较复杂的界面。
2.1 LinearLayout(线性布局)
LinearLayout又称之为线性布局,这个布局会将其内部所包含的控件在线性方向上依次排列。我们可以通过 android:orientation=“horizontal” 属性来指定线性布局内部控件的排列方向:
- vertical :垂直方向排序(默认)
- horizontal :水平方向排列。
2.2 RelativeLayout
RelativeLayout是通过相对定位的方式让控件出现在布局的任何位置,下面是一个RelativeLayout的简单示例:
<RelativeLayout 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="horizontal"
tools:context=".MainActivity">
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 2" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 4" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 5" />
</RelativeLayout>
它的效果如下:
有几个使用点需要你注意:
①android:layout_alignRight属性表示让一个控件的右边与另一个控件的右边缘对齐,下图中的Button6就通过该属性让它和Button2右边缘对齐。
②当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。
在上面的例子中,Button6需要跟Button2进行右边缘对其,所以Button6必须要定义在Button2后面,不然会出现找不到Button2的情况。
2.3 FrameLayout
FrameLayout也叫做帧布局,它的应用场景很少,它会将所有控件默认摆放在布局的左上角。
下面是一个简单的FrameLayout例子:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, World!" />
<Button
android:id="@+id/button_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</FrameLayout>
效果如下:
当然你也可以通过layout_gravity属性来指定控件在布局中的对其方式。layout_gravity和gravity都是Android布局中用于设置对齐方式的属性,但它们的作用范围和应用对象有所不同。
- layout_gravity:这是用于设置控件相对于其父容器的对齐方式。例如,一个按钮上的文本可以使用这个属性来调整其在按钮内部的位置。
- gravity:gravity属性则是设置子元素在该容器内的对齐方式。例如,一个TextView中的文本内容可以使用gravity属性来调整其在TextView内部的位置。
2.4 引入布局
我们所有用到的控件都是直接或者间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。
接下来我们尝试给我们的应用程序增加一个标题栏,在layout目录下新建一个自定义的标题栏布局title.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg">
//标题栏返回键
<Button
android:id="@+id/titleBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="Back"
android:textColor="#fff" />
//标题栏文字
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp" />
//标题栏编辑按钮
<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff" />
</LinearLayout>
效果如下图所示
接下来我们在activity_main.xml中通过引用这个控件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
//引入标题栏控件
<include layout="@layout/title" />
</LinearLayout>
通过这种方式不论我们需要在多少个布局中添加标题栏,只需要一行include就可以了。
2.5 创建自定义控件
在前面的例子中,我们成功通过引入了布局的方式在activity_main.xml中引入了标题栏。但是你有没有想过一个问题,标题栏中的Back和Edit按钮是需要能够响应(点击)事件的,并且他们的点击事件都基本相同。这就使得我们不管是在哪一个Activity中,只要是引入了这个标题栏,都需要在Activity中重新注册一遍返回按钮的点击事件,而且还写了很多重复的代码!为了解决这个问题,可以使用自定义控件。
新建一个Kotlin Class/File文件TitleLayout,并让其继承自LinearLayout。我们来让TitleLayout成为我们的自定义控件吧:
主构造函数 继承
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
//主构造函数逻辑
init {
//通过LayouInflater.from()构建出一个LayoutInflater对象 再调用inflate()动态加载布局文件
//inflate()接收两个参数:要加载的布局文件,给加载好的布局文件添加一个父布局
LayoutInflater.from(context).inflate(R.layout.title, this)
}
}
接下来我们就可以在activity_main.xml中直接添加这个自定义控件了:
需要注意的是,添加自定义控件的时候,我们需要指明自定义控件的完整类名,包名在这里是不可以省略的。
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".MainActivity">
//添加自定义控件
<com.example.uiwidgettest.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
接下来我们尝试为自定义的标题栏控件按钮注册点击事件:
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
//主构造函数
init {
LayoutInflater.from(context).inflate(R.layout.title, this)
//返回按钮的点击事件
titleBack.setOnClickListener {
类型强制转换
val activity = context as Activity
activity.finish()
}
//编辑按钮点击事件
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit Button", Toast.LENGTH_SHORT).show()
}
}
}
这样子我们就通过添加自定义控件的方式将标题栏引入到activity_main.xml中,并且在任何一个Activity中,只要添加了自定义标题栏这个控件,就已经包含了Back和Edit按钮的响应事件。
三、熟悉又陌生的ListView
3.1 ListView的简单用法
ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,而屏幕上原有的数据则会滚动出屏幕。为了使用ListView,先要在activity_main.xml中添加ListView控件:
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".MainActivity">
//添加ListView控件
<ListView
android:id="@+id/my_listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
我们让这个ListView的宽高都跟随父类,这样ListView就占满了整个屏幕。接下来我们在MainActivity中为ListView填充数据:
class MainActivity : AppCompatActivity() {
//ListView的数据源
private val data = listOf("Apple","Banana",···"Cherry","Mango")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//创建适配器 集合数据无法直接传递给ListView 需要通过适配器adapter来进行
泛型 Context ListView列表子项的布局样式 填充的数据
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)
//使用setAdapter()方法为ListView设置适配器
my_listView.adapter = adapter
}
}
需要注意的是,如果想往ListView中填充数据,需要借助适配器(adapter) 来帮我们完成。 ArrayAdapter是一个比较好用的适配器实现类 ,它可以通过泛型来指定要适配的数据类型,我们只需要在ArrayAdapter的构造函数中传入上下文、列表子项布局以及数据就OK了。最后我们通过ListView控件调用setAdapter() 方法将适配器传入,这样ListView和数据就绑定在一起了。
3.2 定制ListView的界面
让我们来尝试实现更高级的ListView界面,让ListView不只是简单的显示文本,而是显示水果的图片和名字。首先定义一个水果实体类Fruit作为ListView适配器泛型的类型,Fruit类的主构造函数要求传入水果的图片和名字。这样当我们创建Fruit对象的时候,必须传入构造函数中要求的所有参数。下面是Fruit类的代码:
//水果实体类(name,image)
class Fruit(val name: String, val imageId: Int) {
}
所有在主构造函数中声明成val或者var的参数会自动成为该类的字段,上面这段代码的作用实际上在编译后等同于如下Java代码:
public final class Fruit {
private final String name;
private final int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
}
言归正传,接下来让我们新建一个名为fruit_item.xml的自定义布局,作为ListView列表子项的样式:
// ListView列表子项的样式
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
//水果图片
<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
//水果名称
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>
接下来我们创建一个自定义的适配器FruitAdapter,这个适配器需要继承自ArrayAdapter,并将ArrayAdapter的泛型指定为Fruit类。FruitAdapter类的代码如下:
Activity实例, ListView列表子项布局id 填充的数据 泛型
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) : ArrayAdapter<Fruit>(activity, resourceId, data) {
// 重写getView()方法获取当前位置的视图
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)// ListView列表子项对象
val fruitImage: ImageView = view.findViewById(R.id.fruitImage) // 水果图片ImageView对象
val fruitName: TextView = view.findViewById(R.id.fruitName) // 水果名称TextView对象
// 通过getItem()获取当前fruit实例
val fruit = getItem(position)
if (fruit != null) {
// 通过setXX()方法设置显示的图片和文字
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
FruitAdapter的主构造函数中,resourceId参数表示的是ListView列表子项的布局ID,这个值在整个生命周期内都不会改变,因此使用val关键字声明它是一个只读属性,这样可以提高代码的安全性和可读性。最后修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
//创建一个ArrayList<Fruit>用来保存列表项的数据
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化水果列表数据
initFruits()
自定义适配器 上下文 ListView的列表项 填充的数据
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
//将水果列表数据绑定到ListView上
my_listView.adapter = adapter
}
private fun initFruits() {
//repeat(n)方法会将Lambda表达式中的内容执行n遍
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple))
fruitList.add(Fruit("Banana", R.drawable.banana))
fruitList.add(Fruit("Orange", R.drawable.orange))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon))
fruitList.add(Fruit("Pear", R.drawable.pear))
fruitList.add(Fruit("Grape", R.drawable.grape))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry))
fruitList.add(Fruit("Cherry", R.drawable.cherry))
fruitList.add(Fruit("Mango", R.drawable.mango))
}
}
}
这样我们就实现了带图片的ListView,下面是他的运行效果图。
现在我们来梳理一下这一小节的操作:
创建Fruit实体类(name, image) ——> 创建列表子项布局文件 fruit_item.xml ——> 创建自定义适配器 FruitAdapter(用于向ListView中传递数据) ——> 通过FruitAdapter自定义适配器将数据传递给ListView
3.3 提升ListView的运行效率
在之前的FruitAdapter.getView()代码中,每次都需要将布局重新加载一遍。当我们快速滚动ListView时,这就会成为性能的瓶颈:
Activity实例, ListView列表项布局id 填充的数据
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) : ArrayAdapter<Fruit>(activity, resourceId, data) {
缓存布局
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//每次都需要重新重新加载布局
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruit = getItem(position)
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
在getView()方法中还有一个convertView参数,它的作用是将之前加载好的布局进行缓存,以便后面进行重用。我们可以借助convertView参数来进行性能优化。 我们可以在FruitAdapter.getView()方法中增加一个convertView的判断逻辑,如果有缓存则直接复用,如果没有缓存则使用LayoutInflater加载布局代码:
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View
if (convertView != null) {
//若convertView不为空则直接复用
view = convertView
} else {
//若convertView为空则使用LayoutInflater加载布局
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
}
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruit = getItem(position)
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}
这样我们的布局就不会重复加载了。当然我们还有其他地方也可以进行优化,例如在getView()方法中我们每次都要通过findViewById()来获取控件实例。我们可以借助一个ViewHolder内部类,用于对ImageView和TextView的控件实例进行缓存:
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
//Kotlin中通过inner class关键字定义内部类
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View
val viewHolder: ViewHolder
if (convertView != null) {
view = convertView
//从View.tag中取出ViewHolder
viewHolder = view.tag as ViewHolder
} else {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
//创建ViewHolder对象 并将ImageView和TextView存在里面
viewHolder = ViewHolder(fruitImage, fruitName)
//将ViewHolder对象存储到View的tag中
view.tag = viewHolder
}
//获取当前Fruit对象
val fruit = getItem(position)
if (fruit != null) {
//给列表项填充数据
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
当convertView为null时,我们新建一个ViewHolder对象,并将ImageView和TextView控件的实例存放在ViewHolder中,然后调用View的setTag()方法将ViewHolder对象存储在View的tag中。当convertView不为null时,则直接调View的getTag()方法将ViewHolder从View中取出。
3.4 ListView的点击事件
我们可以通过setOnItemClickListener()方法设置列表项点击事件的监听器。当用户点击列表项时,该监听器会回调Lambda表达式中的内容。setOnItemClickListener()方法接收四个参数:
- parent:表示列表视图(ListView) 本身。
- view:表示被点击的列表项视图。
- position:表示被点击的列表项在列表中的位置(从0开始)。
- id:表示被点击的列表项的行ID。
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
my_listView.adapter = adapter
//设置ListView点击监听器
my_listView.setOnItemClickListener { parent, view, position, id ->
val fruit = fruitList[position]
Toast.makeText(this, "You clicked item :${fruit.name}!", Toast.LENGTH_SHORT).show()
}
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple))
· · · ·
}
}
}
这次当我们点击ListView中任意列表项时,会通过Toast弹窗显示出你所点击的水果名称。下面是运行效果图:
在ListView.setOnItemClickListener { parent, view, position, id -> ···}中我们接收了四个参数,但是实际上我们只用到了position,剩下三个并没有使用。我们在本章AlertDialog这一小节里说过的:
Kotlin允许我们将lambda表达式中没有用到的参数使用下划线来替代。
因此可以更改为下面这种写法:
//设置ListView点击监听器
my_listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, "You clicked item :${fruit.name}!", Toast.LENGTH_SHORT).show()
}
需要注意的是,虽然我们将不用的三个参数用下划线来代替了,但是他们之间的位置是不能改变的。原来position是在第三参数位,你就不可以将其更改为其他参数位。
四、更强大的滚动控件RecyclerView
ListView只能实现数据的垂直方向滚动效果,如果我们想实现数据的横向滚动,ListView是做不到的。RecyclerView的功能要比ListView更强也更简单,下面就让我们来学习一下吧。
4.1 RecyclerView的基本用法
若要使用RecyclerView,我们需要在app目录下的build.gradle文件的dependencies {· · ·}中添加依赖:
dependencies {
· · ·
implementation 'androidx.recyclerview:recyclerview:1.0.0'
· · ·
}
在activity_main.xml中引入RecyclerView控件:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
//RecyclerView控件
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/my_recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
现在,我们已经引入RecyclerView控件,接下来要做的就是为RecyclerView创建适配器,通过适配器为RecyclerView填充数据:
// RecyclerView的适配器
继承 泛型
class FruitAdapter(val fruitList: ArrayList<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
//内部类ViewHolder用于缓存列表项控件 view通常是RV列表项的最外层布局
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对象 返回ViewHolder类型对象
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
//RecyclerView列表子项对象
列表子项布局 添加父布局 标准写法
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
//重写父类方法 用于对RecyclerView列表项进行赋值(会在每个列表项滚动到屏幕内的时候执行)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
//重写父类方法 用于告诉RecyclerView一共有多少列表项
override fun getItemCount(): Int {
//返回fruitList的长度
return fruitList.size
}
}
到这里我们就已经成功创建好RecyclerView的适配器了,接下来就开始使用RecyclerView吧!修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化水果数据
initFruitList()
//创建一个线性布局管理器
val myLayoutManager = LinearLayoutManager(this)
//通过setLayoutManager()指定RecyclerView的布局方式为线性布局(类似于ListView)
my_recyclerView.layoutManager = myLayoutManager
//创建RecyclerView适配器
val myAdapter = FruitAdapter(fruitList)
//将适配器添加到RecyclerView上
my_recyclerView.adapter = myAdapter
}
/**
* 初始化水果列表数据
*/
private fun initFruitList() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple))
· · ·
fruitList.add(Fruit("Mango", R.drawable.mango))
}
}
}
布局管理器(LayoutManager)通常用于定义组件在容器中的布局方式。
在上面这段代码中我们首先初始化水果列表数据,然后创建LinearLayoutManager布局管理器,这表明我们希望这个布局是按照线性方式展现的。然后我们通过 setLayoutManager()方法将线性布局管理器设置给RecyclerView,这样RecyclerView就可以实现类似于ListView列表的线性效果了。最后我们创建一个FruitAdapter对象作为RecyclerView的适配器,并将其添加到RecyclerView,这样就完成啦~
运行效果如下,和我们之前使用ListView的效果一模一样,不同是我们这次是通过RecyclerView来实现的!
4.2 RecyclerView的横向滚动
在学习了RecyclerView的简单用法后,我们该学习点儿更高级的效果了。我们已经知道ListView只能实现纵向滚动的效果,那么如何实现横向滚动效果呢?我们可以通过RecyclerView轻松做到这一点!
首先我们需要对fruit_list进行调整,因为之前我们列表项是水平排列的,适用于纵向滚动场景。如果我们想实现横向滚动场景,那么我们需要将列表项改成垂直排列:
<?xml version="1.0" encoding="utf-8"?>
<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_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中,我们给线性布局管理器设置组件排列方向。默认是纵向排列的,也就是VERTICAL。我们给它设置成横向排列,这样RecyclerView就可以横向滚动了:
当使用LinearLayoutManager作为RecyclerView的布局管理器时,orientation属性可以指定RecyclerView的滚动方向。
如果设置成VERTICAL,则RecyclerView会垂直滚动。
如果设置成HORIZONTAL,则RecyclerView会水平滚动。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruitList()
val myLayoutManager = LinearLayoutManager(this)
//通过setOrientation()方法设置布局的排列方向和滚动是水平方向的
myLayoutManager.orientation = LinearLayoutManager.HORIZONTAL
my_recyclerView.layoutManager = myLayoutManager
val myAdapter = FruitAdapter(fruitList)
my_recyclerView.adapter = myAdapter
}
这样我们就实现了RecyclerView的横向滚动,效果如下。
4.3 RecyclerView的网格布局
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruitList()
val myLayoutManager = GridLayoutManager(this, 2) // 设置网格布局,每行2列
my_recyclerView.layoutManager = myLayoutManager
val myAdapter = FruitAdapter(fruitList)
my_recyclerView.adapter = myAdapter
}
在上述代码中,我们使用了GridLayoutManager来设置RecyclerView的网格布局,其中第一个参数是Context上下文,第二个参数是每行显示的列数。
4.4 RecyclerView的瀑布流布局
为了显示瀑布流布局,我们需要将fruit_item.xml小小的修改一下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="80dp"
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="center_horizontal"
android:layout_marginTop="10dp"
android:gravity="left" />
</LinearLayout>
然后我们需要在MainActivity中将之前声明的“线性布局管理器LinearLayoutManager”修改为“瀑布流布局管理器StaggeredGridLayoutManager”:
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化水果数据
initFruitList()
//创建瀑布流式布局管理器
val myLayoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
//将瀑布流式布局管理器设置到RecyclerView上
my_recyclerView.layoutManager = myLayoutManager
val myAdapter = FruitAdapter(fruitList)
my_recyclerView.adapter = myAdapter
}
/**
* 初始化水果列表数据
*/
private fun initFruitList() {
repeat(2) {
fruitList.add(Fruit(getRandomLength("Apple"), R.drawable.apple))
fruitList.add(Fruit(getRandomLength("Banana"), R.drawable.banana))
fruitList.add(Fruit(getRandomLength("Orange"), R.drawable.orange))
fruitList.add(Fruit(getRandomLength("Watermelon"), R.drawable.watermelon))
fruitList.add(Fruit(getRandomLength("Pear"), R.drawable.pear))
fruitList.add(Fruit(getRandomLength("Grape"), R.drawable.grape))
fruitList.add(Fruit(getRandomLength("Pineapple"), R.drawable.pineapple))
fruitList.add(Fruit(getRandomLength("Strawberry"), R.drawable.strawberry))
fruitList.add(Fruit(getRandomLength("Cherry"), R.drawable.cherry))
fruitList.add(Fruit(getRandomLength("Mango"), R.drawable.mango))
}
}
/**
* 用于随机生成水果名字长度
*/
private fun getRandomLength(str: String): String {
val n = (1..20).random()
val builder = StringBuilder()
repeat(n) {
builder.append(str)
}
return builder.toString()
}
}
这里需要讲一下 StaggeredGridLayoutManager(spanCount,orientation) 的用法。
它的第一个参数:spanCount表示的是 行数或列数 ,具体取决于排列方向参数orientation。
- 如果orientation为HORIZONTAL,那么spanCount表示的是行数。
- 如果orientation为VERTICAL,那么spanCount表示的是列数。
以下是orientation参数为StaggeredGridLayoutManager.VERTICAL的效果图:
以下是orientation参数为StaggeredGridLayoutManager.HORIZONTAL的效果图:
4.5 RecyclerView的点击事件
在这里我们的目标是实现RecyclerView中子项的点击事件,我们可以将点击事件的处理放到onBindViewHolder的代码块中,这样在每次数据项发生变化时都会设置点击事件。
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
//内部类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)
}
//对RecyclerView列表项进行赋值(在每个列表项滚动到屏幕内的时候执行)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
// 设置文本内容点击事件
holder.itemView.setOnClickListener {
Toast.makeText(holder.itemView.context,"你点击的是 ${fruit.name}的文本内容!",Toast.LENGTH_SHORT).show()
}
// 设置水果图片点击事件
holder.fruitImage.setOnClickListener {
Toast.makeText(holder.itemView.context,"你点击的是 ${fruit.name}的图片!",Toast.LENGTH_SHORT).show()
}
}
override fun getItemCount(): Int {
return fruitList.size
}
}
点击水果图片效果图:
点击水果文本效果图:
其实在ViewHolder的构造函数中设置点击事件也是可以实现同样效果的,但是这样做可能会导致性能问题。因为每次ViewHolder被重新绑定时,都会创建一个新的点击事件监听器。也可能会导致一些异常,因为adapterPosition和fruitList[position]这两个变量在ViewHolder的构造函数中可能还没有被正确地初始化。而在onBindViewHolder方法中设置点击事件监听器,只会在数据项发生变化时才会为新的数据项设置点击事件监听器。
class FruitAdapter(val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
//内部类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)
//ViewHolder主构造函数的初始化
init {
//列表项点击事件
itemView.setOnClickListener {
val position = adapterPosition
val fruit = fruitList[position]
Toast.makeText(itemView.context,"You clicked view ${fruit.name}",Toast.LENGTH_SHORT).show()
}
//水果图片点击事件
fruitImage.setOnClickListener {
val fruit = fruitList[position]
Toast.makeText(itemView.context,"You clicked image ${fruit.name}",Toast.LENGTH_SHORT).show()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
· · ·
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
· · ·
}
override fun getItemCount(): Int {
· · ·
}
}