本章我们将介绍UI开发的相关知识。常用控件包括了TextView、Button、EditText、ImageView、PrograssBar和AlertDialog等;三种布局包括了LinearLayout、RelativeLayout以及FrameLayout;ListView的简单用法、基于图片文字的ListView、利用ConvertView和ViewHolder去提升效率以及setOnItemClickListener点击事件响应;RecyclerView的基本用法、适配器、横向滚动、瀑布流布局以及View的点击事件处理;随后进行了实践(基于nine-patch图片编写聊天界面);最后是延迟初始化lateinit和密封类sealed class。
4.1.如何编写程序界面?
主要通过编写XML来实现,另外Google近些年推出了ConstrainLayout,它是通过拖拽控件来对界面进行操作。在本章我们仅介绍xml。
4.2.常用控件使用方法
4.2.1.TextView
TextView的功能是显示一段文本信息。
4.2.2.Button
与用户进行交互的一个重要控件。先编写xml。代码如下:
.....
其次,点击事件可以采用函数式API进行事件响应,也可以通过实现接口的方式来进行注册。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//第一种调用方法:匿名内部类,利用Java单抽象方法接口特性,使用函数式API来写监听事件。
// btn_click01.setOnClickListener{
// //在此添加处理逻辑
// }
//第二种调用方法:实现接口方法进行注册。让MainActivity实现了View.OnClickListener接口,并重写onClick方法
//setOnClickListener将MainActivity实例传了进去
btn_click01.setOnClickListener(this)
}
//重写的方法,很简单。
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_click01 -> {
//在此添加逻辑
}
}
}
}
4.2.3.EditText
用户在该控件中输入和编辑文本。应用场景譬如发微博、聊QQ等。XML中定义如下:
点击按钮获取EditText里面的内容,代码如下:
//重写的方法,很简单。
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_click01 -> {
//点击按钮完成显式EditText文本内容的功能
//edit_text01.text通过语法糖调用了getText方法,编写代码直接调用实际方法即可,getText可以自动转换为text。
val inputText = edit_text01.text.toString()
Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
}
}
}
4.2.4.ImageView
ImageView用于在界面显示图片,图片通常放在Drawable开头的目录,并且要附上具体分辨率。目前主流屏幕分辨率是xxhdpi的,所以在res目录再建一个drawable-xxhdpi目录,将事先准备好的照片复制到该目录。Xml如下:
相应的响应事件如下:
//通过代码动态更改ImageView图片
R.id.btn_click02 ->{
image_view01.setImageResource(R.drawable.image_2)
}
4.2.5.PrograssBar
PrograssBar是显示一个进度条,表示正在加载数据。Xml代码如下:
Kotlin代码中有相应的响应事件和进度条递增代码。
//android:visibility有三个可选值:visiable(控件可见,默认)、invisible(空间不可见,仍占据原来未知)
// 和gone(控件不仅不可见,也不再占用之前的屏幕),在使用setVisibility使用View.可选值
R.id.btn_click03 -> {
//使用getVisibility判断prograssBar是否可见,如果可见隐藏,否则显示
if (progress_bar01.visibility == View.VISIBLE) {
//调用了setVisibility
progress_bar01.visibility = View.GONE
} else {
progress_bar01.visibility = View.VISIBLE
}
}
//进度条增加
R.id.btn_click04 -> {
progress_bar01.progress += 10
}
4.2.6.AlertDialog
AlertDialog在当前界面显示一个置顶于所有界面之上的对话框,屏蔽掉其他控件的交互,在这里譬如实现防止用户误删除的AlertDialog。
R.id.btn_click05 -> {
//构建一个对话框并使用apply函数
AlertDialog.Builder(this).apply {
//消息标题
setTitle("This is Dialog")
//消息内容
setMessage("Sth important")
//不可取消
setCancelable(false)
//确定按钮点击事件
setPositiveButton("OK") { dialog, which -> "删除吧" }
//取消按钮点击事件
setNegativeButton("Cancel") { dialog, which -> "保留吧" }
//对话框显示出来
show()
}
}
4.3.详解三种基本布局
布局是放置很多控件的容器,布局内部可以放置布局或者控件,也可以通过多层布局嵌套完成复杂界面。
4.3.1.LinearLayout
线性布局,控件在线性方向上水平或者竖直排列,在这里使用android:orientation属性来指定。这里着重注意android:gravity、android:layout_gravity和android:layout_weight三个属性。
4.3.2.RelativeLayout
RelativeLayout相对定位的方式可以让控件出现在布局中的任何位置。控件不仅可以相对于父布局进行定位,也可以相对于其他控件进行定位。代码如下所示:
4.3.3.FrameLayout
帧布局,所有空间都会默认摆放在布局左上角,可能存在控件重叠的情况。除了默认效果之外,可以使用layout_gravity属性来指定控件在布局中的对齐方式。总体来讲,应用场景很少。
4.4.创建自定义控件
所有控件直接或者间接继承自View的,所有布局直接或者间接继承自ViewGroup的。ViewGroup是特殊的View,包含很多子View和ViewGroup是用于放置空间和布局的容器。
4.4.1.引入布局
自定义一个标题栏并让所有Activity引用,防止代码大量重复。Layout目录下建立标题栏title.xml布局,代码如下:
那么如何在程序中引入,修改Activity_main.xml里面的代码:
另外,需要在MainActivity中隐藏系统自带标题栏:
package com.example.myapplication
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)
//通过语法糖调用getSupportActionBar来获得ActionBar的实例,然后调用hide方法将标题隐藏
//由于ActionBar可能为空,因此使用了?.操作符,目的是隐藏原始标题栏
supportActionBar?.hide()
}
}
4.4.2.创建自定义控件
有些布局中需要有一些控件有响应事件的能力,但在每个Activity中单独编写无疑有很多冗余的代码,新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件,代码如下:
package com.example.myapplication
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.*
//继承自LinearLayout使其成为我们的自定义控件。声明两个参数,在init结构体中对标题栏布局进行动态加载
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
//LayoutInflater的from方法可以构建出一个LayoutInflater对象,然后inflate方法动态加载一个布局文件
//inflate加载两个参数,一个是布局文件ID,一个是给加载好的布局再添加一个父布局,在这里指定为TitleLayout,直接传入this。
LayoutInflater.from(context).inflate(R.layout.title, this)
titleBack.setOnClickListener {
//context参数实际上是Activity实例,首先将其抓换为Activity类型,然后再行销毁。
// Kotlin的强制类型转换用as
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked the edit", Toast.LENGTH_SHORT).show()
}
}
}
在Activity_main.xml中引入该自定义控件。
4.5.ListView
4.5.1.ListView的简单用法
新建activity_main.xml文件:
修改MainActivity、构建适配器,并将其传入ListView中去。这样可以通过滚动来看屏幕外数据。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
//提供待展示数据,使用listOf初始化集合
private val data = listOf(
"Apple", "Banana", "Orange", "WaterMelon", "Pear", "Grape", "Cherry","Mango", "Apple", "Banana", "Orange", "WaterMelon", "Pear", "Grape", "Cherry", "Mango"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//集合数据无法直接传递,需要借助适配器。比较好用的是ArrayAdapter,泛型指定为String
//在构造方法中分别传入Activity实例、ListView子项布局的id以及数据源
//simple_list_item_1为子项布局id,Android内置布局文件,里面只有TextView用于显示一段文本
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, data)
//最后利用setAdapter方法将构建好的适配器传递进去,这样ListView和数据之间的关联算是搞定了
listView_main.adapter = adapter
}
}
4.5.2.定制ListView的界面
ListView的每个子项中包含图片和文字,即自定义。新建实体类Fruit:
package com.example.myapplication
//Fruit类有两个字段,一个是水果名,一个是水果对应图片资源的ID
class Fruit(val name: String, val imageid: Int)
在layout目录下新建fruit_item.xml:
新建自定义适配器FruitAdapter,继承自ArrayAdapter,泛型指定为Fruit类。
package com.example.myapplication
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
//定义一个主构造函数来将Activity实例、ListView子项布局id和数据源传递进来
class FruitAdapter(activity: Activity, val resourceId: Int, data: List) :
ArrayAdapter(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//首先使用LayoutInflater子项加载我们的布局,三个参数。
// 最后一个false的意思是父布局声明的layout生效,不会为该View增加父布局。保准写法
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//获取到ImageView和TextView的实例
val fruitImage: ImageView = view.findViewById(R.id.fruit_image)
val fruitName: TextView = view.findViewById(R.id.fruit_Name)
//得到当前项的Fruit实例
val fruit = getItem(position)
//设置图片和文字
if (fruit != null) {
fruitImage.setImageResource(fruit.imageid)
fruitName.text = fruit.name
}
//将布局返回
return view
}
}
最后修改MainActivity里的代码。
package com.example.myapplication
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 fruit_list = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()//初始化水果数据
//创建Adapter对象,并将其作为适配器传递给ListView
val adapter = FruitAdapter(this, R.layout.fruit_item, fruit_list)
listView_main.adapter = adapter
}
fun initFruits() {
//repeat函数将水果数据重复两遍
repeat(2) {
//构造方法中将水果名字和水果id传入,将创建好的对象添加到水果列表中。
fruit_list.add(Fruit("Apple", R.drawable.apple_pic))
fruit_list.add(Fruit("Banana", R.drawable.banana_pic))
fruit_list.add(Fruit("Orange", R.drawable.orange_pic))
fruit_list.add(Fruit("WaterMelon", R.drawable.watermelon_pic))
fruit_list.add(Fruit("Pear", R.drawable.pear_pic))
fruit_list.add(Fruit("PineApple", R.drawable.pineapple_pic))
fruit_list.add(Fruit("StrawBerry", R.drawable.strawberry_pic))
fruit_list.add(Fruit("Cherry", R.drawable.cherry_pic))
}
}
}
4.5.3.提升ListView的效率
ListView快速滑动时,性能成为一个瓶颈,在这里我们借助了两种方案:getView的convertView参数和ViewHolder进行性能优化。
convertView用于将之前已经加载好的布局进行缓存,以便以后重用;如果convertView为空,使用LayoutInflater直接加载,如果不为空,直接对convertView进行重用。
优化convertView后,虽然已经不会加载重复布局,但仍然在调用getView方法时调用view的findviewByID获取控件实例,因此借助ViewHolder来进行优化。优化后的代码如下所示:
package com.example.myapplication
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
//定义一个主构造函数来将Activity实例、ListView子项布局id和数据源传递进来
class FruitAdapter(activity: Activity, val resourceId: Int, data: List) :
ArrayAdapter(activity, resourceId, data) {
//innerclass来定义内部类
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//首先使用LayoutInflater子项加载我们的布局,三个参数。
// 最后一个false的意思是父布局声明的layout生效,不会为该View增加父布局。保准写法
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//用于对ImageView和TextView的控件实例进行缓存
val fruitImage: ImageView = view.findViewById(R.id.fruit_image)
val fruitName: TextView = view.findViewById(R.id.fruit_Name)
//创建ViewHolder对象并将控件实例放在ViewHolder里
viewHolder = ViewHolder(fruitImage, fruitName)
//使用View的setTag方法将ViewHolder存储在View中。
view.tag = viewHolder
} else {
view = convertView
//缓存对象不为空时,使用View的getTag方法将ViewHolder重新取出,这样所有控件的实例都存储于ViewHolder中了
viewHolder = view.tag as ViewHolder
}
//得到当前项的Fruit实例
val fruit = getItem(position)
//设置图片和文字
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageid)
viewHolder.fruitName.text = fruit.name
}
//将布局返回
return view
}
}
4.5.4.ListView的点击事件
使用setOnItemClickListener为ListView注册一个监听器,当点击子项时,调用Lambda表达式,通过position确定哪一个子项。代码示例如下:
// listView_main.setOnItemClickListener { parent, view, position, id ->
// val fruit = fruit_list[position]
// Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
// }
//没用到的参数可以用_来代替。因为是Java单抽象方法接口,所以可用函数式API的写法。onItemClick中接收四个参数。
listView_main.setOnItemClickListener { _, _, position, _ ->
val fruit = fruit_list[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
4.6.RecyclerView
ListView的运行效率和扩展性(只能纵向滚动)仍然有待提高,因此我们介绍一下RecyclerView。RecycleView属于新增空间,需要在app/build.gradle中添加该库依赖。
dependencies {
...
compile 'androidx.recyclerview:recyclerview:1.0.0'
....
}
在这里如果使用implementation引入新库时会报错”org.gradle.internal.metaobject.AbstractDynamicObject”。需要将其改变为compile。随后sync now。修改activity_main.xml。
复制上一小节的fruir_item.xml、Fruit类和所有图片,为RecyclerView新建FruitAdapter适配器,并制定相应的泛型,ViewHolder是一个内部类。代码如下:
package com.example.myapplication
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
//定义一个主构造函数来将Activity实例、ListView子项布局id和数据源传递进来
class FruitAdapter(activity: Activity, val resourceId: Int, data: List) :
ArrayAdapter(activity, resourceId, data) {
//innerclass来定义内部类
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//首先使用LayoutInflater子项加载我们的布局,三个参数。
// 最后一个false的意思是父布局声明的layout生效,不会为该View增加父布局。保准写法
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//用于对ImageView和TextView的控件实例进行缓存
val fruitImage: ImageView = view.findViewById(R.id.fruit_image)
val fruitName: TextView = view.findViewById(R.id.fruit_Name)
//创建ViewHolder对象并将控件实例放在ViewHolder里
viewHolder = ViewHolder(fruitImage, fruitName)
//使用View的setTag方法将ViewHolder存储在View中。
view.tag = viewHolder
} else {
view = convertView
//缓存对象不为空时,使用View的getTag方法将ViewHolder重新取出,这样所有控件的实例都存储于ViewHolder中了
viewHolder = view.tag as ViewHolder
}
//得到当前项的Fruit实例
val fruit = getItem(position)
//设置图片和文字
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageid)
viewHolder.fruitName.text = fruit.name
}
//将布局返回
return view
}
}
最后修改MainActivity中的代码。
package com.example.myapplication
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 fruit_list = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
//创建一个LinearLayoutManager线性布局对象,并将其设置到RecyclerView当中
val layoutmanager = LinearLayoutManager(this)
recycler_view.layoutManager = layoutmanager
//创建FruitAdapter实例并将水果数据传入FruitAdapter构造函数中
val adapter = FruitAdapter(fruit_list)
//最后调用setAdapter来完成适配器设置,从而完成RecyclerView与数据的关联
recycler_view.adapter = adapter
}
private fun initFruits() {
//repeat函数将水果数据重复两遍
repeat(2) {
//构造方法中将水果名字和水果id传入,将创建好的对象添加到水果列表中。
fruit_list.add(Fruit("Apple", R.drawable.apple_pic))
fruit_list.add(Fruit("Banana", R.drawable.banana_pic))
fruit_list.add(Fruit("Orange", R.drawable.orange_pic))
fruit_list.add(Fruit("WaterMelon", R.drawable.watermelon_pic))
fruit_list.add(Fruit("Pear", R.drawable.pear_pic))
fruit_list.add(Fruit("PineApple", R.drawable.pineapple_pic))
fruit_list.add(Fruit("StrawBerry", R.drawable.strawberry_pic))
fruit_list.add(Fruit("Cherry", R.drawable.cherry_pic))
}
}
}
4.6.2.实现横向滚动和瀑布流布局
ListView扩展性不好的原因是只能进行纵向滚动,RecyclerView能做到挺多滚动方式,譬如横向滚动。简单修改fruit_item布局文件。
核心的是在MainActivity.java中加上一句横向排列即可。
layoutmanager.orientation = LinearLayoutManager.HORIZONTAL
相比ListView为什么如此简单?ListView基于自身管理,RecyclerView则将这个工作交给了LayoutManager,除了LinearLayoutManager之外,还有网格布局GridLayoutManager和瀑布流布局StaggeredGridLayoutManager。下面谈谈瀑布流布局,还是得先修改fruit_item.xml。
其次,修改MainActivity的代码:
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import java.lang.StringBuilder
class MainActivity : AppCompatActivity() {
private val fruit_list = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
//两个参数,第一个是几列;第二个是布局的排列方向,我们选择垂直
val layoutmanager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recycler_view.layoutManager = layoutmanager
//创建FruitAdapter实例并将水果数据传入FruitAdapter构造函数中
val adapter = FruitAdapter(fruit_list)
//最后调用setAdapter来完成适配器设置,从而完成RecyclerView与数据的关联
recycler_view.adapter = adapter
}
private fun initFruits() {
//repeat函数将水果数据重复两遍
repeat(2) {
//构造方法中将水果名字和水果id传入,将创建好的对象添加到水果列表中。
fruit_list.add(Fruit(getRandomLengthString("Apple"), R.drawable.apple_pic))
fruit_list.add(Fruit(getRandomLengthString("Banana"), R.drawable.banana_pic))
fruit_list.add(Fruit(getRandomLengthString("Orange"), R.drawable.orange_pic))
fruit_list.add(Fruit(getRandomLengthString("WaterMelon"), R.drawable.watermelon_pic))
fruit_list.add(Fruit(getRandomLengthString("Pear"), R.drawable.pear_pic))
fruit_list.add(Fruit(getRandomLengthString("PineApple"), R.drawable.pineapple_pic))
fruit_list.add(Fruit(getRandomLengthString("StrawBerry"), R.drawable.strawberry_pic))
fruit_list.add(Fruit(getRandomLengthString("Cherry"), R.drawable.cherry_pic))
}
}
private fun getRandomLengthString(str:String):String{
val n = (1..20).random()
val builder = StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}
}
4.6.3.RecyclerView的点击事件
RecyclerView没有setOnItemClickListener这样的注册监听器方法,而是需要我们给子项具体的View去注册点击事件。修改适配器FruitAdapter的onCreateViewHolder方法。
//用于创建ViewHolder实例,将fruit_item加载进来,然后创建ViewHolder实例,最后将布局传入构造函数当中
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitAdapter.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
val viewholder = ViewHolder(view)
//为最外层的布局和ImageView都注册了点击事件,先获取用户点击的position,然后通过position获取相应的Fruit实例,最后Toast
viewholder.itemView.setOnClickListener {
val position = viewholder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked view ${fruit.name}", Toast.LENGTH_SHORT)
.show()
}
viewholder.fruitImage.setOnClickListener{
val position = viewholder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked image ${fruit.name}", Toast.LENGTH_SHORT)
.show()
}
return viewholder
}
4.7.编写界面的最佳实践
实战一下。
4.7.1.制作9-Patch图片
9-Patch图片是经过特殊处理的png图片,能够指定哪些区域可以被拉伸,哪些区域不能被拉伸。普通照片拉伸效果非常差。对普通图片右击->Create 9-Patch file->对图片拖拽表示拉伸部分->删除原来的普通照片。
4.7.2.编写聊天界面
仿聊天软件的聊天界面。首先在app/build.gradle里面添加RecyclerView的依赖库:
compile 'androidx.recyclerview:recyclerview:1.0.0'
其次,修改activity_main.xml里面的代码,放置显示聊天内容RecyclerView、EditText和发送button。
再者,编写消息实体类Msg。
package com.example.myapplication
//消息实体类,两个参数,一个是消息内容,一个是消息类型,消息类型为发出消息和接收消息两种。
class Msg(val content: String, val type: Int) {
companion object {
//将其定义成常量,定义常量的关键字为const,只有在单例类、companion object和顶层方法才能使用const关键字
const val TYPE_RECEIVED = 0
const val TYPE_SENT = 1
}
}
随后,编写RecyclerView的子项左布局msg_left_item.xml和右布局msg_right_item.xml。
创建RecyclerView适配器MsgAdapter,根据不同的ViewType来创建不同的界面。代码如下:
package com.example.myapplication
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.lang.IllegalArgumentException
//根据不同的type创建不同的界面
class MsgAdapter(val msgList: List) : RecyclerView.Adapter() {
//定义两个ViewHolder用于缓存msg_left_item和msg_right_item布局中的控件
inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.left_Msg)
}
inner class RightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.right_Msg)
}
//返回当前position相应的消息类型
override fun getItemViewType(position: Int): Int {
val msg = msgList[position]
return msg.type
}
//根据不同的ViewType加载不同的布局并创建不同的ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == Msg.TYPE_RECEIVED) {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item, parent, false)
return LeftViewHolder(view)
} else {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item, parent, false)
return RightViewHolder(view)
}
}
override fun getItemCount(): Int = msgList.size
//判断ViewHolder类型,若为LeftViewHolder,显示左边消息布局,否则显示右边。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
}
最后修改MainActivity里的代码,为RecyclerView初始化数据,并添加相应的点击事件。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val msgList = ArrayList()
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化几条数据显示
initMsg()
//构建RecyclerView,指定一个LayoutManager和一个适配器
val layoutManager = LinearLayoutManager(this)
recycler_view.layoutManager = layoutManager
adapter = MsgAdapter(msgList)
recycler_view.adapter = adapter
btn_send.setOnClickListener(this)
}
fun initMsg() {
val msg1 = Msg("Hello guy.", Msg.TYPE_RECEIVED)
msgList.add(msg1)
val msg2 = Msg("Hello,who is that?", Msg.TYPE_SENT)
msgList.add(msg2)
val msg3 = Msg("Tom", Msg.TYPE_RECEIVED)
msgList.add(msg3)
}
override fun onClick(p0: View?) {
when (p0) {
btn_send -> {
//获取EditText里面的内容
val content = input_text.text.toString()
if (!content.isEmpty()) {
//如果不为空,则新建Msg将其添加至msgList列表中
val msg = Msg(content, Msg.TYPE_SENT)
msgList.add(msg)
//notifyItemInserted通知列表有新数据插入,显示出来,刷新RecyclerView中的显示
adapter?.notifyItemInserted(msgList.size - 1)
//将RecyclerView显示的数据定位至最后一行
recycler_view.scrollToPosition(msgList.size - 1)
//清空输入框内容
input_text.setText("")
}
}
}
}
}
4.8.Kotlin之延迟初始化和密封类
4.8.1.对变量延迟初始化
很多全局变量不可能为空,但由于Kotlin语法特性,你不得不做许多非空判断保护,即使你很确定这玩意不可能为空。
class MainActivity : AppCompatActivity(), View.OnClickListener {
//因为初始化是在onCreate方法中进行,因此不得不将adapter赋值为null,同时将他的类型声明改成 MsgAdapter?
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = MsgAdapter(msgList)
}
override fun onClick(p0: View?) {
.....
//肯定要进行判空处理,否则编译无法进行
adapter?.notifyItemInserted(msgList.size - 1)
....
}
}
上述的代码全局变量实例越多,编写额外的判空处理代码就越多。因此我们使用lateinit延迟初始化对上面代码进行优化,告诉编译器我待会初始化,一开始不用置null。另外,我们需要判断全局变量是否已经完成了初始化,以避免某个变量重复初始化。代码示例如下:
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var adapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
...
//判断变量是否进行初始化,如果初始化,则不用重复对变量初始化,否则初始化
if(!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}
}
override fun onClick(p0: View?) {
.....
adapter.notifyItemInserted(msgList.size - 1)
....
}
}
4.8.2.使用密封类优化代码
目的是解决为满足编译器要求编写无用条件分支的情况。优化前代码示例如下:
package com.example.myapplication
import java.lang.IllegalArgumentException
//定义一个接口,表示某个操作的执行结果
interface Result
//Success类用于表示成功时的结果
class Success(val msg: String) : Result
//Failure类用于表示失败时的结果
class Failure(val error: String) : Result
//接受一个Result参数,通过判断result类型返回不同的结果
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error
//else这块是完全执行不到的,但缺少的话代码将无法编译过。
//如果新增一个UnKnown类并实现了Result接口,用于表示未知的执行结果,但忘记写分支,将会抛出异常使程序崩溃
else -> throw IllegalArgumentException()
}
解决方法是使用Kotlin密封类,密封类的关键字是sealed class。优化后的代码如下所示:
package com.example.myapplication
import java.lang.IllegalArgumentException
//密封类
sealed class Result
//继承类需要后面加上一对括号
class Success(val msg: String) : Result()
class Failure(val error: String) : Result()
//class unkonwn(val time: String) : Result()
//else条件已经没有了?为什么呢?
//when传入密封类时,Kotlin会自动检查该密封类有哪些子类,并强制要求你对每一个都需要处理(若不处理,编译不会通过)。即使没有else,也不会出现遗漏分支
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error
}