UI开发

常用控件的使用方法

TextView

下面我们就来看一看TextView的更多用法,将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:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is TextView"/>
</LinearLayout>

Android中所有的控件都具有这两个属 性,可选值有3种:match_parent、wrap_content和固定值。固 定值表示表示给控件指定一个固定的尺寸,单位一般用dp,这是一种屏幕密度无关的尺寸单 位,可以保证在不同分辨率的手机上显示效果尽可能地一致,如50 dp就是一个有效的固定值。

修改TextView的文字对 齐方式,如下所示

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

可选值有top、bottom、start、 end、center

另外,我们还可以对TextView中文字的颜色和大小进行修改,如下所示

<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"
android:textColor="#00ff00"
android:textSize="24sp"
android:text="This is TextView"/>
</LinearLayout>

Button

它可配置的属性和TextView是差不多的,我们可以在 activity_main.xml中这样加入Button

<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:text="Button" />
</LinearLayout>

如果你很细心的话,可能会发现我们在XML中指定按钮上的文字明明是Button,可是为什么界 面上显示的却是BUTTON呢?这是因为Android系统默认会将按钮上的英文字母全部转换成大 写,可能是认为按钮上的内容都比较重要吧。如果这不是你想要的效果,可以在XML中添加 android:textAllCaps=”false”这个属性

接下来我们可以在MainActivity中为Button的点击事件注册一个监听器,如下所示

class MainActivity:AppCompatActivity(){
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentVeiw(R.layout.activity_main)
button.setOnClickListener{

}
}
}

除了使用函数式API的方式来注册监听器,也可以使用实现接口的方式来进行注册,代码如下所示

class MainActivity:AppCompatActivity(),View.OnClckListener{
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentVew(R.layout.activity_main)
button.setOnClickListener(this)
}

override fun onClick(v:View?){
whne(v?.id){
R.id.button->{

}
}
}
}

这里我们让MainActivity实现了View.OnClickListener接口,并重写了onClick()方法, 然后在调用button的setOnClickListener()方法时将MainActivity的实例传了进去。这样 每当点击按钮时,就会执行onClick()方法中的代码了

EditText

EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内 容,并可以在程序中对这些内容进行处理。那我们来看一看如何 在界面上加入EditText吧,修改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">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

你可能平时会留意到,一些做得比较人性化的软件会在输入框里显示一些提示性的文字,一旦 用户输入了任何内容,这些提示性的文字就会消失。这种提示功能在Android里是非常容易实现 的,我们甚至不需要做任何逻辑控制,因为系统已经帮我们都处理好了。修改 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">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
/>
</LinearLayout>

不过,随着输入的内容不断增多,EditText会被不断地拉长。这是由于EditText的高度指定的是 wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得非 常难看。我们可以使用android:maxLines属性来解决这个问题,修改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">
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2"
/>
</LinearLayout>

点击按钮以弹出EditText中的消息

class MainActivity:AppCompatActivity(),View.OnclickListener{
lateinit var editText:EditText
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button:Button=findViewById(R.id.button)
editText=findViewById(R.id.editText)
button.setOnClickListener(this)
}
override fun onClick(v:View?){
when(v?.id){
R.id.button->{
var inputText = editText.text.toString()
Toast.makeText(this,inputText,Toast.LENGTH_SHORT).show()
}
}
}
}

ImageView

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

接下来修改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:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="This is TextView"
android:textColor="#00ff00"
android:textSize="24sp"/>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button"
android:textAllCaps="false"/>
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drwaable/img_1"/>

</LinearLayout>

用android:src属性给ImageView指定了一张图片。由于图片的宽和高都 是未知的,所以将ImageView的宽和高都设定为wrap_content,这样就保证了不管图片的尺 寸是多少,都可以完整地展示出来

我们还可以在程序中通过代码动态地更改ImageView中的图片,修改MainActivity的代码,如 下所示

class MainActivity : AppCompatActivity(),View.OnClickListener{
....
override fun onClick(v:View?){
when(v?.id){
R.id.button->{
imageViw.setImageResource(R.drwaable.img_2)
}
}
}
}

ProgressBar

ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。它的用法也 非常简单,修改activity_main.xml中的代码,如下所示

<LinerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientatio="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgerssBar android:id="@id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinerLayout>

android:visibility进行指定,可选值有3种:visible、invisible和gone。visible 表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。 invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明 状态了。gone则表示控件不仅不可见,而且不再占用任何屏幕空间。我们可以通过代码来设置 控件的可见性,使用的是setVisibility()方法,允许传入View.VISIBLE、 View.INVISIBLE和View.GONE这3种值

class MainActivity:AppCompatActivity(),View.OnClickListener{
...
override fun onClick(v:View?){
when(v?.id){
R.id.button->{
if(progressBar.visibility == View.VISIBLE){
progerssBar.visibility = View.GONE
}else{
progressBar.visibility = View.VISIBLE
}
}
}
}
}

另外,我们还可以给ProgressBar指定不同的样式,刚刚是圆形进度条,通过style属性可以将 它指定成水平进度条,修改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">

...

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

</LinearLayout>

指定成水平进度条后,我们还可以通过android:max属性给进度条设置一个最大值,然后在代 码中动态地更改进度条的进度。修改MainActivity中的代码,如下所示

class MainActivity : AppCompatActivity(), View.OnClickListener { 

...

override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
progressBar.progress = progressBar.progress + 10
}
}
}
}

AlertDialog

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

比如为了防止用户误删重要内容,在删除前弹出一个确认对话框。下面我们来学习一下它的用 法,修改MainActivity中的代码,如下所示

class MainActivity:AppCOmpatActivity(),View.onClickListener{
...
override fun onClick(v:View?){
when(v?.id){
R.id.button->{
AlertDialog.Builder(this).apply{
setTitle("This is Dialog")
setMessage("Something important.")
setCancelable(false)
setPostiviButton("OK"){dialog,which->}
setNegativeButton("Cancel"){dialog,which->}
show()
}
}
}
}
}

详解3种基本布局

LinearLayot

下面来看android:layout_gravity属性,它和我们上一节中学到的android:gravity属 性看起来有些相似,这两个属性有什么区别呢?其实从名字就可以看出,android:gravity 用于指定文字在控件中的对齐方式,而android:layout_gravity用于指定控件在布局中的 对齐方式。android:layout_gravity的可选值和android:gravity差不多,但是需要注 意,当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因 为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无 法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有 水平方向上的对齐方式才会生效。修改activity_main.xml中的代码,如下所示:

<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="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="Button 1" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button 2" />
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button 3" />
</LinearLayout>

接下来我们学习LinearLayout中的另一个重要属性——android:layout_weight。这个属 性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要 的作用。比如,我们正在编写一个消息发送界面,需要一个文本编辑框和一个发送按钮,修改 activity_main.xml中的代码,如下所示:

<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_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"
/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send"
/>
</LinearLayout>

你会发现,这里竟然将EditText和Button的宽度都指定成了0 dp,这样文本编辑框和按钮还能 显示出来吗?不用担心,由于我们使用了android:layout_weight属性,此时控件的宽度就 不应该再由android:layout_width来决定了,这里指定成0 dp是一种比较规范的写法。

然后在EditText和Button里将android:layout_weight属性的值指定为1,这表示EditText 和Button将在水平方向平分宽度

为什么将android:layout_weight属性的值同时指定为1就会平分屏幕宽度呢?其实原理很 简单,系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值, 然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值。因此如 果想让EditText占据屏幕宽度的3/5,Button占据屏幕宽度的2/5,只需要将EditText的 layout_ weight改成3,Button的layout_weight改成2就可以了

<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_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"
/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
/>
</LinearLayout>

这里我们仅指定了EditText的android:layout_weight属性,并将Button的宽度改回了 wrap_content。这表示Button的宽度仍然按照wrap_content来计算,而EditText则会占满 屏幕所有的剩余空间。使用这种方式编写的界面,不仅可以适配各种屏幕,而且看起来也更加 舒服。

RelativeLayout

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

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

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

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

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

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

</RelativeLayout>

以上代码不需要做过多解释,因为实在是太好理解了。我们让Button 1和父布局的左上角对 齐,Button 2和父布局的右上角对齐,Button 3居中显示,Button 4和父布局的左下角对齐, Button 5和父布局的右下角对齐。虽然android:layout_alignParentLeft、 android:layout_alignParentTop、android:layout_alignParentRight、 android:layout_alignParentBottom、android:layout_centerInParent这几个属 性我们之前都没接触过,可是它们的名字已经完全说明了它们的作用。

上面例子中的每个控件都是相对于父布局进行定位的,那控件可不可以相对于控件进行定位 呢?当然是可以的,修改activity_main.xml中的代码,如下所示:

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

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

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

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

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

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

</RelativeLayout>

RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout_alignLeft 表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignRight表示 让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有android:layout_alignTop和 android:layout_alignBottom,道理都是一样的

FrameLayout

FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景少了很 多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角。让我们通过例子来看一看吧,修改activity_main.xml中的代码,如下所示:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
/>
</FrameLayout>

可以看到,文字和按钮都位于布局的左上角。由于Button是在TextView之后添加的,因此按钮 压在了文字的上面

当然,除了这种默认效果之外,我们还可以使用layout_gravity属性来指定控件在布局中的 对齐方式,这和LinearLayout中的用法是相似的。修改activity_main.xml中的代码,如下所 示

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="This is TextView"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Button"
/>
</FrameLayout>

系统控件不够用?创建自定义控件

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

引入布局

如果在每个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>

现在标题栏布局已经编写完成了,剩下的就是如何在程序中使用这个标题栏了,修改 activity_main.xml中的代码,如下所示

<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中将系统自带的标题栏隐藏掉,代码如下所示

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

创建自定义控件

引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响 应事件,我们还是需要在每个Activity中为这些控件单独编写一次事件注册的代码。比如标题栏 中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前 Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,无疑会增加很 多重复代码,这种情况最好是使用自定义控件的方式来解决

新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件,代码如下所示

class TitleLayout(context:Context,attrs:AttributeSet):LinerLayout(context,attrs){
init{
LayoutInflater.from(context).inflate(R.layout.title,this)
}
}

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

现在自定义控件已经创建好了,接下来我们需要在布局文件中添加这个自定义控件,修改 activity_main.xml中的代码,如下所示:

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

添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,我们 需要指明控件的完整类名,包名在这里是不可以省略的。

下面我们尝试为标题栏中的按钮注册点击事件,修改TitleLayout中的代码,如下所示

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

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

ListView

ListView的简单使用方法

首先新建一个ListViewTest项目,并让Android Studio自动帮我们创建好Activity。然后修改 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>

接下来修改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
}

集合中的数据是无法直接传递给ListView的,我们还需要借助适配器来完成。Android 中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指 定要适配的数据类型,然后在构造函数中把要适配的数据传入。ArrayAdapter有多个构造函数 的重载,你应该根据实际情况选择最合适的一种。由于我们这里提供的数据都是字符串,因此 将ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入Activity 的实例、ListView子项布局的id,以及数据源。注意,我们使用了 android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个 Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器 对象就构建好了

最后,还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样 ListView和数据之间的关联就建立完成了。

定制ListView 的界面

接着定义一个实体类,作为ListView适配器的适配类型。新建Fruit类,代码如下所示:

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

}

Fruit类中只有两个字段:name表示水果的名字,imageId表示水果对应图片的资源id。

然后需要为ListView的子项指定一个我们自定义的布局,在layout目录下新建 fruit_item.xml,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>

在这个布局中,我们定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于 显示水果的名称,并让ImageView和TextView都在垂直方向上居中显示。

接下来需要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为 Fruit类。新建类FruitAdapter,代码如下所示:

class FruitAdapter(activity:Activity:,val resourceId:int,data:List<Fruit>):
ArrayAdapter<Fruint>(activity,resuorceId,data){
override fun getView(position:Int,convertView:View?,parent:ViewGroup):View{
val view = LayoutInflater.from(context).inflate(resourceId,parent,false)
val fruitImage:ImageView = view.findViewById(R.id.fruitImage)
val fruitName:TextView=view.findViewById(R.id.fruitName)
var fruit = getItem(position)
if(fruit!=null){
fruitImage.setImageResource(fruit.imageId);
fruitName.text=fruit.name
}
return view
}
}

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

在getView()方法中,首先使用LayoutInflater来为这个子项加载我们传入的布局。 LayoutInflater的inflate()方法接收3个参数,前两个参数我们已经知道是什么意思了, 第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不会为这个 View添加父布局。因为一旦View有了父布局之后,它就不能再添加到ListView中了。如果你现 在还不能理解这段话的含义,也没关系,只需要知道这是ListView中的标准写法就可以了,当 你以后对View理解得更加深刻的时候,再来读这段话就没有问题了

我们继续往下看,接下来调用View的findViewById()方法分别获取到ImageView和 TextView的实例,然后通过getItem()方法得到当前项的Fruit实例,并分别调用它们的 setImageResource()和setText()方法设置显示的图片和文字,最后将布局返回,这样我 们自定义的适配器就完成了。

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

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

override fun onCreate(savedInstanceState:BUndle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
val adapter = FruitAdaper(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))
}
}

可以看到,这里添加了一个initFruits()方法,用于初始化所有的水果数据。在Fruit类的 构造函数中将水果的名字和对应的图片id传入,然后把创建好的对象添加到水果列表中。另外, 我们使用了一个repeat函数将所有的水果数据添加了两遍,这是因为如果只添加一遍的话,数 据量还不足以充满整个屏幕。repeat函数是Kotlin中另外一个非常常用的标准函数,它允许你 传入一个数值n,然后会把Lambda表达式中的内容执行n遍。接着在onCreate()方法中创建 了FruitAdapter对象,并将它作为适配器传递给ListView,这样定制ListView界面的任务就 完成了

提升ListView 的运行效率

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

仔细观察你会发现,getView()方法中还有一个convertView参数,这个参数用于将之前加 载好的布局进行缓存,以便之后进行重用,我们可以借助这个参数来进行性能优化。修改 FruitAdapter中的代码,如下所示:

class FruitAdapter(activity:Activity,val resuorceId:Int,data:List<Fruit>) :
ArrayAdapter<Fruit>(activity,resourceId,data){
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
}
var fruitImage:ImageView = view.findViewById(R.id.fruitImage)
var fruitName:TextView = view.findVIew
fruit = getItem(position)
if(fruit!=null){
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}

可以看到,现在我们在getView()方法中进行了判断:如果convertView为null,则使用 LayoutInflater去加载布局;如果不为null,则直接对convertView进行重用。这样就大 大提高了ListView的运行效率,在快速滚动的时候可以表现出更好的性能。

不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是 每次在getView()方法中仍然会调用View的findViewById()方法来获取一次控件的实例。 我们可以借助一个ViewHolder来对这部分性能进行优化,修改FruitAdapter中的代码,如 下所示

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.findViewById(R.id.fruitImage)
val fruitName :TextVIew = view.findViewById(R.id.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,用于对ImageView和TextView的控件实例进行缓存, Kotlin中使用inner class关键字来定义内部类。当convertView为null的时候,创建一个 ViewHolder对象,并将控件的实例存放在ViewHolder里,然后调用View的setTag()方 法,将ViewHolder对象存储在View中。当convertView不为null的时候,则调用View的 getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了ViewHolder里, 就没有必要每次都通过findViewById()方法来获取控件实例了。

ListView的点击事件

话说回来,ListView的滚动毕竟只是满足了我们视觉上的效果,可是如果ListView中的子项不 能点击的话,这个控件就没有什么实际的用途了。因此,本小节我们就来学习一下ListView如 何才能响应用户的点击事件

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

class MainActivity:AppComPatActivity(){
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(savedInstanceState)
initFruits()
val adapter = FruitAdappter(this,R.layout.fruit_item,fruitLis)
listView.adapter = adapter
listView.setOnItemClickListener{parent,view,position,id->
vla fruit = fruitList[positon]
Toast.maktText(this,fruit.name,Toast.LENGTH_SHORT).show()}
}
}

可以看到,我们使用setOnItemClickListener()方法为ListView注册了一个监听器,当用 户点击了ListView中的任何一个子项时,就会回调到Lambda表达式中。这里我们可以通过 position参数判断用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果的 名字显示出来

另外你会发现,虽然这里我们必须在Lambda表达式中声明4个参数,但实际上却只用到了 position这一个参数而已。针对这种情况,Kotlin允许我们将没有用到的参数使用下划线来替 代,因此下面这种写法也是合法且更加推荐的

listView.setOnItemClickListener { _, _, position, _ -> 
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}

RecyclerView

Android提供了一个更强大的滚动控件——RecyclerView。它可以说是一个增强版的 ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView存在的各种不足之 处。

RecyclerView的基本用法

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

dependencies { 
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

接下来修改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准备一个适配器,新建FruitAdapter类,让这个适配器继承自 RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。其中, ViewHolder是我们在FruitAdapter中定义的一个内部类,代码如下所示

class FruitAdapter(val fruitList:List<Fruit>):
RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
inner class ViewHolder(view:View):RecycleView.ViewHolder(view){
val fruitImage:ImageIvew = view.findViewById(R.id.fruitImage)
val fruitName:TextView = view.findViewById(R.id.fruirName)
}

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
}

这是RecyclerView适配器标准的写法,虽然看上去好像多了好几个方法,但其实它比ListView 的适配器要更容易理解。这里我们首先定义了一个内部类ViewHolder,它要继承自 RecyclerView.ViewHolder。然后ViewHolder的主构造函数中要传入一个View参数,这 个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewById()方 法来获取布局中ImageView和TextView的实例了。

FruitAdapter中也有一个主构造函数,它用于把要展示的数据源传进来,我们后续的操作都 将在这个数据源的基础上进行。 继续往下看,由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写 onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法。 onCreateViewHolder()方法是用于创建ViewHolder实例的,我们在这个方法中将 fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入构造 函数当中,最后将ViewHolder的实例返回。onBindViewHolder()方法用于对 RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里我们通过 position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和 TextView当中即可。getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有 多少子项,直接返回数据源的长度就可以了

适配器准备好了之后,我们就可以开始使用RecyclerView了,修改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 = LinerLayoutManager(this)
recycylerView.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))
}
}


}

可以看到,这里使用了一个同样的initFruits()方法,用于初始化所有的水果数据。接着在 onCreate()方法中先创建了一个LinearLayoutManager对象,并将它设置到 RecyclerView当中。LayoutManager用于指定RecyclerView的布局方式,这里使用的 LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。接下来我们创 建了FruitAdapter的实例,并将水果数据传入FruitAdapter的构造函数中,最后调用 RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关 联就建立完成了。

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

首先要对fruit_item布局进行修改,因为目前这个布局里面的元素是水平排列的,适用于纵 向滚动的场景,而如果我们要实现横向滚动的话,应该把fruit_item里的元素改成垂直排列 才比较合理。修改fruit_item.xml中的代码,如下所示:

<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_gravity="center_horizontal"
android:layout_marginTop="10dp" />

</LinearLayout>

可以看到,我们将LinearLayout改成垂直方向排列,并把宽度设为80 dp。这里将宽度指定为 固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView的子项 就会有长有短,非常不美观,而如果用match_parent的话,就会导致宽度过长,一个子项占 满整个屏幕

然后我们将ImageView和TextView都设置成了在布局中水平居中,并且使用 layout_marginTop属性让文字和图片之间保持一定距离

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

override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
val layoutManager = LinerLayoutMananger(this)
layoutManager.orientation = LinerLayoutManager.HORIZONTAL
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
...

}
}

MainActivity中只加入了一行代码,调用LinearLayoutManager的setOrientation()方法 设置布局的排列方向。默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL 表示让布局横行排列,这样RecyclerView就可以横向滚动了。

你可以用手指在水平方向上滑动来查看屏幕外的数据。 为什么ListView很难或者根本无法实现的效果在RecyclerView上这么轻松就实现了呢?这主要 得益于RecyclerView出色的设计。ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager。LayoutManager制定了一套可扩展的布局排列接口, 子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了

除了LinearLayoutManager之外,RecyclerView还给我们提供了GridLayoutManager和 StaggeredGridLayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于 实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。这里我们来实现 一下效果更加炫酷的瀑布流布局,网格布局就作为课后习题,交给你自己来研究了。

<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_gravity="left"
android:layout_marginTop="10dp" />
</LinearLayout>

这里做了几处小的调整,首先将LinearLayout的宽度由80 dp改成了match_parent,因为瀑 布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。其次我们使用了 layout_margin属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。最后 还将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 layoutMaanager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManagre.VERTICAL)
recyclerView.layoutManager = layoutManager
val adapter FruitAdatpe(fruitList)
recyclerView.adapter = adapter
}

private fun initFruits(){
repeat(2) {
fruitList.add(Fruit(getRandomLengthString("Apple"),
R.drawable.apple_pic))
fruitList.add(Fruit(getRandomLengthString("Banana"),
R.drawable.banana_pic))
fruitList.add(Fruit(getRandomLengthString("Orange"),
R.drawable.orange_pic))
fruitList.add(Fruit(getRandomLengthString("Watermelon"),
R.drawable.watermelon_pic))
fruitList.add(Fruit(getRandomLengthString("Pear"),
R.drawable.pear_pic))
fruitList.add(Fruit(getRandomLengthString("Grape"),
R.drawable.grape_pic))
fruitList.add(Fruit(getRandomLengthString("Pineapple"),
R.drawable.pineapple_pic))
fruitList.add(Fruit(getRandomLengthString("Strawberry"),
R.drawable.strawberry_pic))
fruitList.add(Fruit(getRandomLengthString("Cherry"),
R.drawable.cherry_pic))
fruitList.add(Fruit(getRandomLengthString("Mango"),
R.drawable.mango_pic))
}
}
private fun getRandomLengthString(str:String):String{
val n = (1...20).random()
val builder = StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString
}
}

首先,在onCreate()方法中,我们创建了一个StaggeredGridLayoutManager的实例。 StaggeredGridLayoutManager的构造函数接收两个参数:第一个参数用于指定布局的列 数,传入3表示会把布局分为3列;第二个参数用于指定布局的排列方向,传入 StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列。最后把创建好的实例 设置到RecyclerView当中就可以了,就是这么简单

没错,仅仅修改了一行代码,我们就已经成功实现瀑布流布局的效果了。不过由于瀑布流布局 需要各个子项的高度不一致才能看出明显的效果,为此我又使用了一个小技巧。这里我们把眼 光聚焦到getRandomLengthString()这个方法上,这个方法中调用了Range对象的 random()函数来创造一个1到20之间的随机数,然后将参数中传入的字符串随机重复几遍。在 initFruits()方法中,每个水果的名字都改成调用getRandomLengthString()这个方法 来生成,这样就能保证各水果名字的长短差距比较大,子项的高度也就各不相同了。

RecycleView的点击事件

和ListView一样,RecyclerView也必须能响应点击事件才可以,不然的话就没什么实际用途 了。不过不同于ListView的是,RecyclerView并没有提供类似于 setOnItemClickListener()这样的注册监听器方法,而是需要我们自己给子项具体的View 去注册点击事件。这相比于ListView来说,实现起来要复杂一些

那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView,偏偏在点击 事件上却没有处理得非常好呢?其实不是这样的,ListView在点击事件上的处理并不人性化, setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具 体的某一个按钮呢?虽然ListView也能做到,但是实现起来就相对比较麻烦了。为此, RecyclerView干脆直接摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View去 注册,就再没有这个困扰了。

下面我们来具体学习一下如何在RecyclerView中注册点击事件,修改FruitAdapter中的代 码,如下所示:

class FruitAdapter(val fruitList:List<Fruit>):
RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
override fun onCreateViewHolder(parent:ViewGroup,viewType:Int):ViewHolder{
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item,false)
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener{
val position = viewHolder.adapterPosition
val fruit = fruitList[positon]
Toast.makeText(parent.context,"you clicked view ${fruit.name}",Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener{
val positon = viewHolder.adapterPositon
val fruit = fruitList[position]
Toast.makeText(parent.context,"you clicked image ${fruit.name}"),Toast.LENGTH_SHORT).show()
}
return viewHolder
}

}

ViewBinding的使用

项目集成

需要使用ViewBinding的功能,需要在对应的module的build.gradle文件中启用ViewBinding支持。

android {
...
viewBinding {
enabled = true
}
}

如果您希望在生成绑定类时忽略某个布局文件,请将tools:viewBindingIgnore="true"属性添加到相应布局文件的根视图中:

<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>

在Activity中使用

如需设置绑定类的实例以供 Activity 使用,请在 Activity 的onCreate()方法中执行以下步骤:

  1. 调用生成的绑定类中包含的静态inflate()方法。此操作会创建该绑定类的实例以供 Activity 使用。
  2. 通过调用 getRoot()方法或使用kotlin语法获取对根视图的引用。
  3. 将根视图传递到setContentView,使其成为屏幕上的活动视图
private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding .inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
}

配置后使用方式如下:

binding.btnTest.setText("test");
binding.button.setOnClickListener(new View.OnClickListener() {
// todo
});

在Adapter中使用ViewBinding

public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> {

private List<String> mList;

public MainAdapter(List<String> list) {
mList = list;
}

@NonNull
@Override
public MainAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//之前的写法
//View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_comment, parent, false);
//ViewHolder holder = new ViewHolder(view);

//使用ViewBinding的写法
LayoutCommentBinding commentBinding = LayoutCommentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
ViewHolder holder = new ViewHolder(commentBinding);
return holder;
}

@Override
public void onBindViewHolder(@NonNull MainAdapter.ViewHolder holder, int position) {
holder.mTextView.setText(mList.get(position));
}

@Override
public int getItemCount() {
return mList.size();
}

static class ViewHolder extends RecyclerView.ViewHolder {

TextView mTextView;

//之前的写法
//public ViewHolder(@NonNull View itemView) {
// super(itemView);
// mTextView = itemView.findViewById(R.id.tv_include);
//}

//使用ViewBinding的写法
ViewHolder(@NonNull LayoutCommentBinding commentBinding) {
super(commentBinding.getRoot());
mTextView = commentBinding.tvInclude;
}

}
}

ViewPage2

MainActivyty.java

public class MainActivity extends AppCompatActivity{
@Override
protected void onCreate(BUndle savedInstanceState){
super.onCreate(savedInstanceState)
setContentVeiw(R.layout.activity_main);
ViewPager2 viewPager = findViewById(R.id.viewPager);
viewPagerAdapter viewPagerAdpater = new ViewPagerAdapter();
viewPager.setAdapter(viewPagerAdapter);
}
}

ViewPagerAdapter.java

public class ViewPagerAdapter extends RecyclerView.Adapter<ViewPagerAdapter.ViewPagerViewHolder>{
private List<String> titles = new ArrayList<>();
private List<Integer> colors = new ArrayLIst<>();

public ViewPagerAdapter(){
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");
titles.add("hello");

colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);
colors.add(R.color.white);

}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,int viewType){
return new ViewPagerViewHolder(LayoutInlater.from(parent.getContext()).inflate(R.ayout.item_pager,parent,false));
}

@Override
public void onBindViewHolder(@NonNull ViewPagerViewHolder holder,int position){
holder.mYv.setText(titles.get(position));
holder.mContainer.setBakcgroundResource(colors.get(position));
}

@Override
public int getItemCount(){
return 10;
}

class ViewPagerViewHolder extends RecyclerView.ViewHolder{
TextVeiw mTv;
RelativeLayout mContainer;
public viewPagerViewHolder(@NonNull View itemView){
super(itemView);
mContainer = itemView.findViewById(R.id.container);
mTv = itemView.findViewById(R.id.tvTitle);
}

}

item_pager.xml

<RealtiveLayout xmlns:android="https//schemas.android.com/apk/res"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:id = "@+id/container"
>
<TextView
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:id = "+id/tvTitle"
android:layout_centerInParent = "true"
android:textColor = "#ff4532"
android:textSize = "32dp"
android:text = "hello"
/>
</RelativeLayout>

colors.xml

<resources>
<color name="red">#ff4411</color>
<color name="black">000000</color>
<color name="white">#ffffff</color>
</resources>

编写界面的最佳实践

制作9-Patch图片

那么9-Patch图片到底有什么实际作用呢?我们还是通过一个例子来看一下吧。首先在 UIBestPractice项目中放置一张气泡样式的图片message_left.png

我们将这张图片设置为LinearLayout的背景图片,修改activity_main.xml中的代码,如下所 示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/message_left">
</LinearLayout>

可以看到,由于message_left的宽度不足以填满整个屏幕的宽度,整张图片被均匀地拉伸 了!这种效果非常差,用户肯定是不能容忍的,这时就可以使用9-Patch图片来进行改善。 制作9-Patch图片其实并不复杂,只要掌握好规则就行了,那么现在我们就来学习一下。 在Android Studio中,我们可以将任何png类型的图片制作成9-Patch图片。首先对着 message_left.png图片右击→Create 9-Patch file

我们可以在图片的4个边框绘制一个个的小黑点,在上边框和左边框绘制的部分表示当图片需要 拉伸时就拉伸黑点标记的区域,在下边框和右边框绘制的部分表示内容允许被放置的区域。使 用鼠标在图片的边缘拖动就可以进行绘制了,按住Shift键拖动可以进行擦除。绘制完成后效果 如图4.43所示。

最后记得要将原来的message_left.png图片删除,只保留制作好的message_left.9.png图片 即可,因为Android项目中不允许同一文件夹下有两张相同名称的图片(即使后缀名不同也不行)。重新运行程序,效果如图4.44所示。

编写精美的聊天界面

开始编写主界面,修改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"
background="#d8e0e8">
<androidx.recyclerview.widget.RecyclerView
android:id="@id/recyclerView"
aandroid:id=layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinderLayout
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"/>
</LinderLayout>
</LinearLayout>

我们在主界面中放置了一个RecyclerView用于显示聊天的消息内容,又放置了一个EditText用 于输入消息,还放置了一个Button用于发送消息。这里用到的所有属性都是我们之前学过的, 相信你理解起来应该不费力。

然后定义消息的实体类,新建Msg,代码如下所示:

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

Msg类中只有两个字段:content表示消息的内容,type表示消息的类型。其中消息类型有两 个值可选:TYPE_RECEIVED表示这是一条收到的消息,TYPE_SENT表示这是一条发出的消息。这里我们将TYPE_RECEIVED和TYPE_SENT定义成了常量,定义常量的关键字是const, 注意只有在单例类、companion object或顶层方法中才可以使用const关键字。

接下来开始编写RecyclerView的子项布局,新建msg_left_item.xml,代码如下所示:

<FrameLayout xmlns:"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:pddding="10dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_grativity="left"
android:background="drawable/message_left">
<TextView
android:id="@id/leftMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff"/>
</LinearLayout>
</FrameLayout>

这是接收消息的子项布局。这里我们让收到的消息居左对齐,并使用message_left.9.png作为 背景图。

类似地,我们还需要再编写一个发送消息的子项布局,新建msg_right_item.xml,代码如下所 示

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<LinerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@drawable/message_right">
<TextView
android:id="@id/rightMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textColor="#000"/>
</LinerLayout>
</FrameLayout>

这里我们让发出的消息居右对齐,并使用message_right.9.png作为背景图,基本上和刚才的 msg_left_item.xml是差不多的

接下来需要创建RecyclerView的适配器类,新建类MsgAdapter,代码如下所示:

class MsgAdapter(val msgList:List<Msg>):RecyclerView.Adapter<RecyclerView.ViewHolder>(){
inner class LeftViewHolder(view:View):RecyclerView.ViewHolder(view){
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}
inner class RightViewHolder(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) = if(viewType==Msg.TYPE_RECEIVED){
val view = LayoutInflater.fron(parent.context).inflate(R.layout.msg_left_item,parent,false)
LeftViewHolder(view)
}else{
val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,parent,false)
RightViewHolder(view)
}
override fun onBindViewHolder(holder:RecyclerVIew.ViewHolder,position:Int){
val msg = msgList[position]
when(holder){
is LeftViewHolder->holder.leftMsg.text = msg.content
is RightViewHolder->holder.rightMst.text = msg.content
else -> throw IlleaglArgumentException()
}
}
}

上述代码中用到了一个新的知识点:根据不同的viewType创建不同的界面。首先我们定义了 LeftViewHolder和RightViewHolder这两个ViewHolder,分别用于缓存 msg_left_item.xml和msg_right_item.xml布局中的控件。然后要重写 getItemViewType()方法,并在这个方法中返回当前position对应的消息类型。

接下来的代码你应该就比较熟悉了,和我们之前学习的RecyclerView用法是比较相似的,只是 要在onCreateViewHolder()方法中根据不同的viewType来加载不同的布局并创建不同的 ViewHolder。然后在onBindViewHolder()方法中判断ViewHolder的类型:如果是 LeftViewHolder,就将内容显示到左边的消息布局;如果是RightViewHolder,就将内容 显示到右边的消息布局。

最后修改MainActivity中的代码,为RecyclerView初始化一些数据,并给发送按钮加入事件响 应,代码如下所示:

class MainActivity : AppCompatActivity(),View.OnClickListener{
private val msgList = ArrayLIst<Msg>()
private var adapter:MsgAdapter? = null
override fun onCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentVeiw(R.layout.activity_main)
initMsg()
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
adapter = MsgAdapter(magList)
recyclerView.adapter = adapter
send.setOnClickListner(this)
}
override fun onClick(v:View?){
when(v){
send->{
val content = inputText.text.toString()
if(content.isNotEmpty()){
val msg = Msg(content,Msg.TYPE_SENT)
magList.add(msg)
adapter?.notifyItemInserted(msgLsig.size-1)
recyclerView.scrollToPosition(msgLisg.size-1)
inputText.setText("")
}
}
}
}
private fun initMst(){
val masl = Msg("Hello guy.",Msg.TYPE_RECEIVED)
msgLisg.add(msg1)
val msg2 = Msg("Hello. Who is that?",Msg.TYPE_SENT)
msgList.add(msg2)
val msg3 = Msg("This is Tom.Nice talking to you",Msg.TYPE_RECEIVED)
msgList.add(msg3)
}
}

Kotlin课堂:延迟初始化和密封类

对变量延迟初始化

延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量 进行初始化,这样就不用在一开始的时候将它赋值为null了。

private lateinit var adapter:MsgAdapter

当然,使用lateinit关键字也不是没有任何风险,如果我们在adapter变量还没有初始化的 情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个 UninitializedPropertyAccessException异常

另外,我们还可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够 有效地避免重复对某一个变量进行初始化操作,示例代码如下

if(::adapter.isInitialized){
adapter = MsgAdpater(msgList)
}

具体语法就是这样,::adapter.isInitialized可用于判断adapter变量是否已经初始 化。虽然语法看上去有点奇怪,但这是固定的写法。然后我们再对结果进行取反,如果还没有 初始化,那么就立即对adapter变量进行初始化,否则什么都不用做。

使用密封类优化代码

首先来了解一下密封类具体的作用,这里我们来看一个简单的例子。新建一个Kotlin文件,文件 名就叫Result.kt好了,然后在这个文件中编写如下代码:

interface Result
class Success(val msg:String):Result
class Failure(val error:Exception):Result

这里定义了一个Result接口,用于表示某个操作的执行结果,接口中不用编写任何内容。然后 定义了两个类去实现Result接口:一个Success类用于表示成功时的结果,一个Failure类 用于表示失败时的结果,这样就把准备工作做好了。

接下来再定义一个getResultMsg()方法,用于获取最终执行结果的信息,代码如下所示

fun getResultMsg(result:Result) = when(result){
is Success -> result.msg
is Failure -> result.error.message
else -> throw IllegalArgumentException()
}

etResultMsg()方法中接收一个Result参数。我们通过when语句来判断:如果Result属 于Success,那么就返回成功的消息;如果Result属于Failure,那么就返回错误信息。到 目前为止,代码都是没有问题的,但比较让人讨厌的是,接下来我们不得不再编写一个else条 件,否则Kotlin编译器会认为这里缺少条件分支,代码将无法编译通过。但实际上Result的执 行结果只可能是Success或者Failure,这个else条件是永远走不到的,所以我们在这里直接 抛出了一个异常,只是为了满足Kotlin编译器的语法检查而已。

另外,编写else条件还有一个潜在的风险。如果我们现在新增了一个Unknown类并实现 Result接口,用于表示未知的执行结果,但是忘记在getResultMsg()方法中添加相应的条 件分支,编译器在这种情况下是不会提醒我们的,而是会在运行的时候进入else条件里面,从 而抛出异常并导致程序崩溃。

密封类的关键字是sealed class,它的用法同样非常简单,我们可以轻松地将Result接口改 造成密封类的写法:

sealed class Result 
class Success(val msg:String):Result()
class Failure(val reror:Exception):Result()

可以看到,代码并没有什么太大的变化,只是将interface关键字改成了sealed class。另 外,由于密封类是一个可继承的类,因此在继承它的时候需要在后面加上一对括号,这一点我 们在第2章就学习过了。

那么改成密封类之后有什么好处呢?你会发现现在getResultMsg()方法中的else条件已经不 再需要了,如下所示:

fun getResultMsg(result: Result) = when (result) { 
is Success -> result.msg
is Failure -> "Error is ${result.error.message}"
}

为什么这里去掉了else条件仍然能编译通过呢?这是因为当在when语句中传入一个密封类变量 作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应 的条件全部处理。这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的 情况。而如果我们现在新增一个Unknown类,并也让它继承自Result,此时 getResultMsg()方法就一定会报错,必须增加一个Unknown的条件分支才能让代码编译通 过。

了解了这么多关于密封类的知识,接下来我们看一下它该如何结合MsgAdapter中的 ViewHolder一起使用,并顺便优化一下MsgAdapter中的代码。

观看MsgAdapter现在的代码,你会发现onBindViewHolder()方法中就存在一个没有实际作 用的else条件,只是抛出了一个异常而已。对于这部分代码,我们就可以借助密封类的特性来 进行优化。首先删除MsgAdapter中的LeftViewHolder和RightViewHolder,然后新建一个 MsgViewHolder.kt文件,在其中加入如下代码:

sealed class MsgViewHolder(view: View) : RecyclerView.ViewHolder(view) 

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

class RightViewHolder(view: View) : MsgViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}