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 >
它可配置的属性和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()方法中执行以下步骤:
调用生成的绑定类中包含的静态inflate()方法。此操作会创建该绑定类的实例以供 Activity 使用。
通过调用 getRoot()方法或使用kotlin语法获取对根视图的引用。
将根视图传递到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() { });
在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) { 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; 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); } }
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); } }
<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):Resultclass 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) }