【8】应用开发——基本布局与列表的使用

提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。

一、常用控件

Android的TextView、Button、EditText、ImageView组件对你来说都太容易啦,此处就不再赘述了。我们只简单讲一下AlertDialog。

1.1 AlertDialog

在 Kotlin 中,setPositiveButtonsetNegativeButton分别用于设置对话框中确认按钮和取消按钮的点击逻辑。
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 {
       · · ·
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值