《第一行代码》第三版之UI开发入门(五)

       本章我们将介绍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
}

 

你可能感兴趣的:(第一行代码第三版)