文章目录

  • 一、常用控件的使用方法
  • 1、TextView
  • 2、Button
  • 3、EditText
  • 4、ImageView
  • 5、ProgressBar
  • Android控件的可见性
  • 6、AlertDialog
  • 二、3种布局
  • 1、LinearLayout
  • 2、RelativeLayout
  • 3、FrameLayout
  • 四、自定义控件
  • 1、引入布局
  • 2、创建自定义控件
  • 五、ListView
  • 1、ListView的简单用法
  • 2、定制ListView的界面
  • 3、提升ListView的运行效率
  • 4、ListView的点击事件
  • 六、RecyclerView
  • 1、RecyclerView的基本用法
  • 2、实现横向滚动和瀑布流布局
  • (1)横向滚动
  • ListView与RecyclerView的区别
  • (2)瀑布流滚动
  • 3、RecyclerView的点击事件


一、常用控件的使用方法

1、TextView

TextView主要用于再界面上显示一段文本信息。

举个例子,首先创建一个UIWidgestTest项目。
activity_main.xml中的代码:

<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:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/textView"
        android:text="This is TextView'" />
    
</LinearLayout>

其中android:layout_widthandroid:layout_height分别指定控件的宽和高,Android中的所有控件都具有这两个属性,可选值有:match_parent(与父布局相同)、wrap_content(刚好包裹控件内的内容)、固定值(指定固定尺寸,单位一般为dp)。
上述代码就表示让TextView的宽度与父布局一样宽,高度正好包含其中内容。

TestView文件中文字是默认居左上角对其的,此时虽然TestView的宽度充满了整个屏幕,可是由于文字内容不够长,所以效果上看不出来。
现在修改TestView的对齐方式:

<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:text="This is TextView'"
        android:gravity="center"/>
</LinearLayout>

使用android:gravity来指定文字对齐方式,可选值有:top、bottom、start、end、center等,可以使用“|”来同时指定多个值。此处指定的center效果等同于center_vertical|center_horizontal,表示文字在垂直和水平方向都是居中对齐。

其他属性不一一介绍了。

2、Button

按钮,它可以配置的属性与TestView类似。

例如:

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

Android系统默认会将按钮上的英文字母全部转换成大写,此时按钮上显示的是"BUTTON"。
可以通过android:textAllCaps="false"来保留指定的原始文字内容。

下面给Button的点击实践注册一个监听器:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
        	//此处添加逻辑
        }
    }
}

此处利用了Java单抽象方法接口的特性,从而可以使用函数式API的写法来监听按钮的点击事件,这样每次点击按钮时就会执行Lambda表达式中的代码,只需在Lambda表达式中添加待实现的逻辑即可。
除此之外也可以是哟个实现接口的方式来注册:

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

这里让MainActivity实现了View.OnClickListener接口,并重写了Onclick()方法。
在调用button的setOnClickListener()方法时将MainActiity实例传进去即可。

3、EditText

EditText允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。例如QQ、微信等都在其上应用。

举个例子,在activity_main.xml中添加:

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

这个时候EditText已经在界面上显示出来了。

如果想要在输入框中添加系统提示文字,输入文字后就消失的话,添加android:hint属性即可,如下:

<EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入信息"/>

但现在还有一个问题,随着你输入的内容不断增多,EditText会不断拉长,因为EditText的高度设定的时warp_content总能包含住里面的内容。
要解决这个问题,我们可以使用android:maxLines属性来解决这个问题:

<EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入信息"
        android:maxLines="2"/>

这里指定了EditText最大行数为两行,当输入的内容超过两行,文本就会向上滚动,EditText则不会拉伸。

还可以结合使用EditText和Button来完成一些功能,比如点击按钮获取EditText中输入的内容:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            val inputText = editText.text.toString()
            Toast.makeText(this,inputText,Toast.LENGTH_SHORT).show()
        }
    }
}

此时在按钮的点击实践中调用EditText的getText()方法来获取输入的内容,再用toString()方法将内容转换成字符串,最后使用Toast显示出来。

4、ImageView

ImageView时用于界面展示图片的一个控件。
图片通常放在drawable开头的目录下,并且要带上具体的分辨率。
现在主流的手机屏幕分辨率大多是xxhdpi的,所以在res目录下新建一个drawable-xxhdpi存放图片。

假如此时在drawable-xxhdpi目录下添加了img_1.jpg和img_2.jpg,在activity_mainxml添加:

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

此处用android:src指定了一张图片,将宽和高都设置成warp_content,保证补灌图片的尺寸是多少,都可以完整展示。

可以在程序中通过代码动态地更改ImageView中的图片,如下:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            imageView.setImageResource(R.drawable.img_2)
        }
    }
}

在按钮的点击事件中,通过调用ImageView的setImageResource()方法将显示的图片改变成img_2。

5、ProgressBar

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

在activity_main.xml中添加:

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

此时运行程序,就会看到屏幕中间有一个圆形进度条正在旋转。

通过style属性可以修改进度条的样式,例如将其改成水平进度条:

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

此时将其设置成了水平进度条,最大值是100。

要想让这个进度条在加载完时消失,就要先了解Android控件的可见性。

Android控件的可见性

所有Android控件都具有这个属性,可以通过android:visibility属性进行指定,可选值有三种:visible(可见、默认值)、invisible(不可见、仍占空间)、gone(不可见、不占空间)。
可以通过代码来设置控件的可见性,使用setVisibility()方法,允许传入View.VISIBLEView.INVISIBLEView.GONE三种值。

举个例子,用按钮控制进度条的消失和出现:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            if(progressBar.visibility == VISIBLE){
                progressBar.visibility = View.GONE
            }else{
                progressBar.visibility = View.VISIBLE
            }
        }
    }

6、AlertDialog

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

举个例子:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button.setOnClickListener {
            AlertDialog.Builder(this).apply {
                setTitle("警告!")
                setMessage("重要文件警告!")
                setCancelable(false)
                setPositiveButton("OK"){ dialog, which ->
                }
                setNegativeButton("Cancel"){ dialog, which ->
                }
                show()
            }
        }
    }

首先通过AlertDialog.Builder构建一个对话框,这里使用了Kotlin标准函数中的apply函数,在apply函数中设置对话框的标题、内容、可否使用back键关闭对话框等属性,调用setPositiveButton()方法为对话框按钮设置点击事件,最后用show()方法将对话框显示出来。

二、3种布局

布局是一种可用于布置很多控件的容器,它可以按照一定的规律调整内部控件的位置。
布局内除了可以放置控件外,还可以放置布局,通过多层布局的嵌套,就可以完成一些比较复杂的界面实现。

1、LinearLayout

LinearLayout又称作线性布局,通过android:orientation属性指定排列方向,vertical是垂直排列,horizontal是水平排列,如果不指定的话默认是horizontal

注意,如果LinearLayout的排列方向是horizontal,内部控件就绝不能将宽度设定为match_parent,否则单一个控件就会将水平方向占满,其他控件就无可放置的位置了;同理,排列方式为vertical,内部控件就不能将高度指定为match_parent。

下面看android:layout_gravity属性,区别于android:gravity属性,前者是用于指定控件在布局中的对齐方式,后者是指定文字在控件中的布局方式。
二者可选值差不多,但需要注意的是,当排列方向为horizontal时,只有垂直方向的对齐方式才会生效;同理,排列方向为vertical时,只有水平方向的对齐方式才会生效。

接下来看android:layout_weight属性,它允许使用比例的方式来指定控件的大小。
例如:

<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:id="@+id/button1"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:text="Button1"/>
    <Button
        android:id="@+id/button2"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:text="Button2"/>
</LinearLayout>

两个按钮的layout_width的宽度都写成了0dp,这是一个比较规范的写法,因为使用了layout_weight属性之后,现在宽度就不由layout_width来决定了。

两个按钮的layout_weight属性都是1,现在界面展示的是两个按钮在水平方向对半分。
系统根据layout_weight分宽度的原理是,把所有的控件的layout_weight值相加求和,然后按照每个控件所占的比例为该控件layout_weight的值÷和。

当然也可以指定部分控件的layout_width值来实现更好的效果,例如:

<EditText
        android:id="@+id/input_message"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:hint="请输入信息"/>
<Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button2"/>

这样就是一个标准的对话框发送栏。

2、RelativeLayout

RelativeLayout,又称相对布局,它比LinearLayout排列规则不同,它可以通过下个对定位的方式让控件出现在布局的任何位置,也正是因此,RelativeLayout中的属性非常多。

举个例子:

<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/b1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:text="按钮1"/>
    <Button
        android:id="@+id/b2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:text="按钮2"/>
    <Button
        android:id="@+id/b3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="按钮3"/>
    <Button
        android:id="@+id/b4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true"
        android:text="按钮4"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:text="按钮5"/>
</RelativeLayout>
  • android:layout_alignParentLeft:相对于父布局的左边
  • android:layout_alignParentRight:相对于父布局的右边
  • android:layout_centerInParent:相对于父布局的中间
  • android:layout_alignParentTop:相对于父布局的上边
  • android:layout_alignParentBottom:相对于父布局的下边

所以这个代码最后显示的效果是四个角落以及一个正中心。

也可以根据其他控件进行定位,例如:

<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/b3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="按钮3"/>

    <Button
        android:id="@+id/b1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/b3"
        android:layout_toLeftOf="@id/b3"
        android:text="按钮1"/>
    <Button
        android:id="@+id/b2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/b3"
        android:layout_toRightOf="@id/b3"
        android:text="按钮2"/>

    <Button
        android:id="@+id/b4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/b3"
        android:layout_toLeftOf="@id/b3"
        android:text="按钮4"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/b3"
        android:layout_toRightOf="@id/b3"
        android:text="按钮5"/>
</RelativeLayout>

这个效果就是中间是按钮三,其他四个按钮都位于按钮三的四个角。

  • android:layout_above:位于相对控件上方
  • android:layout_below:位于相对控件下方
  • android:layout_toLeftOf:位于相对控件左侧
  • android:layout_toRightOf:位于相对控件右侧
  • android:layout_alignLeft:与相对控件左边缘对齐
  • android:layout_alignRight:与相对控件右边缘对齐
  • android:layout_alignTop:与相对控件上边缘对齐
  • android:layout_alignBottom:与相对控件下边缘对齐

当然,这些属性的值就是相对控件的id。

3、FrameLayout

又称帧布局,所有控件默认摆放在布局的左上角。

例如:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/t1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1234567890"/>
    <Button
        android:id="@+id/b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮"/>
</FrameLayout>

两个控件都会重叠在左上角,后添加的按钮在上。

除默认效果外,还可以使用layout_gravity属性指定控件在布局中的对齐方式:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/t1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:text="1234567890"/>
    <Button
        android:id="@+id/b7"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="按钮"/>
</FrameLayout>

此时指定TextView在FrameLayout在布局中左对齐,Botton右对齐。

四、自定义控件

我们所用的所有控件都是直接或者简洁继承自View的,所用的布局都是直接或者简洁继承自ViewGroup的。
View是Android中最基本的一种UI组件,它可以在屏幕中绘制一块矩形区域,并且能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础上又添加了格子特有的功能。
而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。

当系统自带的控件不能满足我们的需求时,我们就可以利用这样的继承结构来创建自定义控件。

1、引入布局

例如要创建一个自定义的标题栏,单独写的话只需要加入两个Button和一个TextView,然后在布局中摆好即可,如果要在每个Activity中加入这个标题栏,我们就可以使用引入布局的方式来解决这个问题。

首先在layout文件夹下新建一个title.xml

<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="Back"
            android:textColor="#fff" />

    <TextView
            android:id="@+id/titleText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:gravity="center"
            android:text="Title Text"
            android:textColor="#fff"
            android:textSize="24sp" />

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

</LinearLayout>

如代码所示,现在一个标题栏以及写好了,其中android:background用于为控件或布局准备一个背景,android:layout_margin属性可以用于指定控件的间距。

现在标题栏已经写好了,下面就是在程序中使用了。
下面修改activity_first.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>

最后在MainActivity中讲系统自带的标题栏隐藏:

package com.example.uicustomviews

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

class MainActivity : AppCompatActivity() {

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

通过getSupportActionBar()方法来获得ActionBar实例,再调用hide()方法将其隐藏。

2、创建自定义控件

通过自定义控件可以减轻很多工作量,比如你不必在每个Activity中为这些控件单独编写一次事件的注册代码。

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

package com.example.uicustomviews

import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.title.view.*
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
    init {
        LayoutInflater.from(context).inflate(R.layout.title, this)
        titleBack.setOnClickListener {
            val activity = context as Activity
            activity.finish()
        }
        titleEdit.setOnClickListener {
            Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
        }
    }
}

此时我们在主构造函数中声明了Context和AttributeSet两个参数,在布局中引入本控件时就会调用这个构造函数,然后再init结构体中需要对标题栏布局进行动态加载,借助 LayoutInflater 来实现,通过 LayoutInflater 的from()方法可以构建出一个 LayoutInflater 对象,然后调用inflate()方法就可以动态加载一个布局文件。
inflate()方法接收两个参数:第一个是要加载的布局文件的id;第二个参数是给加载好的布局再添加一个父布局,这里指定为TitleLayout,所以用this。

而且还为标题栏注册了两个点击事件,当点击返回按钮时销毁当前Activity,当点击编辑按钮的时候弹出一段文本。
注意,在TitleLayout中接收的context参数实际上是一个Activity实例,在返回按钮的点击事件中,我们要先将它转换成Activity类型,再调用finish销毁当期Activity。
Kotilin中类型强制转换用的是as关键字。

接下来再布局文件中添加这个自定义控件:

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

添加自定义组件时需要指明控件的完整类名,包名不可省略。

五、ListView

ListView控件就是用于,如果大量数据展示时候,允许用户滑动屏幕将屏幕外的数据滑入屏幕内,同时原有的内容滑出屏幕外。

1、ListView的简单用法

举个简单的例子:
activity_main.xml:

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

这里只是添加了一个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")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
        listView.adapter = adapter
    }
}

这里首先准备好数据,简单地使用一个data集合来进行测试。

集合中的数据需要借助适配器来传给ListView。
Android提供了很多适配器实现类,这里使用ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把适配的数据传入。ArrayAdapter有多个构造函数的重载,在使用时挑选最适合实际情况的一种。这里提供的数据都是字符串,所以将泛型指定为String,然后再ArrayAdapter的构造函数中以此传入Activity的实例、ListView子项布局的id、以及数据源。注意,这里使用了android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。

然后调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,建立ListView和数据之间的关联。

2、定制ListView的界面

首先准备好一组图片资源,分别对应data中提供的每一种水果。

接着定义一个Fruit实体类,作为ListView适配器的适配类型:

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

然后为ListView的子项指定一个自定义布局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:gravity="center_vertical"
        android:layout_marginLeft="10dp"/>
</LinearLayout>

在这个布局中,定义了一个ImageView用于显示图片,一个TextView用于显示名称,且都在垂直方向上居中显示。

接下来创建一个自定义的适配器,继承自ArrayAdapter,并且将泛型指定为Fruit类:

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

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view: View = LayoutInflater.from(context).inflate(resourceId, parent, false)
        val fruitImage: ImageView = view.fruitImage
        val fruitName: TextView = view.fruitName
        val fruit = getItem(position)
        if (fruit != null) {
            fruitImage.setImageResource(fruit.imageId)
            fruitName.text = fruit.name
        }
        return view
    }
}

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

在getView()中,首先使用LayoutInflater来为这个子项加载我们传入的布局,inflate()接收三个参数:第一个是要加载的布局文件的id;第二个参数是给加载好的布局再添加一个父布局;第三个参数指定成false,表示只让我门在父布局中声明的layout生效,但不会为这个View添加父布局。这是ListView的一个标准写法,因为一旦View有了父布局,就不能再添加到ListView中了。

接下来调用View的findViewById()方法分别获取到ImageView和TextView的实例,然后通过getItem()方法得到当项的Fruit实例,并分别调用他们的setImageResource()和setText()方法设置显示的图片和文字,最后将布局返回。

最后修改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))
        }
    }
}

首先初始化所有水果数据,然后在onCreate()方法中创建一个FruitAdapter对象,并且将它作为适配器传给ListView,这样定制的ListView界面的任务就完成了。

3、提升ListView的运行效率

ListView控件有很多细节可以优化,其中运行效率是很重要的一点。

目前所写的ListView的运行效率是很低的,因为在FruitAdapter的getView()中,每次都将布局重新加载了一遍,当ListView快速滚动的时候,这就会成为性能的瓶颈。

在getView()方法中还有一个convertView参数,这个参数用于将加载好的布局进行缓存,以便之后进行重用,以此来优化性能。

修改FruitAdapter中的代码:

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val view: View
        if(convertView == null){
            view = LayoutInflater.from(context).inflate(resourceId, parent, false)
        }
        else{
            view = convertView
        }
        val fruitImage: ImageView = view.fruitImage
        val fruitName: TextView = view.fruitName
        val fruit = getItem(position)
        if (fruit != null) {
            fruitImage.setImageResource(fruit.imageId)
            fruitName.text = fruit.name
        }
        return view
    }

只需要在getView()中加入判断即可利用convertView优化性能。

继续看代码,每次在getView()方法中仍会调用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 fruitImage: ImageView = view.fruitImage
            val fruitName: TextView = view.fruitName
            viewHolder = ViewHolder(fruitImage,fruitName)
            view.tag = viewHolder
        }
        else{
            view = convertView
            viewHolder = view.tag as ViewHolder
        }

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

这里新增了一个内部类ViewHolder,用于对Image和Textview的控件实例进行缓存。
Kotlin通过inner class关键字来定义内部类。
当convertView为null的时候,创建一个ViewHolder对象,并且将空间实力存放在ViewHolder中,当convertView不为null的时候,则调用View的getTag()方法将ViewHolder重新取出。
这样就将所有控件的实例都缓存在了ViewHolder中,就可以避免每次都通过findViewById()来获取控件实例了。

4、ListView的点击事件

修改MainActivity中的代码:

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.setOnItemClickListener{ parent,view,position,id ->
            val fruit = fruitList[position]
            Toast.makeText(this,fruit.name,Toast.LENGTH_SHORT).show()
        }
    }

这里使用setOnItemClickListener()方法为ListView注册了一个监听器,用户带年纪了ListView中的任意一个子项,就会回调Lambda表达式中,通过position参数判断用户点击的是哪个子项,然后获取到相应的水果。
上述代码中Lambad表达式中声明了四个参数,但只使用了position这一个参数,那么其他的参数可以用下划线来代替。

六、RecyclerView

1、RecyclerView的基本用法

首先在项目的app/build.gradle文件d dependencies
闭包中导入对RecyclerView库的依赖:

implementation 'androidx.recyclerview:recyclerview:1.0.0'

下面利用RecyclerView写以下上文中的例子:

首先修改activity_main.xml:

<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/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

在布局中加入RecyclerView,并为其指定一个id,宽度高度与父布局相同,让它占满整个空间,由于RecyclerView不是内置在系统SDK中的,所以需要把完整包名写出来。

下面为RecyclerView写一个适配器,新建FruitAdapter类,让其继承RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder,其中ViewHolder是FruitAdapter中的一个内部类:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
    inner class ViewHolder(view: View):RecyclerView.ViewHolder(view){
        val fruitImage: ImageView = view.fruitImage
        val fruitName: TextView = view.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() = fruitList.size
}

其中首先定义了一个内部类,ViewHolder,继承自RecycleView.ViewHolder。然后在其主构造函数中传入一个View参数,这个参数通常是RecyclerView子项的最外层布局,可以通过findViewById()方法来获取布局中的实例。

由于FruitAdapter继承自RecyclerView.Adapter,那么必须重写上文代码中的三个方法。
onCreateViewHlder()用于创建ViewHolder实例,并把加载出来的布局传入构造函数中,最后将ViewHolder的实例返回。
onBindViewHolder()方法用于对RecycleView的子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里通过position参数得到当前项的Fruit实例,然后将数据设置到ViewHolder中即可。
getItemCount()方法用于告诉RecyclerView一共有多少子项。

下面修改MainAcivity中的代码:

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)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recyclerView.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对象,并将其设置到RecyclerView中,LayoutManager用于指定RecyclerView的布局方式,这里使用了LinearLayoutManager意思是使用线性布局,可以实现类似ListView的效果。接下来将水果数据传入自定义的适配器中,然后完成适配器设置,关联数据即可。

2、实现横向滚动和瀑布流布局

(1)横向滚动

首先对fruit_item布局进行修改,使其适用与横向滚动的场景,即将其中的元素改为垂直排列:

<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_marginTop="10dp"/>

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

下面修改MainActiivity:

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)
        layoutManager.orientation = LinearLayoutManager.HORIZONTAL
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recyclerView.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的setOrientation()方法设置布局排列方向横向排列。

这样就可以实现横向滚动了。

ListView与RecyclerView的区别

ListView的布局排列是由自身去挂你的,而RecyclerView是交给了LayoutManager。LayoutManager制定了一套可扩展的布局排列接口,子类只需要按照这个接口的规范来实现就能制定出各种不同排列方式的布局了。

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

(2)瀑布流滚动

首先来修改fruit_item.xml中的代码:

<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="5dp">
    <ImageView
        android:id="@+id/fruitImage"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="10dp"/>

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

此处将LinearLayout的宽度改成了match_parent,因为瀑布流宽度应该是根据布局的列数自动适配的。
另外将TextView的对齐方式改为了左对齐。

接着修改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 = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
        recyclerView.layoutManager = layoutManager
        val adapter = FruitAdapter(fruitList)
        recyclerView.adapter = adapter
    }

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

    private fun getRandomLengthName(name: String): String {
        val length = Random().nextInt(20) + 1
        val builder = StringBuilder()
        for (i in 0 until length) {
            builder.append(name)
        }
        return builder.toString()
    }
}

首先在onCreate()方法中创建了一个StaggeredGridLayoutManager的实例,它的构造函数接收两个参数:第一个参数用于指定布局的列数;第二个参数用于指定布局的排列方向,上文代码中指定了布局纵向排列。最后把创建好的实例设置到RecyclerView中即可。
getRandomLengthName()方法用于设置文本的长度,便于直观地感受瀑布流的界面。

3、RecyclerView的点击事件

不同于ListView,RecyclerView中没有提供类似于setOnItemClickListener()这样的注册监听器的方法,而是需要自己给子项具体的View去注册点击事件。

修改FruitAdapter中的代码:

class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
    inner class ViewHolder(view: View):RecyclerView.ViewHolder(view){
        val fruitImage: ImageView = view.fruitImage
        val fruitName: TextView = view.fruitName
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        val viewHolder = ViewHolder(view)
        viewHolder.itemView.setOnClickListener{
            val position = viewHolder.adapterPosition
            val fruit = fruitList[position]
            Toast.makeText(parent.context,"你点击了水果 ${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() = fruitList.size

此时为最外层和ImageView都注册了点击事件,itemView表示的就是最外层布局。