Android UI控件和布局

说明: 本文是郭霖《第一行代码-第3版》的读书笔记

4.1 如何编写程序界面

  • 编写XML,这是传统的方法

  • ConstraintLayout,Google推出的新方法,可以在可视化编辑器中拖动控件操作

这里使用的是编写XML方法

4.2 常用控件

4.2.1 TextView

修改activity_main.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="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal|center_vertical"
        android:textColor="#00ff00"
        android:textSize="24sp"
        android:text="This is TextView"/>

</LinearLayout>

android:id是控件唯一标识符

android:layout_widthandroid:layout_height是所有控件都具有的两个属性,可选值3个:

  • match_parent,当前控件大小和父布局一样
  • wrap_parent,当前控件大小刚好能包住里面的内容
  • 固定值,指定固定尺寸,单位是dp,像素独立单位,可以保证在不同分辨率的设备上显示效果尽可能相同

android:gravity,指定文字的对齐方式,TextView文字默认是左上角对齐的,所以可能看不出match_parent,这里指定了水平居中和垂直居中。

android:textColor, 指定文字的颜色

android:textSize, 指定文字的大小,单位是sp,这样当用户修改系统字体尺寸时,应用程序中的文字大小也会跟着修改。

4.2.2 Button

在activity_main中加入一个Button控件:

<?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="match_parent">
    
...
    
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="Button"/>
    
</LinearLayout>

android:textAllCaps指定为false可以保留指定的原始文字内容。因为Android默认将Button上的英文字母全部转成大写

在MainActivity中为Button的点击事件注册一个监听器:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 注意不通过findViewById来找到button而直接写,需要引入'android-kotlin-extensions'插件
        button.setOnClickListener { 
            //添加逻辑
        }
    }
}

上述方法是使用函数式API的方式来中注册监听器,也可以使用实现接口的方式来注册:

class MainActivity : AppCompatActivity(), View.OnClickListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 用这种方式的时候不要忘了设置ClickListener
        button.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when (p0?.id) {
            R.id.button -> {
                //TODO
            }
        }
    }
}

4.2.3 EditText

允许输入和编辑内容的控件,并可以在程序中对这些内容进行处理。

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

...

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

</LinearLayout>

android:hint是EditText无输入时的提示文本。

android:maxlines: 随着输入的增多,文本框被不断拉长,这是因为我们指定高度是wrap_content,指定max_lines能固定输入文本框的大小,超过两行时,文本就会向上滚动而不会拉伸EditText。

点击按钮获取EditText中的内容

override fun onClick(p0: View?) {
    when (p0?.id) {
        R.id.button -> {
            // 获取EditText中的内容
            val inputText = editText.text.toString()
            Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
        }
    }
}

4.2.4 ImageView

imageView通常是放在以drawable开头的目录下的,并且要带上具体的分辨率,主流的手机屏幕分辨率大多数是xxhdpi的,因此可以在res目录下新建一个drawable-xxhdpi目录。

修改activity_main.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="match_parent">

...

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

</LinearLayout>

android:src指定图片源

还可以在代码中动态更改图片:

override fun onClick(p0: View?) {
    when (p0?.id) {
        R.id.button -> {
            // 按下按钮更改图片源
            imageView.setImageResource(R.drawable.img_2)
        }
    }
}

4.2.5 ProgressBar

顾名思义,ProcessBar即进度条,用来显示进度

修改activity_main.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="match_parent">

...

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

</LinearLayout>

android:visibility设置控件的可见性,是所有控件都有的属性。可选值有

  • visible, 控件可见,默认属性

  • invisiable, 控件不可见,但仍占据原来的屏幕空间

  • gone , 控件不可见,而且不占用屏幕空间

也可以通过代码改变这个属性的值,使用的是setVisiability()方法,可选值有View.VISIBLEView.INVISIABLEGONE

style="?android:attr/progressBarStyleHorizontal"设置进度条样式为水平,此时静止在0,默认的是动态的环形不断旋转的进度条,当设置成水平进度条时,就可以设置一个最大值android:max,然后在代码内更改进度:

override fun onClick(p0: View?) {
    when (p0?.id) {
        R.id.button -> {
            // 点击一下Button进度加一截
            progressBar.progress += 10
        }
    }
}

4.2.6 AlertDialog

AlertDialog,顾名思义是警告对话框,可以弹出一个对话框,置于所有Activity之上,屏蔽掉其他Activity的交互,用于提醒一些非常重要的内容或者警告信息。

    override fun onClick(p0: View?) {
        when (p0?.id) {
            R.id.button -> {
                // AlertDialog
                // apply函数
                AlertDialog.Builder(this).apply {
                    setTitle("This is a AlertDialog")
                    setMessage("Something important")
                    setCancelable(true)
                    setPositiveButton("OK") { dialog, which ->
                        Log.d("AlertDialog", "Choose OK")
                    }
                    setNegativeButton("Cancel") { dialog, which ->
                        Log.d("AlertDialog", "Choose Cancel")
                    }
                    show()
                }
            }
        }
    }

代码中setPositiveButton("OK") {dialog, which ->},dialog,which这部分是高阶函数,也可以写成:

builder.setPositiveButton(android.R.string.yes) { _,_ ->
            Toast.makeText(applicationContext,
                    android.R.string.yes, Toast.LENGTH_SHORT).show()
        }

以下是Journal Dev中的讲解:https://www.journaldev.com/309/android-alert-dialog-using-kotlin

The function type is (DialogInterface, Int) -> Unit. DialogInterface is an instance of the Dialog and Int is the id of the button that is clicked.

In the above code, we’ve represented this function as a Higher Order Kotlin function. The dialog and which represents the two arguments.

4.3 详解三种布局

布局是一种可以放置很多控件的容器,除了放控件,还可以放布局,即布局的嵌套。

4.3.1 LinerLayout

LinearLayout是线性布局,可以通过android:orientation指定线性排列的方向。默认是android:orientation=horizontal.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

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

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

</LinearLayout>

注意: 如果LinearLayout的排列方向是horizontal,那么内部的控件就绝对不能将宽度指定为match_parent。同理,如果排列方向是vertical, 那么不能将高度指定为match_partent

android:layout_gravity: 用于指定控件在布局中的对齐方式,而android:gravity用于指定文字在控件中的对齐方式。需要注意:当LinearLayout的排列是horizontal时,只有垂直方向的对齐方式才会生效。

还有一个重要的属性是layout_weight,允许使用比例的方式指定控件的大小。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:hint="Type Something"/>

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

</LinearLayout>

因为我们指定了layout_weight,所以控件的宽度不再由layout_width指定,所以这里可以给0dpandroid:layout_weight相当于是权重。也可以让它更好看些,比如Button的大小给他wrap_content.

4.3.2 RelativeLayout

RelativeLayout是相对布局,通过相对定位的方式让控件出现在布局的任何位置。RelativeLayout的属性非常多。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

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

    <Button
        android:id="@+id/button3"
        android:text="Button 3"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button4"
        android:text="Button 4"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

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

</RelativeLayout>

感觉这个注意下写法就好了。但上面的例子都是相对于父布局进行定位的,当然也可以相对于控件定位。用相对控件定位的时候需要先声明相对的这个控件。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button3"
        android:text="Button 3"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

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

    <Button
        android:id="@+id/button2"
        android:text="Button 2"
        android:layout_above="@id/button3"
        android:layout_toRightOf="@id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button4"
        android:text="Button 4"
        android:layout_below="@id/button3"
        android:layout_toLeftOf="@id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button5"
        android:text="Button 5"
        android:layout_below="@id/button3"
        android:layout_toRightOf="@id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

就感觉这种RelativeLayout指定的是相对的大概的位置,没有那么精确。除此之外,还有android:layout_alignLeft这类的属性,表示左边缘和另一个控件的左边缘对齐,这种相对的一般都要指定两个才知道在哪吧。

4.3.3 FrameLayout

FrameLayout是帧布局,这种布局很简单,所有控件都会默认摆在布局的左上角。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:text="Button"/>

    <TextView
        android:id="@+id/textView"
        android:layout_gravity="left|top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is a TextView"/>

</FrameLayout>

除了默认效果之外,也可以使用layout_gravity来指定控件在布局中的对齐方式。

目前FrameLayout应用场景偏少,但后面在Fragment时还可以用到它。

4.4 创建自定义控件

首先介绍控件和布局的继承结构:

img

  • 所有控件都直接或间接继承自View,所有布局都继承自ViewGroup
  • View是Android中基本的UI组件,可以在屏幕上绘制一块矩形,并能响应这块区域的各种事件
  • ViewGroup是一种特殊的View,可以包含许多子View和子ViewGroup,是一个放置布局和控件的容器

4.4.1 引入布局

如果我们想让所有的Activity都有相同的标题栏,那么是否可以不重复写代码?当然是可以的,可以通过引入布局的方式来解决这个问题:在layout目录下新建一个title.xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    <!-- 因为这里是要做一个title,所以高度能包着内容就行了,不能设置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:textColor="#fff"
        android:text="Back"/>

    <TextView
        android:id="@+id/titleText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        <!-- 文字也要设置居中,不然的话矩形框居中了,但文字没居中就看不出来效果 -->
        android:gravity="center"
        android:layout_weight="1"
        android:text="Title Text"
        android:textColor="#fff"
        android:textSize="24sp"/>

    <Button
        android:id="@+id/titleEdit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="5dp"
        android:background="@drawable/edit_bg"
        android:text="Edit"
        android:textColor="#fff"/>
    
</LinearLayout>

android:backgroud: 用于为布局或者控件指定背景,可以是颜色,也可以是图片来填充

android:layout_margin: 指定控件在上下左右方向上的间距,也可以使用android:layout_marginLeft或者android:layout_marginTop等属性分别指定。

下面是如何使用这个标题,直接在activity_main.xml上修改如下:

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

    <include layout="@layout/title"/>

</LinearLayout>

只需要通过一行include语句引入标题栏布局就可以了。

最后别忘了隐藏掉系统自带的标题栏

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportActionBar?.hide()
    }
}

这里调用getSupportActionBar()方法获得ActionBar的实例,再调用hide()方法将标题栏隐藏起来。有关ActionBar的知识会在12章中讲到。

使用这种方式,不管有多少Activity要添加标题栏,只需要一行include语句就行了。

4.4.2 创建自定义控件

引入布局的技巧使得我们不需要重复编写布局,但如果有一些控件要能响应事件,那还是需要在每个Activity中写一次事件注册的代码。这个时候最好使用自定义控件的方法来解决:

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)
        val titleEdit: Button = findViewById(R.id.titleEdit)
        titleBack.setOnClickListener {
            val activity = context as Activity
            activity.finish()
        }
        titleEdit.setOnClickListener {
            Toast.makeText(context, "You clicked Edit.", Toast.LENGTH_SHORT).show()
        }
    }
}

首先需要声明contextattrs这两个参数,然后在init结构体内声明加载的逻辑:先通过LayoutInflater的from()方法构建出一个LayoutInflater对象,调用inflater()方法动态加载一个布局文件,第一个参数传入布局文件,第二个参数是给加载的布局添加一个父布局。

这里我们给返回和编辑按钮注册了点击事件。注意:TitleLayout接收的context参数实际上是一个Activity的实例,我们需要先将它转成Activity类型,再调用finish()方法销毁当前的Activity(不太理解这句话)。Kotlin中强制类型转换的关键字是as

然后就可以在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">

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

</LinearLayout>

4.5 最常用最难用的控件:ListView

手机屏幕大小有限,当有大量数据要展示的时候可以借助ListView,ListView允许用户通过滑动的方式将屏幕外的数据滚动到屏幕内,屏幕上原有的数据滚动出屏幕。比如QQ聊天记录。

4.5.1 ListView的简单用法

首先修改android_main.xml代码添加ListView:

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

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

</LinearLayout>

然后修改Main_Activity.kt中的代码如下:

class MainActivity : AppCompatActivity() {
    private val data = listOf("Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
            "Pineapple", "Strawberry", "Cherry", "Mango", "Apple", "Banana", "Orange", "Watermelon",
            "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango");

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

        val adpter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)
        listView.adapter = adpter
    }
}

首先我们有水果名字的集合需要显示,但是集合中的数据无法直接传给ListView,需要借助适配器来实现。Android中提供了很多适配器的实现类,这里使用的是ArrayAdapter.可以通过泛型指定要适配的数据类型。ArrayAdapter构造函数需要传入的参数是:Activity的实例、ListView子项布局的id,以及数据源。这里子项布局使用的是Android内置的布局文件android.R.layout_simple_list_item_1,里面只有一个TextView,可用于简单显示一段文本。最后调用ListView的setAdapter()方法,将构建好的适配器传进去。

4.5.2 定制ListView的界面

上面写的ListView太过简单。我们想让每一个子项显示图片和文字。

新建一个Fruit

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

然后添加一个子项布局,左边是Img,右侧是文字说明

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp">

    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_vertical"
        android:layout_margin="10dp"/>

    <TextView
        android:id="@+id/fruitName"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_vertical"
        android:layout_margin="10dp"/>

</LinearLayout>

接下来需要创建一个自定义的适配器,

//自定义的适配器继承自ArrayAdapter,重写了getView方法
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
    ArrayAdapter<Fruit>(activity, resourceId, data) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
        val fruit = getItem(position)  //获取当前项的Fruit实例
        if (fruit != null) {
            view.fruitName.text = fruit.name
            view.fruitImage.setImageResource(fruit.imageId)
        }
        return view
    }
}

getView()在每个子项被滚动到屏幕的时候会被调用,首先使用LayoutInflater加载我们传入的布局,这里第三个参数指定为False表示我们只让在父布局中声明的Layout属性生效,但不会为这个View添加父布局。因为一旦View有了父布局后,就无法添加到ListView中了。(不太理解)再通过getItem()方法得到当前项的Fruit实例,为其赋予ImageId和name,最后将布局返回。

在MainActivity中:

class MainActivity : AppCompatActivity() {
    
    private val fruitList = ArrayList<Fruit>()

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

        // 初始化水果的数据
        initFruits()
        val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
        listView.adapter = adapter
    }

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

初始化FruitList,然后构建我们的FruitAdapter适配器,传入ListView当中就可以了。

repeat()函数是Kotlin中非常常用的标准函数,允许传入一个数值n,然后将Lambda表达式中的内容执行n遍。

4.5.3 提升ListView的运行效率

ListView难用是因为有很多细节可以优化,比如运行效率。上面的FruitAdapter的getView()方法中,对每一个子项,都要调用getView()每次都要将布局加载一遍,但在getView()方法中有一个convertView参数,可以将之前加载好的布局缓存:

//修改FruitAdapter中的getView方法
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val view: View
    // 如果convertView为空则加载布局,否则使用缓存下来的布局
    if (convertView == null) {
        view = LayoutInflater.from(context).inflate(resourceId, parent, false)
    } else {
        view = convertView
  
    val fruit = getItem(position)  //获取当前项的Fruit实例
    if (fruit != null) {
        view.fruitName.text = fruit.name
        view.fruitImage.setImageResource(fruit.imageId)
    }
    return view
}

同时我们也不需要每次创建itemView的时候都去重复绑定控件,不能都通过view.findViewById()来查找,可以缓存到一个自定义的类ViewHolder中。

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

    inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view: View
        val viewHolder: ViewHolder
        if (convertView == null) {
            view = LayoutInflater.from(context).inflate(resourceId, parent, false)
            val fruitName: TextView = view.findViewById(R.id.fruitName)
            val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
            viewHolder = ViewHolder(fruitImage, fruitName)
            view.tag = viewHolder
        } else {
            view = convertView
            viewHolder = view.tag as ViewHolder
        }

        val fruit = getItem(position)  //获取当前项的Fruit实例
        if (fruit != null) {
            viewHolder.fruitName.text = fruit.name
            viewHolder.fruitImage.setImageResource(fruit.imageId)
        }
        return view
    }
}

ViewHolder类中用tag对ImageView和TextView进行缓存,然后在需要的时候再取出来。Kotlin中使用inner class来声明内部的类。

4.5.4 ListView的点击事件

override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       // 初始化水果的数据
       initFruits()
       val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
       listView.adapter = adapter
    	//为ListView注册监听器
       listView.setOnItemClickListener { parent, view, position, id ->
           val fruit = fruitList[position]
           Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
       }

没有用到的参数可以用占位符_代替

4.6 更强大的滚动控件:RecycleView

如果不使用一些技巧优化ListView,那么其效率会很差,扩展性也不够好,只能实现纵向滚动的效果。RecycleView不仅能实现和ListView同样的效果,而且也优化了其不足之处。目前Android官方更推荐使用RecycleView。

4.6.1 RecycleView的基本用法

RecycleView属于新增控件,需要在项目的build.gradle中添加RecycleView的依赖。

打开app/build.gradle文件,在dependencies上添加如下内容:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation "androidx.recyclerview:recyclerview:1.2.1"
    // For control over item selection of both touch and mouse driven selection
    // 现在的RecycleView好像要添加这一项
    implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

然后修改布局:

<?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="@+id/recycleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

接下来需要为RecycleView准备一个适配器,继承自RecycleView.Adapter,并指定泛型为自己内部定义的ViewHolder:

class FruitAdapter(val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

    inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
        val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
        val fruitName: TextView = view.findViewById(R.id.fruitName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        return ViewHolder(view)
    }

    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 {
        return fruitList.size
    }
}

上述是RecycleView适配器的标准写法。由于继承自RecycleView.Adapter,因此需要重写onCreateViewHolder()、onBindViewHolder()、getItemCount()这三个方法。

onCreateViewHolder(): 用于创建ViewHolder的实例,在这个方法中传入fruit_item的布局

onBindViewHolder(): 用于对子项数据赋值

getItemCount(): 告诉RecycleView共有多少项,直接传入List的长度

修改MainActivity的代码如下:

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

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

        initFruits()
        val layoutManager = LinearLayoutManager(this)
        recycleView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recycleView.adapter = adapter
        }

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

首先需要初始化所有的水果数据,

再创建一个LinearLayoutManager对象,并将它设置到RecycleView当中。LinearLayoutManager对象指定了RecycleView的布局方式。

接下来创建一个适配器实例,赋给RecycleView。这样RecycleView和数据之间就关联起来了。

4.6.2 实现横向滚动和瀑布流布局

横向滚动

首先需要改一下每一个子项的布局,从水平变为垂直,然后需要在MainActivity中改layoutManager为水平就可以了。

瀑布流布局

class MainActivity : AppCompatActivity() {

    private val fruitList = ArrayList<Fruit>()

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

        initFruits()
        // 第一个参数指定3列,第二个参数指定布局的排列方向
        val layoutManager = StaggeredGridLayoutManager(3,
                StaggeredGridLayoutManager.VERTICAL)
        recycleView.layoutManager = layoutManager
        
        // 网格布局
        // val layoutManager = GridLayoutManager(this, 3)
        // recycleView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recycleView.adapter = adapter
        }
}

感觉布局还是得自己去调,怎么好看怎么来,这里主要掌握API的用法。

4.6.3 RecycleView的点击事件

ListView提供了一个叫setOnItemClickListener()的方法,但是RecycleView并没有类似的方法,需要我们自己给子项具体的View去注册点击事件。(这其实是一件好事,尽管麻烦一些)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
    val viewHolder = ViewHolder(view)
    // 注册ViewHolder的点击事件
    viewHolder.itemView.setOnClickListener {
        val position = viewHolder.adapterPosition
        val fruit = fruitList[position]
        Toast.makeText(parent.context, "You clicked Item ${fruit.name}", Toast.LENGTH_SHORT).show()
    }
    viewHolder.fruitImage.setOnClickListener {
        val position = viewHolder.absoluteAdapterPosition
        val fruit = fruitList[position]
        Toast.makeText(parent.context, "You clicked Image ${fruit.name}", Toast.LENGTH_SHORT).show()
    }
    return viewHolder
}

可以看到,我们在onCreateViewHolder()函数内注册点击事件这里的itemView指的是子项的最外层布局。然后我们又找到子项的imageView,为其注册点击事件。

4.7 编写界面的最佳实践

4.7.1 制作9-Patch图片

这个九宫格的图片之前也碰到过,指的是一张图片哪里能被拉伸。

在AndroidStudio中能直接绘制9-Patch图片,左键点击黑色的就是能被拉伸的区域,按住Shift拖动可以擦除。

4.7.2 编写聊天界面

首先还是在build.gradle里添加recycleView的依赖,然后修改activity_main.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="match_parent"
    android:background="#d8e0e8">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycleView"
        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="Type something here"
            android:maxLines="2"/>

        <Button
            android:id="@+id/send"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send"/>

    </LinearLayout>

</LinearLayout>

添加了一个RecycleView,一个Button,一个EditText。

再创建一个Msg消息类:

class Msg(val content: String, val type: Int) {
    companion object {
        const val TYPE_RECEIVED = 0
        const val TYPE_SEND = 1
    }
}

Kotlin中定义常量的关键字是const,但是只有在单例类、companion object或顶层方法中才可以使用const关键字。

然后还需要编写RecycleView的子项布局:

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

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android: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>

关于padding和margin的区别:
在这里插入图片描述

为了使用RecycleView,需要定义适配器,适配器的写法和前面差不多,不过这里会判断是接收到的消息还是发送的消息,从而inflate不同的布局。

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

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

    inner class RightViewHold(view: View) : RecyclerView.ViewHolder(view) {
        val rightMsg: TextView = view.findViewById(R.id.rightMsg)
    }

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

    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 RightViewHold(view)
        }
    }

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

    override fun getItemCount(): Int {
        return msgList.size
    }
}

最后修改MainActivity中的代码:

class MainActivity : AppCompatActivity() {

    private val msgList = ArrayList<Msg>()
    private var adapter: MsgAdapter? = null

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

        //初始化消息列表
        initMsg()
        //加载布局管理器
        val layoutManager = LinearLayoutManager(this)
        recycleView.layoutManager = layoutManager
        //适配器
        adapter = MsgAdapter(msgList)
        recycleView.adapter = adapter
        //为Button添加响应
        send.setOnClickListener {
            val content = inputText.text.toString()
            if (content != null) {
                val msg = Msg(content, Msg.TYPE_SEND)
                msgList.add(msg)
                //刷新RecycleView中的显示
                adapter?.notifyItemInserted(msgList.size - 1)
                //定位到最后一行
                recycleView.scrollToPosition(msgList.size - 1)
                //清空输入框
                inputText.setText("")
            }
        }
    }

    private fun initMsg() {
        val msg1 = Msg("Hello smallSheep.", Msg.TYPE_RECEIVED)
        val msg2 = Msg("Hello, Who is that?", Msg.TYPE_SEND)
        val msg3 = Msg("This is BigZ, Nice to meet u.", Msg.TYPE_RECEIVED)
        msgList.add(msg1)
        msgList.add(msg2)
        msgList.add(msg3)
    }
}

这里有几个方法值得注意:

adapter.notifyItemInserted(): 用于通知列表有新数据插入,这样新增的消息才会在RecycleView中显示出来,或者也可以调用notifyDataSetChanged()方法,这样增删改数据都可以显示出来,但效率稍低。

recycleView.scrollToPosition: 将显示的数据定位到最后一行,保证新增的数据看得到。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值