Android入门 | 控件与布局

1.常用控件的使用方法

Android 给我们提供了大量的 UI 控件,合理地使用这些控件可以非常轻松的编写出相当不错的界面,下面我们就挑选集中常用的控件,详细介绍一下它们的使用方法。

1.1 TextView

TextView 用于再界面上显示一段文本信息,接下来我们看看 TextView 的更多用法。

<?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">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is TextView" />

</LinearLayout>


除此之外我们还可以为宽和高设置固定大小,单位是dp,这是一种与屏幕密度无关的尺寸单位,可以保证在不同分辨率的手机上显示的效果尽可能的一致。

由于 TextView 默认都是居左上角对齐的,但是由于文字的内容不够长,所以从效果上看不出来,我们可以添加如下代码设置 TextView 的对齐方式。

android:gravity="center"

gravity 一共有 topbottomstartendcenter 等可选属性,并且可以使用 |进行分割从而设置多个属性,其中 center 的效果等同于 center_vertical|center_horizontal,表示文字在垂直和水平方向上都居中对齐。

除此之外我们还可以对 TextView 的字体颜色和大小进行设置,

<TextView
    android:id="@+id/textView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="This is TextView"
    android:textSize="24sp"
    android:textColor="#0f0" />

textSize 属性用于设置文字大小,它的单位是 sp,这样当用户修改了系统中文字的大小时该文字也会发生变化。 textColor 用于设置文字的颜色。

当然 TextView 还有很多其它的属性,在这里就不一一介绍了。

1.2 Button

Button 是程序用于和用户进行交互的一个重要控件,它可配置的属性和 TextView 差不多。

在 xml 中加入 Button

<Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Button" />

在 xml 中加入 Button 后的界面效果


在 Android 系统中会默认将按钮上的英文字母全部转换为大写,如果不想要这样可以为按钮添加android:textAllCaps="false" 属性进行设置。

除此之外,我们还可以为按钮添加监听器

  • 利用 Java 单抽象方法接口特性

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            button.setOnClickListener({
                // 在此处添加逻辑
            })
        }
    }
    
  • 使用实现接口的方式注册监听器

    class MainActivity : AppCompatActivity(), View.OnClickListener {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            button.setOnClickListener(this)
        }
    
        override fun onClick(v: View?) {
            when (v?.id) {
                R.id.button -> {
                    // 在此处添加逻辑
                }
            }
        }
    }
    

1.3 EditText

EditText 是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。EditText 的应用场景应该算是非常普遍了,发短信、发微博、聊QQ等等,在进行这些操作时,你不得不使用到 EditText

在 xml 中加入 EditText

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

除此之外 Android 系统内置了一个十分常用的功能,就是当用户未输入时在输入框中有一行提示文字,当用户输入后提示文字就没了,要想实现这个功能只需要在控件中加入android:hint="XXX",其中 XXX 就是提示的文本,下面我们来尝试一下。

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Type something here" />

不过随着输入内容的不断增多,EditText 会被不断拉长,这是由于 EditText 的高度是 wrap_content。因此它总能包含住里面的内容,但是当输入内容过多时,界面就会变得非常难看,在此我们可以使用 android:maxLines 属性来解决这个问题。

修改 xml 文件

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:maxLines="2"
    android:hint="Type something here" />

这样设置之后无论如何都只会在 EditText 中显示两行文字

使用 EditText + Button 获取输入的内容

class MainActivity : AppCompatActivity(), View.OnClickListener {

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

    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.button -> {
                // 获取 editText 中的文本内容并转换为字符串
                val str = editText.text.toString()
                // 将获取到的内容使用 Toast 显示出来
                Toast.makeText(this, str, Toast.LENGTH_SHORT).show()
            }
        }
    }
}

1.4 ImageView

ImageView 是用于在界面上展示图片的一个控件,它可以让我们的程序界面变得更加丰富多彩。

图片通常是放在以 drawable 开头的目录下的,并且要带上具体的分辨率。现在最主流的手机屏幕分辨率大多是 xxhdpi 的,所以我们在 res 目录下创建一个 drawable-xxhdpi 目录,然后将图片放到该目录下。

修改 xml 文件

<ImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/img_1" />

引入图片通过 android:src 属性。

点击按钮实现图片的切换。

override fun onClick(v: View?) {
    when (v?.id) {
        R.id.button -> {
            // 点击按钮实现图片的切换
            imageView.setImageResource(R.drawable.img_2)
        }
    }
}

1.5 ProgressBar

ProgressBar 用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。

使用 ProgressBar

<ProgressBar
    android:id="@+id/progressBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />


旋转的进度条可以表示系统正在加载数据,那么加载完之后就需要将进度条隐藏,那么可以通过 android:visibility 属性进行设置,该属性有三个值 分别是:visibleinvisiblegone。其中 visible 表示可见(默认值),invisible 表示不可见,但是它仍然占据着原来的位置和大小。 gone 表示不可见,而且不占据原来的位置和空间。

除了使用 xml 设置之外,也可以使用 setVisibility() 方法,传入 View.VISIBLEView.INVISIBLEView.GONE 这3种值。

实现点击按钮显示与隐藏进度条

override fun onClick(v: View?) {
    when (v?.id) {
        R.id.button -> {
            if (progressBar.visibility == View.VISIBLE) {
                progressBar.visibility = View.GONE
            } else {
                progressBar.visibility = View.VISIBLE
            }
        }
    }
}

另外,还可以为进度条设置不同的样式,刚刚是圆形的进度条,通过 style 属性可以将它改为水平的进度条

修改 xml 文件

<ProgressBar
    android:id="@+id/progressBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" 
    style="?android:attr/progressBarStyleHorizontal"
    android:max="100"
    />

通过以上代码将进度条设置为水平之后还将进度条的最大进度设置为了100.

通过点击按钮增加进度条的进度。

override fun onClick(v: View?) {
    when (v?.id) {
        R.id.button -> {
            progressBar.progress = progressBar.progress + 10;
        }
    }
}

1.6 AlertDialog

AlertDialog 可以在当前界面弹出一个对话框,这个对话框时顶置于所有界面元素之上的,能够屏蔽其他控件的交互能力,因此 AlertDialog 一般用于提示一些重要的内容或者警告等信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框。

override fun onClick(v: View?) {
    when (v?.id) {
        R.id.button -> {
            AlertDialog.Builder(this).apply {
                setTitle("This is Dialog")
                setMessage("Something important")
                setCancelable(false)
                setPositiveButton("OK") {
                    dialog, which ->
                }
                setNegativeButton("Cancel") {
                    dialog, which ->
                }
                show()
            }
        }
    }
}

这里首先通过 AlertDialog.Builder 构建了一个对话框,然后通过 Kotlin 中的 apply() 函数,在 apply() 函数中为这个对话框设置了标题、内容、是否使用 Back 键取消以及对话框中的确认和取消按钮,最后调用 show() 方法进行显示。

2.详解 3 种基本布局

一个丰富的界面是由很多个控件组成的,那么我们如何才能让各个控件都有条不紊的摆放在界面上,而不是乱糟糟的呢?这就需要借助布局来实现了,布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面,除了在布局中放置控件外,还可以在布局内嵌套布局。

它们的关系如下图所示:

2.1 LinearLayout

LinearLayout 又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个布局会将它所包含的控件在线性的方向上依次排列。

2.1.1 为线性布局指定方向

  • 纵向排列 vertical
    <?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"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 1" />
    
        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 2" />
    
        <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 3" />
    </LinearLayout>
    
  • 横向排列 horizontal
    <?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"
        android:orientation="horizontal"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 1" />
    
        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 2" />
    
        <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button 3" />
    </LinearLayout>
    


横向排列与纵向排列的切换只需要改变 orientation 即可,如果不指定 orientation 属性,默认值是 horizontal

注意:当指定布局排列方式为 horizontal 时,控件的宽度不能设置为 match_parent,否则这个控件就会占满屏幕,其它控件都无法显示了。vertical 也是同理。

2.1.2 layout_gravity

该属性与前面用到的 gravity 属性看起来有些相似,gravity 用于指定文字在控件中的对其方式,而 layout_gravity 用于指定控件在布局中的对齐方式。这两个属性的可选参数差不多,但是需要注意:只有当线性布局的方向为 horizontal 时,垂直方向的对齐方式才会生效,同理,线性布局的方式为 vertical 时,水平方向的对齐方式才会生效。

<?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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:text="Button 1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button 2" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Button 3" />
</LinearLayout>

运行效果:


2.1.3 layout_weight

该属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。

例如:编写一个消息发送界面,需要一个文本编辑框和一个发送按钮

<?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"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <EditText
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:hint="Type something"
        android:inputType="text" />

    <Button
        android:id="@+id/send"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAllCaps="false"
        android:text="send" />
</LinearLayout>

这里将这两个控件的宽度都设置为 0dp,通常这样控件就无法显示出来了,但是在后面加上 android:layout_weight="1" 代表每个控件各占 1 份宽度,所以会出现如下效果:

该属性的原理是将 LinearLayout 中所有 layout_weight 的值相加,然后再通过指定的比例进行分配。

2.2 RelativeLayout

RelativeLayout 被称为相对布局,它和 LinearLayout的排列规则不同,RelativeLayout 显得更加随意,它可以通过相对定位的方式让控件出现在布局的任何位置。

2.2.1 使用相对布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:text="Button 1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:text="Button 2" />

    <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/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true"
        android:text="Button 4" />

    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:text="Button 5" />
</RelativeLayout>

运行效果:


以上代码的的含义分别是,让控件位于父容器的左侧、右侧、上面、下面和在父容器中居中,这点通过属性的名称即可得知。

上面的例子使用的是以父容器为参照,接下来演示一下以控件为参照:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">

    <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>

运行效果:


这次设置的属性都要以参照控件的 id 为属性值,这里的 layout_above 是让该控件位于目标控件的上方,layout_below 是让控件位于目标控件的下方。layout_toLeftOflayout_toRightOf 分别是让控件位于目标控件的左侧和右侧。

注意:在使用该方式的时候建议将目标控件写在参照它的控件的上面,否则可能会出现找不到 id 的情况。

除此之外再介绍几个属性:

  • layout_alignLeft:该属性是指将该控件的左边缘与参照控件的左边缘对齐
  • layout_alignRight:该属性是指将该控件的右边缘与参照控件的右边缘对齐
  • layout_alignTop:该属性是指将该控件的上边缘与参照控件的上边缘对齐
  • layout_alignBottom:该属性是指将该控件的下边缘与参照控件的下边缘对齐

2.3 FrameLayout

FrameLayout 又称作帧布局,它相比于前面两种布局就简单多了,因此它的应用场景就少了很多,这种布局没有丰富的定位方式,所有控件都会默认摆放在布局的左上角。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is TextView" />

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button 1" />
</FrameLayout>

运行效果:


从运行结果可以看出,文字和按钮都位于布局的左上角。由于 Button 是在 TextView 之后添加的,因此压在了文字的上面(先添加也在文字上面)。

除了默认样式之外,我们还可以为控件添加对齐方式:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Button 1" />

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:text="This is TextView" />
</FrameLayout>

通过指定对齐方式后可以实现按钮位于屏幕右侧,文字在左侧的效果。

3.自定义控件

3.1 引入布局

经过前的学习,相信创建一个标题栏已经不是什么困难的事情了,只需要加入两个 Button 和 一个 TextView 即可。但是很多页面都需要用到标题栏,如果每个页面都重新写一个标题栏就会造成代码大量重复,这时可以通过引入布局来解决这个问题。

在 layout 目录下创建 title.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="wrap_content"
    android:background="@drawable/title_bg"
    android:orientation="horizontal">

    <Button
        android:id="@+id/titleBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        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="match_parent"
        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_margin="5dp"
        android:background="@drawable/edit_bg"
        android:text="Back"
        android:textColor="#fff"/>

</LinearLayout>

引入布局

<?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">

    <include layout="@layout/title" />
</androidx.constraintlayout.widget.ConstraintLayout>

修改 MainActivity

package com.example.uicustomviews

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 将系统标题栏隐藏
        supportActionBar?.hide()
    }
}

运行结果:


这样,便可以达到 xml 的复用。

3.2 创建自定义控件

引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个 Activity 中为这些控件单独编写一次事件注册的代码。比如标题栏中的返回按钮,其实不管是在哪一个 Activity 中,这个按钮的功能都是相同的,即销毁在当前 Activity。而如果在每个 Activity 中都需要重新注册一遍返回按钮的点击事件,无疑会增加很多的重复代码,所以这种情况最好使用自定义控件的方式来解决。

新建 TitleLayout 继承自 LinearLayout,让它称为我们自定义控件的标题栏控件

package com.example.uicustomviews

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout

class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

    init {
        LayoutInflater.from(context).inflate(R.layout.title, this);
    }
}

这里我们在 TitleLayout 的主构造函数中声明了 ContextAttributeSet 这两个参数,在布局中引入 TitleLayout 控件时就会调用这个构造函数。然后在 init 结构体中需要对标题栏布局进行动态加载,这就要借助 LayoutInflater 来完成了。通过 LayoutInflaterfrom() 方法可以构建出一个 LayoutInflater对象,然后调用 inflate() 方法就可以动态加载一个布局文件。inflate() 方法接收两个参数,一个是布局文件,另一个是父布局。

修改 activity_main.xml 代码

<?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">

    <com.example.uicustomviews.TitleLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

注意:在这里控件名要指定全限定类名。

运行效果:

3.2.1 给标题栏中的按钮注册点击事件

修改 TitleLayout 代码

package com.example.uicustomviews

import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.title.view.*

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,当点击编辑按钮时,提示一段文本。

注意

  • TitleLayout 中接收的 context 参数实际上是一个 Activity 的实例,在返回按钮的点击事件里,我们要先将它转换成 Activity 类型,然后再调用 finish() 方法销毁当前 Activity。
  • Kotlin 中的强制类型转换使用的关键字是 as

这样,当某个布局文件引入 TitleLayout 时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这样可以省去很多编写重复代码的工作。

4.ListView

ListView 在过去绝对可以称得上是 Android 中最常用的控件之一,几乎所有的应用程序都会用到它。由于手机屏幕空间比较有限,所以为了可以显示更多的内容就会用到 ListViewListView 可以通过手指滑动将屏幕外的数据滚动到屏幕内。

4.1 ListView 的简单用法

修改 activity_main.xml 文件

<?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">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

修改 MainActivity

package com.example.listviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val data = listOf("Apple", "Banana", "Orange", "Watermelon",
            "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
            "Apple", "Banana", "Orange", "Watermelon",
            "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, data)
        listView.adapter = adapter
    }
}

在这里首先将要显示的数据准备好,然后创建一个 ArrayAdapter 并将它与 ListView 绑定,其中 ArrayAdapter 第一个参数是指上下文对象,这里需要在 MainActivity 中显示,所以输入 this,然后设置 ListView 子项布局的 id,这里选择 Android 系统内置的布局文件,里面只有一个 TextView,可用于简单地显示一段文本。

运行结果:

4.2 定制 ListView 界面

创建 Fruit 类,作为 ListView 的适配器类型

class Fruit(val name: String, val imageId: Int) {
}

Fruit 类只有两个字段,name 是水果的名字,imageId 表示水果对应的图片资源的 id。

编写 ListView 的布局,命名为 fruit_item.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="60dp"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/apple_pic"
        android:layout_gravity="center"
        android:layout_marginLeft="10dp" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="48dp"
        android:text="苹果"
        android:gravity="center_vertical"
        android:layout_marginLeft="10dp" />
</LinearLayout>

创建自定义适配器

package com.example.listviewtest

import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView

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)   // 获取当前项的 fruit 实例

        if (fruit != null) {
            fruitImage.setImageResource(fruit.imageId)
            fruitName.text = fruit.name
        }
        return view
    }
}

FruitAdapter 定义了一个主构造函数,用于将 Activity 的实例、ListView 子项布局和数据源传进来。另外又重写了 getView() 方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。

在方法内部,首先加载布局文件,然后从加载到的布局文件中获取 ImageViewTextView,接着再通过当前项的 position 获取 Fruit 实例。并将图片和文字设置上,最后返回布局。

注意kotlin-android-extensions 插件在 ListView 的适配器中无法工作,所以需要使用 findViewById() 方法获取控件。kotlin-android-extensions 插件主要应用于 Activity 和 Fragment 中。

修改 MainActivity 代码

package com.example.listviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

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)
        listView.adapter = adapter
    }

    private fun initFruits() {
        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))
        }
    }
}

运行效果:

4.3 提升 ListView 的运行效率

前面的代码虽然可以实现出基本的效果,但是 FruitAdapter 中的 getView 方法每次用户滑动屏幕时都会被调用,每次调用都会重新加载一次布局,所以快速滑动的话很可能造成卡顿。

优化 FruitAdapter

package com.example.listviewtest

import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView

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) {
            view = LayoutInflater.from(context).inflate(resourceId, parent, false);
        } else {
            view = convertView
        }
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
        val fruit = getItem(position)   // 获取当前项的 fruit 实例

        if (fruit != null) {
            fruitImage.setImageResource(fruit.imageId)
            fruitName.text = fruit.name
        }
        return view
    }
}

在这里,首先对 convertView 进行判断,如果为空的话才进行布局的加载,否则直接使用 convertView,这样可以有效的减少布局加载的次数。

以上方式虽然可以减少布局加载次数,但是却无法避免重复使用 findViewById() 方法。

减少控件获取优化

package com.example.listviewtest

import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView

class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
    ArrayAdapter<Fruit>(activity, resourceId, data) {

    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 = LayoutInflater.from(context).inflate(resourceId, parent, false);
            val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
            val fruitName: TextView = view.findViewById(R.id.fruitName)
            // 将加载完成的图片与文字保存到 ViewHolder 中
            viewHolder = ViewHolder(fruitImage, fruitName)
            view.tag = viewHolder
        } else {
            view = convertView
            viewHolder = view.tag as ViewHolder
        }
        val fruit = getItem(position)   // 获取当前项的 fruit 实例

        if (fruit != null) {
            viewHolder.fruitImage.setImageResource(fruit.imageId)
            viewHolder.fruitName.text = fruit.name
        }
        return view
    }
}

这里使用内部类 ViewHolder,用于对 ImageViewTextView 进行缓存。如果 convertView 为空时,创建一个 ViewHolder 对象,将控件实例保存到 ViewHolder 中,然后调用 view.setTag 方法将 viewHolder 对象存储到 View 中。如果 convertView 不为空,则调用 view.getTag() 方法,把 ViewHolder 重新取出。这样就避免了多次获取控件实例。

4.3 ListView 的点击事件

修改 MainActivity 代码

package com.example.listviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

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)
        listView.adapter = adapter
        // 为 listView 的 item 绑定监听器
        listView.setOnItemClickListener { parent, view, position, id ->
            val fruit = fruitList[position]
            Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
        }
    }

    private fun initFruits() {
        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))
        }
    }
}

5.RecyclerView

ListView 虽然可以实现我们想要实现的效果,但是如果不做一些优化,性能将是一个问题,另外 ListView 也只能实现纵向滚动,如果我们想实现横向滑动的话 ListView 是无法做到的,但是 RecyclerView 却可以做到。

5.1 RecyclerView 的基本用法

RecyclerView 和前面所学的控件不同,它属于新增控件,需要在 build.gradle 中引入一下。

引入 RecyclerView

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

注意:添加完之后点击一下 Sync Now。

修改 activity_main.xml

<?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>

创建 FruitAdapter

package com.example.recyclerviewtest

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class FruitAdapter(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)
    }

    /**
     * 返回 item 的总条数
     */
    override fun getItemCount(): Int = fruitList.size

    /**
     * 将控件与实体进行绑定
     */
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.fruitImage)
        holder.fruitName.text = fruit.fruitName
    }

}

在这里首先定义了一个内部类 ViewHolder,它继承自 RecyclerView.ViewHolder。然后 ViewHolder 的主构造函数中要传入一个 View 参数,这个参数通常就是 RecyclerView 子项的最外层布局,那么我们就可以通过 findViewById() 方法获取布局中的控件了。

FruitAdapter 中也有一个主构造函数,它用于把要展示的数据源传进来。

修改 MainActivity

package com.example.recyclerviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 初始化数据
        initFruit()
        // 指定布局方式
        val layoutManager = LinearLayoutManager(this)
        // 将布局方式设置到 recyclerView 中
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        // 绑定适配器
        recyclerView.adapter = adapter
    }

    private fun initFruit() {
        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))
        }
    }
}

运行效果:

5.2 实现横向滚动和瀑布流

5.2.1 实现横向滚动

修改 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:orientation="vertical">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"
        android:src="@drawable/apple_pic" />

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:layout_gravity="center_horizontal"
        android:text="苹果" />
</LinearLayout>

修改 MainActivity 的代码

package com.example.recyclerviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 初始化数据
        initFruit()
        val layoutManager = LinearLayoutManager(this)
        // 指定布局方式
        layoutManager.orientation = LinearLayoutManager.HORIZONTAL
        // 将布局方式设置到 recyclerView 中
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        // 绑定适配器
        recyclerView.adapter = adapter
    }

    private fun initFruit() {
        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))
        }
    }
}

为什么 RecyclerView 可以轻易实现 ListView 无法实现的效果呢?这时因为 ListView 的布局排列是由自身去管理,而 RecyclerView 则将工作交给了 LayoutManager。LayoutManager 制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同的排列方式的布局了。

除了 LinearLayoutManager 之外,RecyclerView 还提供了 GridLayoutManager 和 StaggeredGridLayoutManager 这两种内置的布局排列方式。GridLayoutManager 用于实现网格布局,StaggeredGridLayoutManager 用于实现瀑布流布局。

运行结果:

5.2.2 实现瀑布流

修改 fruit_item.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="wrap_content"
    android:layout_margin="5dp"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="center_horizontal"
        android:src="@drawable/apple_pic" />

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

修改 MainActivity

package com.example.recyclerviewtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 初始化数据
        initFruit()
        // 指定布局方式
        val layoutManager = StaggeredGridLayoutManager(3,
                RecyclerView.VERTICAL)
        // 将布局方式设置到 recyclerView 中
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        // 绑定适配器
        recyclerView.adapter = adapter
    }

    private fun initFruit() {
        repeat(2) {
            fruitList.add(Fruit(getRanLenStr("Apple"), R.drawable.apple_pic))
            fruitList.add(Fruit(getRanLenStr("Banana"), R.drawable.banana_pic))
            fruitList.add(Fruit(getRanLenStr("Orange"), R.drawable.orange_pic))
            fruitList.add(Fruit(getRanLenStr("Watermelon"), R.drawable.watermelon_pic))
            fruitList.add(Fruit(getRanLenStr("Pear"), R.drawable.pear_pic))
            fruitList.add(Fruit(getRanLenStr("Grape"), R.drawable.grape_pic))
            fruitList.add(Fruit(getRanLenStr("Pineapple"), R.drawable.pineapple_pic))
            fruitList.add(Fruit(getRanLenStr("Strawberry"), R.drawable.strawberry_pic))
            fruitList.add(Fruit(getRanLenStr("Cherry"), R.drawable.cherry_pic))
            fruitList.add(Fruit(getRanLenStr("Mango"), R.drawable.mango_pic))
        }
    }

    /**
     * 随机生成水果名称的长度
     */
    private fun getRanLenStr(str: String) : String {
        val n = (1..20).random()
        val builder = StringBuilder()
        repeat(n) {
            builder.append(str)
        }
        return builder.toString()
    }
}

这里需要修改 layoutManager 的方式,将它指定为 StaggeredGridLayoutManager,并设置要显示的行(列)和显示方向。

运行效果:


5.3 RecyclerView 点击事件

和 ListView 一样,RecyclerView 也必须能响应点击事件才行,但是 RecyclerView 中并没有提供 setOnItemClickListener() 方法来注册监听器,而是需要我们自己给子项具体的 View 去注册点击事件。
表面上看好像 RecyclerView 的设计不如 ListView,但是由于 setOnItemClickListener() 方法注册的是子项的点击事件,如果项点击的是子项里具体的某个按钮的话,RecyclerView 就方便很多。

修改 FruitAdapter

package com.example.recyclerviewtest

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView

class FruitAdapter(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.fruitName}",
                Toast.LENGTH_SHORT
            ).show()
        }
        viewHolder.fruitImage.setOnClickListener {
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(
                parent.context, "you clicked image ${fruit.fruitName}",
                Toast.LENGTH_SHORT
            ).show()
        }
        return viewHolder
    }

    /**
     * 返回 item 的总条数
     */
    override fun getItemCount(): Int = fruitList.size

    /**
     * 将控件与实体进行绑定
     */
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.fruitImage.setImageResource(fruit.fruitImage)
        holder.fruitName.text = fruit.fruitName
    }
}

6.Kotlin 课堂:延迟初始化和密封类

前面学习了 Kotlin 语言的许多特性,包括变量不可变,变量不可为空等等。这些特定都是为了尽可能地保证程序安全而设计的,但是有些时候这些特性也会在编码时给我们带来不少的麻烦。

比如,如果你的类中存在很多全局变量实例,为了保证它们能够满足 Kotlin 的空指针检查语法标准,你不得不做许多的非空判断保护才行,即使你非常确定它们不会为空。

6.1 使用延迟初始化

private var adapter: MsgAdapter? = null

override fun onCreate(savedInstanceState: Bundle?) {
    adapter = MsgAdapter(msgList)
    recyclerView.adapter = adapter
}

override fun onClick(v: View?) {
    adapter?.notifyItemInserted(msgList)
}

在以上例子中,即使我们知道了 adapter 一定会被初始化,但是也必须在使用 adapter 时进行判空处理,否则编译肯定无法通过。

而当全局的实例越来越多时,这个问题也会变得越来越明显,到时候必须要写大量的判空处理代码,而这只是为了满足 Kotlin 编译器的要求。

使用 lateinit 关键字

lateinit 关键字可以告诉 Kotlin 编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它负值为 null 了。

private lateinit var adapter: MsgAdapter

override fun onCreate(savedInstanceState: Bundle?) {
    adapter = MsgAdapter(msgList)
    recyclerView.adapter = adapter
}

override fun onClick(v: View?) {
    adapter.notifyItemInserted(msgList)
}

可以看到,我们在 adapter 变量的前面加上了 lateinit 关键字,这样就不用再一开始的时候将它赋值为 null 了。

当然,使用 lateinit 关键字也不是没有任何风险的,如果在 adapter 变量没有初始化的情况下就使用它,那么程序就会崩溃并抛出异常。

注意:如果对全局变量使用了 lateinit 关键字,请一定要确保在使用之前将其初始化。

另外我们还可以通过代码判断一个全局变量是否已经完成了初始化,这样可以避免在某些情况下对某一个变量进行重复初始化

private lateinit var adapter: MsgAdapter

if (!::adapter.isInitialized) {
    adapter = MsgAdapter(msgList)
}

以上代码表示判断 adapter 是否已经初始化,! 同样代表取反操作。

6.2 使用密封类

密封类通常可以结合 RecyclerView 适配器中的 ViewHolder 一起使用,但是密封类的使用场景远不止于此,它可以在很多时候帮助你写出更加规范和安全的代码。

不使用密封类的写法:

interface Result {
    class Success(val msg: String) : Result
    class Failure(val error: Exception) : Result

    fun getResultMsg(result: Result) = when (result) {
        is Success -> result.msg
        is Failure -> result.error.message
        else -> throw IllegalArgumentException()
    }
}

这里的 getResultMsg 方法接收一个 Result 类型参数,如果它属于 Success 就返回成功信息,如果属于 Failure 就返回失败的信息,但是我们还不得不编写一个 else 语句块,否则编译将报错。可是执行结果除了成功就是失败,根本没有第三种情况,这只是为了满足语法检查。

另外,编写 else 条件还有一个潜在的风险。如果增加了一个 Unknown 类并实现了 Result 接口,用于表示未知的结果,可是却又忘记在 getResultMsg 函数中添加相应的分支,这样就可能进入 else 条件从而报错。

使用密封类解决问题

sealed class Result {
    class Success(val msg: String) : Result()
    class Failure(val error: Exception) : Result()

    fun getResultMsg(result: Result) = when (result) {
        is Success -> result.msg
        is Failure -> result.error.message
    }
}

可以看到,只需要将 interface 改成 sealed class 再将返回类型后面添加上 (),这样就可以不写 else 分支,从而减少报错的可能性。

这么写就不报错的原因是因为向 when 语句传入一个密封类做条件时,Kotlin 编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写 else 条件,也不可能出现漏写条件分支的情况。

如果此时再添加一个 Unknown 类,并实现 Result 接口,此时 getResultMsg 方法就一定会报错,必须再添加 Unknown 分支才可以让代码通过编译。

创建 MsgViewHolder

sealed class MsgViewHolder(view: View) : RecyclerView.ViewHolder(view) {

    class LeftViewHolder(view: View) : MsgViewHolder(view) {
        val leftMsg: TextView = view.findViewById(R.id.leftMsg)
    }

    class RightViewHolder(view: View) : MsgViewHolder(view) {
        val rightMsg: TextView = view.findViewById(R.id.rightMsg)
    }
}

修改 MsgAdapter

class MsgAdapter(val msgList: ArrayList<Msg>) : RecyclerView.Adapter<MsgViewHolder>() {

    override fun getItemViewType(position: Int): Int {
        val msg = msgList[position]
        return msg.type
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = if (viewType == Msg.TYPE_RECEIVER) {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item, parent, false)
        MsgViewHolder.LeftViewHolder(view)
    } else {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item, parent, false)
        MsgViewHolder.RightViewHolder(view)
    }

    override fun getItemCount(): Int = msgList.size

    override fun onBindViewHolder(holder: MsgViewHolder, position: Int) {
        val msg = msgList[position]
        when (holder) {
            is MsgViewHolder.LeftViewHolder -> holder.leftMsg.text = msg.content
            is MsgViewHolder.RightViewHolder -> holder.rightMsg.text = msg.content
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272