抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

课本

书籍资源进入官网下载,PC端进入

第四章- 软件也要拼脸蛋,UI开发的点点滴滴

常见控件写法

常见公共属性

控件ID
android:id="@+id/text1"
控件在整个布局中的宽度:match_parent(根据父元素),wrap_content(根据内容),xxdp(类型html的px是一种单位)
android:layout_width="match_parent"
控件在整个布局中的高度:可选项和宽度一样
android:layout_height="wrap_content"
控件本身的垂直对齐方式:可多个值|分隔
android:gravity="center"
控件在整个布局中的垂直对齐方式.若使用线性布局固定了垂直或者水平,那么则只能选择相反方向的值,不然无法生效
android:layout_gravity="center" 
控件的可见属性,可选值:visible:显示控件(默认值),invisible:不可见但任然占用屏幕空间,gone:不可见且不占用屏幕空间
android:visibility="gone"

常用控件-文本(TextView)

在安卓中显示文本使用的控件是TextView.若要使用它,在activity的布局文件中添加<TextView/>标签即可

<!--
控件内容
android:text="你好"
控件的文本颜色
android:textColor="#cc66ff"
控件的文字大小,使用sp单位可随系统大小变化而变化
android:textSize="30sp"
-->
<TextView
    android:id="@+id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:layout_gravity="center"
    android:text="你好"
    android:textColor="#cc66ff"
    android:textSize="30sp"
    />

常用控件-按钮(Button)

在安卓中显示文本使用的控件是Button.若要使用它,在activity的布局文件中添加<Button/>标签即可

<!-- 
文本是否全大写
android:textAllCaps="false"
按钮文本
android:text="按钮" />
-->
<Button
    android:id="@+id/button1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAllCaps="false"
    android:text="按钮" />

使用函数式API注册监听事件

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 关键代码开始
        var buttn1 = findViewById<Button>(R.id.button1)
        buttn1.setOnClickListener{
            Toast.makeText(this,"你点击了按钮",Toast.LENGTH_LONG).show()
        }
        // 关键代码结束
    }
}

使用接口实现监听

// 使Activity也实现View.OnClickListener接口
class MainActivity : AppCompatActivity(), View.OnClickListener{

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var buttn1 = findViewById<Button>(R.id.button1)

        // 这里要将事件丢到MainActivity中,用于给方法onClick传递View
        buttn1.setOnClickListener(this)
    }

    // 这里接受所有触发了Click的View
    override fun onClick(p0: View?) {
      // 先判断View是否为空,然后直接将id传入进行匹配
        when(p0?.id){
          // 这里可以看成p0.id==R.id.button1
            R.id.button1->{
                Toast.makeText(this,"你点击了按钮",Toast.LENGTH_LONG).show()
            }
        }
    }
}

常见控件-可编辑文本框(EditText)

EditText 它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理

<!--
文本框提示文本
android:hint="这是输入框提示文字"
界面最大显示行数,超出部分隐藏
android:maxLines="2"
-->
<EditText
  android:id="@+id/editText"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:hint="这是输入框提示文字"
  android:maxLines="2"
/>

通过点击按钮来获取EditText文本

class MainActivity : AppCompatActivity(), View.OnClickListener{
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var buttn1 = findViewById<Button>(R.id.button1)
        buttn1.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when(p0?.id){
            R.id.button1->{
                // 关键代码开始
                val editText = findViewById<EditText>(R.id.editText)
                val inText = editText.text.toString()
                Toast.makeText(this,inText,Toast.LENGTH_LONG).show()
                // 关键代码结束
            }
        }
    }
}

常见控件-图片(ImageView)

ImageView 是用于在界面上展示图片的一个控件,它可以让我们的程序界面变得更加丰富多彩.图片通常是放在以drawable 开头的目录下的,并且要带上具体的分辨率。现在最主流的手机屏幕分辨率大多是xxhdpi 的,所以我们在res 目录下再新建一个drawable-xxhdpi 目录,然后将事先准备好的两张图片img_1.png 和img_2.png (在随书资源的源码\第4章\UIWidgetTest\app\src\main\res\drawable-xxhdpi目录下)
制到该目录当中。

<!--
图片路径@drawable关键字会根据设备大小自动寻找对应分辨率图片
android:src="@drawable/img_1"
-->
<ImageView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@drawable/img_1"
  />

使用代码更改src

class MainActivity : AppCompatActivity(), View.OnClickListener{
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var buttn1 = findViewById<Button>(R.id.button1)
        buttn1.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when(p0?.id){
            R.id.button1->{
                // 关键代码开始 
                val imgView = findViewById<ImageView>(R.id.image)
                imgView.setImageResource(R.drawable.img_2)
                // 关键代码结束
            }
        }
    }
}

常见控件-进度条(ProgressBar)

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

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

使用代码控制进度条的可见性

class MainActivity : AppCompatActivity(), View.OnClickListener{
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var buttn1 = findViewById<Button>(R.id.button1)
        buttn1.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when(p0?.id){
            R.id.button1->{
                // 关键代码开始
                val progressBar = findViewById<ProgressBar>(R.id.progress_1)
                when(progressBar.visibility){
                    View.VISIBLE-> progressBar.visibility = View.GONE
                    else -> progressBar.visibility = View.VISIBLE
                }
                // 关键代码结束
            }
        }
    }
}

此时,这个并不是进度条而是循环圆圈,我们可以给它加上style来变成进度条

<!--
进度条样式,可选值有很多。
style="?android:attr/progressBarStyleHorizontal"
进度条范围
android:max="100"
-->
<ProgressBar
    android:id="@+id/progress_1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="?android:attr/progressBarStyleHorizontal"
    android:max="100"
    />

使用代码来更改进度,每次点击按钮加10

class MainActivity : AppCompatActivity(), View.OnClickListener{
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var buttn1 = findViewById<Button>(R.id.button1)
        buttn1.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when(p0?.id){
            R.id.button1->{
                // 关键代码开始
                val progressBar = findViewById<ProgressBar>(R.id.progress_1)
                progressBar.progress = progressBar.progress + 10
                // 关键代码结束
            }
        }
    }
}

常见控件-消息弹窗(AlertDialog)

AlertDialog 可以在当前界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够
屏蔽其他控件的交互能力,因此AlertDialog 一般用于提示一些非常重要的内容或者警告信息

class MainActivity : AppCompatActivity(), View.OnClickListener{
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var buttn1 = findViewById<Button>(R.id.button1)
        buttn1.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when(p0?.id){
            R.id.button1->{
                // 关键代码开始
                AlertDialog.Builder(this).apply {
                    setTitle("这是一个弹窗")
                    setMessage("这是弹窗内容")
                    // 是否可以用返回键关闭对话框
                    setCancelable(false)
                    // 确认事件
                    setPositiveButton("确认",{dialog,which->})
                    // 取消事件
                    setNegativeButton("关闭",{dialog,which->})
                }.show()
                // 关键代码结束
            }
        }
    }
}

其它控件

到此为止第一行代码三中的全部控件已经讲解完毕,其他的控件前往安卓官网指南->界面->外观和风格进行了解

基本布局

控件和布局的关系

博客第一行代码学习布局和控件的关系

线性布局-LinearLayout

这个布局会将它所包含的控件在线性方向(垂直或水平)上依次排列
基本结构

<?xml version="1.0" encoding="utf-8"?>
<!--
排列方式vertical:垂直,horizontal:水平
android:orientation="vertical"
xml规则在作为根元素时使用
xmlns:android="http://schemas.android.com/apk/res/android"
-->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >

</LinearLayout>

宽度平分-layout_weight

给在同一水平方向宽度为0dp的控件加上此属性,会将在这一方向添加此属性的控件layout_weight的值总和N,除以每个控件layout_weight的值M,得到单个宽度.若同一方向有设置固定宽度或者wrap_content的控件,则在计算时只会占用剩余的空间
例如下面的layout_weight总和为5,按钮1和按钮3分别占2/5,按钮2占1/5.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >
<!--关键代码开始-->
    <Button
        android:layout_width="0dp"
        android:layout_weight="2"
        android:layout_height="wrap_content"

        android:text="按钮1"
        />
    <Button
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:text="按钮2"
        />
    <Button
        android:layout_width="0dp"
        android:layout_weight="2"
        android:layout_height="wrap_content"
        android:text="按钮3"
        />
<!--关键代码结束-->
</LinearLayout>

相对布局-RelativeLayout

它可以通过相对定位的方式让控件出现在布局的任何位置。也正因为如此,RelativeLayout 中的属性非常多,不过这些属性都是有规律可循的,其实并不难理解和记忆。

根据父标签对齐

下面是一个简单的根据父标签上下左右居中对齐,属性见名知其意就不再赘述

<?xml version="1.0" encoding="utf-8"?>
<!--关键代码开始-->
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="左上角"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="右上角"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="居中"
        android:layout_centerInParent="true"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="左下角"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="右下角"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        />
</RelativeLayout>
<!--关键代码结束-->

博客第一行代码效果相对布局

根据同级标签对齐

下面是一个简单的根据同级标签上下左右居中对齐,属性见名知其意就不再赘述

注意: 被引用的标签一定要在最前面

<?xml version="1.0" encoding="utf-8"?>
<!--关键代码开始-->
<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    >
    <Button
        android:id="@+id/root"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Root"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/root"
        android:layout_toLeftOf="@+id/root"
        android:text="左上角"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/root"
        android:layout_toRightOf="@+id/root"
        android:text="右上角"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/root"
        android:layout_toLeftOf="@+id/root"
        android:text="左下角"
        />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/root"
        android:layout_toRightOf="@+id/root"
        android:text="右上角"
        />
</RelativeLayout>
<!--关键代码结束-->

博客第一行代码相对定位根据同级标签对齐

约束布局-ConstraintLayout

安卓官方文档

由于ConstraintLayout 的特殊性,很难展示如何通过xml进行操作,所以使用可视化编辑器来对界面进行动态操作

要使用constraintLayout布局,先将根标签修改为如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">

</androidx.constraintlayout.widget.ConstraintLayout>

为了更加简单的开发,将不再使用Android Studio的Code模式,将改为Design模式
博客第一行代码切换设计模式

由于无法进行文字描述,请前往哔哩哔哩学习

自定义控件

控件和布局的继承结构
博客第一行代码控件布局和结构的继承

所有布局都是直接或间接继承ViewGroup的.控件其实就是在View 的基础上又添加了各自特有的功能.而ViewGroup则是一种特殊的View ,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器

引入布局

当一个控件在不同的地方被重复调用,当系统自带的控件并不能满足我们的需求时,可以利用上面的继承结构创建自定义控件以实现控件的复用,就类似于Vue的Component.下面我们就来学习一下创建自定义控件的两种简单方法。先将准备工作做好,创建一个UICustomViews 项目,实现自定义标题栏控件.

  1. res目录下创建图片文件夹drawable-xxhdpi 目录,将配套资源中的源码\第4章\UICustomViews\app\src\main\res\drawable-xxhdpi\复制
  2. layout目录下新建一个title.xml布局
    代码如下:
    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"
     >
     <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="返回"
         android:textColor="#fff"/>
     <TextView
         android:id="@+id/textText"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:layout_gravity="center"
         android:gravity="center"
         android: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="编辑"
         android:textColor="#fff"/>
    </LinearLayout>
    
  3. 引用title.xml
    若要引用改文件,则在对应xml文件中键入一下代码
    <include layout="@layout/title"/>
    
    这里以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>
    
  4. 覆盖原有标题栏
    某一些设备可能存在自带的标题栏在对应activity.kt文件中使用如下代码隐藏掉.
    supportActionBar?.hide()
    
    以MainActivity为例
    class MainActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         supportActionBar?.hide()
     }
    }
    
  5. 更改主题
    如果不更改主题可能会出现看不清等意外情况,打开res/values/themes.xml文件.更改parentTheme.AppCompat.Light.DarkActionBar.如下
    <resources xmlns:tools="http://schemas.android.com/tools">
     <!-- Base application theme. -->
     <style name="Theme.UIConstomViews" parent="Theme.AppCompat.Light.DarkActionBar">
         <!-- Primary brand color. -->
         <item name="colorPrimary">@color/purple_500</item>
         <item name="colorPrimaryVariant">@color/purple_700</item>
         <item name="colorOnPrimary">@color/white</item>
         <!-- Secondary brand color. -->
         <item name="colorSecondary">@color/teal_200</item>
         <item name="colorSecondaryVariant">@color/teal_700</item>
         <item name="colorOnSecondary">@color/black</item>
         <!-- Status bar color. -->
         <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
         <!-- Customize your theme here. -->
     </style>
    </resources>
    
  6. 最终效果
    博客第一行代码自定义布局最终效果

自定义控件

引入布局的技巧确实解决了重复编写布局代码的问题,但无法响应事件.
我们还是需要在每个Activity中为这些控件单独编写一次事件注册的代码,不管是在哪一个Activity中,这个控件的功能都是相同的,这就是称为自定义控件.

  1. 新建TitleLayout.kt文件让它继承LinearLayout
    class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
    }
    
    这里我们在TitleLayout的主构造函数中声明了ContextAttributeSet这两个参数,在布局中引入TitleLayout 控件时就会调用这个构造函数。
  2. 然后在init结构体中需要对标题栏布局进行动态加载,这就要借助LayoutInflater. 通过LayoutInflater 的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件。inflate()方法接收两个参数:
    • 第一个参数是要加载的布局文件的id,这里我们传入R.layout.title;
    • 第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout ,于是直接传入this。
      class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
        init {
            LayoutInflater.from(context).inflate(R.layout.title,this)
        }
      }
      
  3. 引用控件,在这里以activity_main.xml为例
    ```xml
    <?xml version=”1.0” encoding=”utf-8”?>
<com.example.uiconstomviews.TitleLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

</LinearLayout>


4. 为标题栏的返回按钮注册点击事件,修改`TitleLayout中的代码
```kotlin
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
    init {
        LayoutInflater.from(context).inflate(R.layout.title,this)
        // 关键代码开始
         val titleBack:Button = findViewById(R.id.titleBack)
        titleBack.setOnClickListener{
            // 将Context转为Activity类型
            val activity = context as Activity
            activity.finish()
        }
        // 关键代码结束
    }
}

当点击返回按钮时销毁当前Activity

  1. 为标题栏的编辑按钮注册点击事件,修改`TitleLayout中的代码
    class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
     init {
         LayoutInflater.from(context).inflate(R.layout.title,this)
          val titleBack:Button = findViewById(R.id.titleBack)
         titleBack.setOnClickListener{
             // 将Context转为Activity类型
             val activity = context as Activity
             activity.finish()
         }
         // 关键代码开始
         val titleEdit = findViewById<Button>(R.id.titleEdit)
         titleEdit.setOnClickListener{
             Toast.makeText(context,"你点击了编辑按钮",Toast.LENGTH_LONG).show()
         }
         // 关键代码结束
     }
    }
    
    博客第一行代码自定义控件编辑按钮

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

列表-ListView(最常用和最难用的控件)

ListView 的简单用法

  1. 首先新建一个ListViewTest 项目,然后修改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">
         <ListView
             android:id="@+id/listView"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
     </LinearLayout>
    
  2. 接下来修改MainActivity中的代码

    class MainActivity : AppCompatActivity() {
     // 这是提供给listView的数据,
     // 这些数据可以从网上下载,也可以从数据库中读取,应该视具体的应用程序场景而定。
     // 这里我们就简单使用一个data集合来进行测试
     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)
         /*
         * 集合中的数据是无法直接传递给ListView 的,我们还需要借助适配器来完成
         * Android中提供了很多适配器的实现类,其中第一行代码中认为最好用的就是ArrayAdapter
         * 它可以通过 泛型 来指定要适配的数据类型,然后在构造函数中把要 适配的数据 传入。
         * 在这里因为全是String所以泛型为String
         * 然后在ArrayAdapter 的构造函数中依次传入Activity的实例、
         * ListView 子项布局的id,以及数据源。
         * 注意,我们使用了android.R.layout.simple_list_item_1作为ListView 子项布局的id,
         * 这是一个Android内置的布局文件,里面只有一个TextView ,可用于简单地显示一段文本。
         * */
         val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
         val listView  = findViewById<ListView>(R.id.listView)
         // 最后,还需要调用ListView 的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView 和数据之间的关联就建立完成了。
         listView.adapter = adapter
     }
    }
    

    博客第一行代码listView简单使用效果1

定制ListVie的界面

  1. 首先需要准备好一组图片资源,见配套资源源码\第4章\ListViewTest\app\src\main\res\drawable-xxhdpi\.复制里面的图片到res/drawable-xxhdpi文件夹.
  2. 定义实体类Fruit用于作为适配器的适配类型
    // Fruit类中只有两个字段:name表示水果的名字,imageId表示水果对应图片的资源id。
    class Fruit(name:String,imageId:Int) {
    }
    
  3. 要为ListView的子项指定一个我们自定义的布局,在layout 目录下新建
    fruit_item.xml ,代码如下所示:
    <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>
    
    在这个布局中,我们定义了一个ImageView 用于显示水果的图片,又定义了一个TextView 用于显示水果的名称,并让ImageView 和TextView 都在垂直方向上居中显示。
  4. 要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit类。新建类FruitAdapter,代码如下所示:
    // FruitAdapter定义了一个主构造函数,用于将 Activity的实例 、ListView子项 布局 的id和 数据源 传递进来。
    class FruitAdapter(activity: Activity,val resouceId: Int, data: List<Fruit>) :ArrayAdapter<Fruit>(activity, resouceId, data) {
     // 重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
     override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
         /*
         *LayoutInflater的inflate()方法接收3个参数,
         * 第一个表示int resource  代表需要加载资源的id、
         * 第二个ViewGroup root 代表资源需要被添加的地方
         * 第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不会为这个View 添加父布局。
         * */
         val view = LayoutInflater.from(context).inflate(resouceId,parent,false)
         //获得单个列表的文本框
         val fruitImage = view.findViewById<ImageView>(R.id.fruitImage)
         //获得单个列表的图片
         val fruitName = view.findViewById<TextView>(R.id.fruitName)
         // 获得当前的Fruit实例
         val fruit = getItem(position)
         if(fruit!=null){
             fruitImage.setImageResource(fruit.imageId)
             fruitName.text = fruit.name
         }
         // 最后返回布局
         return view
     }
    }
    
  5. 接下来就是使用FruitAdapter,在MainActivity中修改模拟数据,将适配器换成FruitAdapter

    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")
     // 创建一个Fruit集合,将数据添加进去
     private val fruitList = ArrayList<Fruit>().apply {
         // repeat函数用于将代码重复执行
         repeat(2) {
             add(Fruit("Apple", R.drawable.apple_pic))
             add(Fruit("Banana", R.drawable.banana_pic))
             add(Fruit("Orange", R.drawable.orange_pic))
             add(Fruit("Watermelon", R.drawable.watermelon_pic))
             add(Fruit("Pear", R.drawable.pear_pic))
             add(Fruit("Grape", R.drawable.grape_pic))
             add(Fruit("Pineapple", R.drawable.pineapple_pic))
             add(Fruit("Strawberry", R.drawable.strawberry_pic))
             add(Fruit("Cherry", R.drawable.cherry_pic))
             add(Fruit("Mango", R.drawable.mango_pic))
         }
     }
    
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         // 换成自己的Fruit适配器
         val adapter = FruitAdapter(this,R.layout.fruit_item,fruitList)
         val listView  = findViewById<ListView>(R.id.listView)
         listView.adapter = adapter
     }
    }
    

提升ListView 的运行效率

之所以说ListView 这个控件很用,是因为它有很多细节可以优化,其中运行效率就是很重要的一点。目前我们ListView 的运行效率是很低的,因为在FruitAdapter的getView()方法中,每次都将布局重新加载了一遍,当ListView 快速滚动的时候,这就会成为性能的瓶颈。
在getView()方法中还有一个convertView参数,这个参数会将之前加载好的view进行缓存,以便之后进行复用.我们修改FruitAdapter中的代码进行优化.

class FruitAdapter(activity: Activity,val resouceId: Int, data: List<Fruit>) :ArrayAdapter<Fruit>(activity, resouceId, data) {
    // 定义一个内部类来缓存ImageView和TextView控件
    inner class ViewHolder(val fruitImage:ImageView,val fruitName:TextView)
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val viewHolder:ViewHolder
        val view:View
        if (convertView ==null){
            view = LayoutInflater.from(context).inflate(resouceId,parent,false)
            // 创建两个控件实例
            val fruitImage = view.findViewById<ImageView>(R.id.fruitImage)
            val fruitName = view.findViewById<TextView>(R.id.fruitName)
            // 将创建的实例进行缓存。
            viewHolder = ViewHolder(fruitImage,fruitName)
            // 再将缓存好的viewHolder保存再view,tag里面,这样就会存在convertView中,
            view.tag  = viewHolder
        }else{
            view = convertView
            // 存在的话就取出viewHolder
            viewHolder = view.tag as ViewHolder
        }

        val fruit = getItem(position)
        if(fruit!=null){
            viewHolder.fruitImage.setImageResource(fruit.imageId)
            viewHolder.fruitName.text = fruit.name
        }
        // 最后返回布局
        return view
    }
}

ListView的点击事件

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

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")
    private val fruitList = ArrayList<Fruit>().apply {
        repeat(2) {
            add(Fruit("Apple", R.drawable.apple_pic))
            add(Fruit("Banana", R.drawable.banana_pic))
            add(Fruit("Orange", R.drawable.orange_pic))
            add(Fruit("Watermelon", R.drawable.watermelon_pic))
            add(Fruit("Pear", R.drawable.pear_pic))
            add(Fruit("Grape", R.drawable.grape_pic))
            add(Fruit("Pineapple", R.drawable.pineapple_pic))
            add(Fruit("Strawberry", R.drawable.strawberry_pic))
            add(Fruit("Cherry", R.drawable.cherry_pic))
            add(Fruit("Mango", R.drawable.mango_pic))
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val adapter = FruitAdapter(this,R.layout.fruit_item,fruitList)
        val listView  = findViewById<ListView>(R.id.listView)
        listView.adapter = adapter
        // 关键代码开始
        // 监听item点检事件,position判断用户点击的是哪一个子项。
        // 在Kotlin中未被使用的参数可以使用_替代
        listView.setOnItemClickListener{ _, _, position, _ ->
            val fruit = fruitList[position]
            Toast.makeText(this,fruit.name,Toast.LENGTH_LONG).show()
        }
        // 关键代码结束
    }
}

更强大的滚动控件-RecyclerView

基本用法

  1. 检查是否存在依赖
    最新版的Android Studio已经集成了RecyclerView,请在布局文件中输入RecyclerView查看是否有提示
    博客第一行代码检查是否存在RecyclerView2
    若没有请参考这篇文章添加依赖
  2. 修改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">
     <androidx.recyclerview.widget.RecyclerView
         android:id="recyclerView"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"/>
    </LinearLayout>
    
    需要注意的是,由于RecyclerV iew 并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。
  3. 由于实现的效果一样,所以将ListView项目中的图片复制过来,同时也将Fruit类和fruit_item.xml复制过来.
  4. 为RecyclerView指定适配器,新建FruitAdapter类继承RecyclerView.Adapter且泛型指定为FruitAdapter.ViewHolder.

    // FruitAdapter的参数是传入数据源。
    class FruitAdapter(val fruitList:List<Fruit>):RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
     // View参数通常是RecyclerView子项的最外层布局,这样就能在内部使用findViewById()找到ImageView和TextView.
     inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view){
         val fruitImage : ImageView = view.findViewById(R.id.fruitImage)
         val fruitName : TextView = view.findViewById(R.id.fruitName)
     }
    
     // 该方法用于将布局传入构造函数最后将加载好控件的ViewHolder实例返回
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         val view = LayoutInflater.from(parent.context)
         .inflate(R.layout.fruit_item,parent,false)
         return ViewHolder(view)
     }
    
     // 每个子项被滚到屏幕内时执行,通过position获得当前子项index.然后设置image和name
     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
         val fruit = fruitList[position]
         holder.fruitImage.setImageResource(fruit.imageId)
         holder.fruitName.text = fruit.name
     }
    
     // 该方法用于获取数据源长度
     override fun getItemCount(): Int = fruitList.size
    }
    
  5. 适配器准备好了之后,我们就可以开始使用RecyclerView了,修改MainActivity中的代码,如
    下所示:
    class MainActivity : AppCompatActivity() {
     private val fruitList = ArrayList<Fruit>().apply {
         repeat(2) {
             add(Fruit("Apple", R.drawable.apple_pic))
             add(Fruit("Banana", R.drawable.banana_pic))
             add(Fruit("Orange", R.drawable.orange_pic))
             add(Fruit("Watermelon", R.drawable.watermelon_pic))
             add(Fruit("Pear", R.drawable.pear_pic))
             add(Fruit("Grape", R.drawable.grape_pic))
             add(Fruit("Pineapple", R.drawable.pineapple_pic))
             add(Fruit("Strawberry", R.drawable.strawberry_pic))
             add(Fruit("Cherry", R.drawable.cherry_pic))
             add(Fruit("Mango", R.drawable.mango_pic))
         }
     }
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         // 创建一个线性布局对象,作用是设定布局方式为线性布局
         val layoutManager = LinearLayoutManager(this)
         val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
         // 设置布局方式为线性布局
         recyclerView.layoutManager = layoutManager
         // 传入数据源
         val adapter = FruitAdapter(fruitList)
         recyclerView.adapter = adapter
     }
    }
    
  6. 现在运行一下程序,效果如图所示。
    博客第一行代码3学习recyclerView效果1
    可以看到,我们使用RecyclerView 实现了和ListView几乎一模一样的效果,虽说在代码量方面
    并没有明显的减少,但是逻辑变得更加清晰了。

实现横向滚到和瀑布流布局

若要实现为了实现横向滚动的话,ListView就做不到了.这时候就需要使用RecyclerView来实现了.

  1. 修改fruit_item.xml,将LinearLayout中的对齐方式(orientation)修改为垂直排列.
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="80dp"
     android:layout_height="wrap_content">
     <ImageView
         android:id="@+id/fruitImage"
         android:layout_width="40dp"
         android:layout_height="40dp"
         android:layout_gravity="center_horizontal"
         android:layout_marginLeft="10dp"/>
     <TextView
         android:id="@+id/fruitName"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal"
         android:layout_marginLeft="10dp"/>
    </LinearLayout>
    
    将LinearLayout 改成垂直方向排列,并把宽度设为80 dp 。这里将宽度指定为固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView 的子项就会有长有短,非常不美观,而如果用match_parent的话,就会导致宽度过长,一个子项占满整个屏幕。然后我们将ImageV iew 和Te xtView 都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一定距离
  2. 修改MainActivity中的代码,如下所示:
    class MainActivity : AppCompatActivity() {
     private val fruitList = ArrayList<Fruit>().apply {
         repeat(2) {
             add(Fruit("Apple", R.drawable.apple_pic))
             add(Fruit("Banana", R.drawable.banana_pic))
             add(Fruit("Orange", R.drawable.orange_pic))
             add(Fruit("Watermelon", R.drawable.watermelon_pic))
             add(Fruit("Pear", R.drawable.pear_pic))
             add(Fruit("Grape", R.drawable.grape_pic))
             add(Fruit("Pineapple", R.drawable.pineapple_pic))
             add(Fruit("Strawberry", R.drawable.strawberry_pic))
             add(Fruit("Cherry", R.drawable.cherry_pic))
             add(Fruit("Mango", R.drawable.mango_pic))
         }
     }
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val layoutManager = LinearLayoutManager(this)
         // 设置排列方向是垂直
         layoutManager.orientation = LinearLayoutManager.HORIZONTAL
         val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
         recyclerView.layoutManager = layoutManager
         val adapter = FruitAdapter(fruitList)
         recyclerView.adapter = adapter
     }
    }
    
    MainActivity 中只加入了一行代码,调用LinearLayoutManager 的setOrientation()方法设置布局的排列方向。默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL表示让布局横行排列,这样RecyclerV iew 就可以横向滚动了。
    博客第一行代码检查是否存在RecyclerView流布局1
  3. 实现瀑布流布局

    1. 修改一下fruit_item.xml中的代码,如下所示:

      <?xml version="1.0" encoding="utf-8"?>
      <LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_margin="10dp">
      <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginLeft="10dp"/>
      <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_marginLeft="10dp"/>
      </LinearLayout>
      

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

    2. 接着修改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
      
      class MainActivity : AppCompatActivity() {
        private val fruitList = ArrayList<Fruit>().apply {
            repeat(2) {
                add(Fruit(getRandomLengthString(getRandomLengthString("Apple")), R.drawable.apple_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Banana")), R.drawable.banana_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Orange")), R.drawable.orange_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Watermelon")), R.drawable.watermelon_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Pear")), R.drawable.pear_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Grape")), R.drawable.grape_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Pineapple")), R.drawable.pineapple_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Strawberry")), R.drawable.strawberry_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Cherry")), R.drawable.cherry_pic))
                add(Fruit(getRandomLengthString(getRandomLengthString("Mango")), R.drawable.mango_pic))
            }
        }
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
      
      //      更改为网格布局
            val layoutManager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
      
            val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
            recyclerView.layoutManager = layoutManager
            val adapter = FruitAdapter(fruitList)
            recyclerView.adapter = adapter
        }
      
      //  将水果名字随机重复。
        private fun getRandomLengthString(str:String):String{
            val n = (1..20).random()
            val builder = StringBuilder()
            repeat(n){
                builder.append(str)
            }
            return builder.toString()
        }
      }
      

      StaggeredGridLayoutManager的构造函数接收两个参数:

      • 第一个参数用于指定布局的列s数,传入3表示会把布局分为3列
      • 第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列。
        这时候运行项目你大概会看到这样的效果
        博客第一行代码学习瀑布流效果

RecyclerView的点击事件

RecyclerView 并没有提供类似于setOnItemClickListener()这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件。这相比于ListView 来说,实现起来要复杂一些。
RecyclerView摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View 去注册.

  1. 下面我们来具体学习一下如何在RecyclerView 中注册点击事件,修改FruitAdapter中的代码,如下所示:
    ```kotlin
    package com.example.recyclerviewtest

import android.media.Image
import android.util.Log
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):RecyclerView.Adapter(){
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)

// 将view转换为viewHolder
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener {
// 提取当前被点击view的position
val position = viewHolder.adapterPosition
// 提取出当前fruit对象
val fruit = fruitList[position]
Toast.makeText(parent.context,”你点击了文本${fruit.name}”,Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context,”你点击了图片${fruit.name}”,Toast.LENGTH_SHORT).show()
}

    return viewHolder
}

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

override fun getItemCount(): Int = fruitList.size

}

可以看到,这里我们是在onCreateViewHolder()方法中注册点击事件。上述代码分别为最外层布局和ImageView 都注册了点击事件,itemView 表示的就是最外层布局。RecyclerView的强大之处也在于此,它可以轻松实现`子项中任意控件或布局的点击事件`。我们在两个点击事件中先获取了用户点击的position ,然后通过position 拿到相应的Fruit实例,再使用Toast 分别弹出两种不同的内容以示区别。
![博客第一行代码3学习recyclerView效果点击事件效果](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码3学习recyclerView效果点击事件效果.gif)

## 编写界面的最佳实践
既然已经学习了那么多UI开发的知识,是时候实战一下了。这次我们要综合运用前面所学的大量内容来编写出一个较为复杂且相当美观的聊天界面,你准备好了吗?要先创建一个`UIBestPractice` 项目才算准备好了哦。
### 制作9-Patch (点9)图片
9-Patch你之前可能没有听说过这个名词,它是一种被特殊处理过的png 图片,能够`指定`哪些`区域`可以`被拉伸`、哪些区域不可以。接下来还是通过一个例子来了解一下它吧!  
1. 复制资料的`源码\第4章\UIBestPractice\app\src\main\res\drawable-xxhdpi\`下的`message_left_original.png`和`message_right_original.png`文件到`res\drawable-xxhdpi`,如图所示.
![博客第一行代码最佳实践复制聊天图片](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码最佳实践复制聊天图片.png)
2. 制作作9-Patch 图片其实并不复杂,只要掌握好规则就行了,那么现在我们就来学习一下。  
    在Andr oid Studio 中,我们可以将任何png 类型的图片制作成9-Patch 图片。首先对着`message_left_original.png` 图片右击→`Create 9-P atch file` ,会弹出如图所示的对话框。这里保持默认文件名就可以了,其实就相当于创建了一张以9.png 为后缀的同名图片,点击“Save” 完成保存。
    ![博客第一行代码点9确认框](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码点9确认框.png)
    ![博客第一行代码创建点9文件步骤](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码创建点9文件步骤.gif)
    这时Andr oid Studio 会显示如图所示的编辑界面。
    ![博客第一行代码创建完点9文件](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码创建完点9文件.png)
3. 在体魄四个边框按鼠标左键可以进行边框绘制,被标黑的区域表示该方向图片可以被拉伸,按shift进行拖动可以取消标记.
   左键标记
   ![博客第一行代码点9标黑](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码点9标黑.gif)
   shift+左键取消标记
    ![博客第一行代码点9取消标黑](https://jsdelivr.007666.xyz/gh/1802024110/GitHub_Oss@main/img/博客第一行代码点9取消标黑.gif)
    自行修改图片left和right或者将资料中的.9文件复制即可.

### 编写精美的聊天界面
既然是要编写一个聊天界面,那肯定要有收到的消息和发出的消息。
1. 接下来开始编写主界面,修改`activity_main.xml` 中的代码,如下所示:
   ```xml
   <?xml version="1.0" encoding="utf-8"?>
<!--
    使用线性布局作为约束
    垂直分部子控件
    设置淡灰色作为聊天背景
-->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="#d8e0e8"
    >
    <!-- 使用RecyclerView(约束布局)来束缚聊天信息 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />
<!--    使用线性布局来约束发送消息控件-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
<!--        文本框-->
        <EditText
            android:id="@+id/inputText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="在这输入消息"
            android:maxLines="2"
            />
<!--        发送按钮-->
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送"/>
    </LinearLayout>
</LinearLayout>
  1. 然后定义消息的实体类,新建Msg.kt,代码如下所示:
     class Msg(val content: String, val type: Int) { 
         companion object { 
             const val TYPE_RECEIVED = 0 
             const val TYPE_SENT = 1 
         } 
     }
    
    Msg类中只有两个字段:content表示消息的内容,type表示消息的类型。其中消息类型有两个值可选:TYPE_RECEIVED表示这是一条收到的消息,TYPE_SENT表示这是一条发出的消息。这里我们将TYPE_RECEIVEDTYPE_SENT定义成了常量,定义常量的关键字是const,注意只有单例类companion object顶层方法中才可以使用const关键字
  2. 接下来开始编写RecyclerView的子项布局,新建msg_left_item.xml ,代码如下所示:
             <?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="wrap_content"
         android:padding="10dp">
         <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="left"
             android:background="@drawable/message_left">
         <TextView
             android:id="@+id/leftMsg"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:gravity="center"
             android:layout_margin="10dp"
             android:textColor="#fff"/>
         </LinearLayout>
     </FrameLayout>
    
    里我们让收到的消息居左对齐,并使用message_left.9.png 作为背景图。
  3. 接下来开始编写个发送消息的子项布局,新建msg_right_item.xml ,代码如下所示:
         <?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="wrap_content"
         android:padding="10dp">
         <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="right"
             android:background="@drawable/message_right">
         <TextView
             android:id="@+id/rightMsg"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:gravity="left"
             android:layout_margin="10dp"
             android:textColor="#000000"/>
         </LinearLayout>
     </FrameLayout>
    
    里我们让收到的消息居右对齐,并使用message_right.9.png 作为背景图。
  4. 创建RecyclerView 的适配器类,新建类MsgAdapter,代码如下所示:
    ```kotlin
    package com.example.uibaseproject

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

class MsgAdapter(val msgList:List) : RecyclerView.Adapter(){
// 左边控件内部类,用于缓存控件
inner class LeftViewHolder(view: View,val leftMsg : TextView=view.findViewById(R.id.leftMsg)) : RecyclerView.ViewHolder(view)
// 右边控件内部类,用于缓存控件
inner class RightViewHolder(view: View,val rightMsg : TextView=view.findViewById(R.id.rightMsg)) : RecyclerView.ViewHolder(view)
// 获得是接受还是发送
override fun getItemViewType(position: Int): Int = msgList[position].type
// 根据不同类型来加载不同布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when(viewType){
Msg.TYPE_RECEIVED -> LeftViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item,parent,false))
else -> RightViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,parent,false))
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val msg = msgList[position]
    when(holder){
        is LeftViewHolder -> holder.leftMsg.text = msg.context
        is RightViewHolder -> holder.rightMsg.text = msg.context
        else -> throw IllegalArgumentException()
    }
}

override fun getItemCount(): Int = msgList.size

}

7. 最后修改`MainActivity` 中的代码,为RecyclerView初始化一些数据,并给发送按钮加入事件响应,代码如下所示:
```kotlin
class MainActivity : AppCompatActivity() {
    // 
    private val msgList = ArrayList<Msg>().apply {
        add( Msg("Hello",Msg.TYPE_RECEIVED))
        add( Msg("HI",Msg.TYPE_SENT))
        add( Msg("小明",Msg.TYPE_RECEIVED))
        add( Msg("小红",Msg.TYPE_SENT))
        add( Msg("在做什么",Msg.TYPE_RECEIVED))
        add( Msg("写代码",Msg.TYPE_SENT))
    }
    private var adapter:MsgAdapter? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val layoutManager  = LinearLayoutManager(this)
        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = layoutManager
        adapter = MsgAdapter(msgList)
        recyclerView.adapter = adapter

        val send = findViewById<Button>(R.id.send)
        send.setOnClickListener{
            val inputText = findViewById<EditText>(R.id.inputText)
            val content = inputText.text.toString()
            if(content.isNotEmpty()){
                val msg = Msg(content,Msg.TYPE_SENT)
                msgList.add(msg)
                adapter?.notifyItemInserted(msgList.size-1)
                recyclerView.scrollToPosition(msgList.size -1)
                inputText.setText("")
            }
        }
    }
}

msgList中初始化了几条数据用于在RecyclerV iew 中显示,接下来按照标准的方式构建RecyclerView ,给它指定一个LayoutManager 和一个适配器。然后在发送按钮的点击事件里获取了EditText 中的内容,如果内容不为空字符串,则创建一个新的Msg对象并添加到msgList 列表中去。之后又调用了适配器的notifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerV iew 中显示出来。或者你也可以调用适配器的notifyDataSetChanged()方法,它会RecyclerV iew 中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一些。接着调用RecyclerV iew 的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息。最后调用EditTe xt 的setText()方法将输入的内容清空。这样所有的工作都完成了,终于可以检验一下我们的成果了。
运行程序之后,你将会看到非常美观的聊天界面,并且可以输入和发送消息,如图所示。
博客第一行代码学习最好的示例

  1. 本篇代码示例
    Github

评论