说明: 本文是郭霖《第一行代码-第3版》的读书笔记
4.1 如何编写程序界面
-
编写XML,这是传统的方法
-
ConstraintLayout,Google推出的新方法,可以在可视化编辑器中拖动控件操作
这里使用的是编写XML方法
4.2 常用控件
4.2.1 TextView
修改activity_main.xml的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal|center_vertical"
android:textColor="#00ff00"
android:textSize="24sp"
android:text="This is TextView"/>
</LinearLayout>
android:id
是控件唯一标识符
android:layout_width
和android:layout_height
是所有控件都具有的两个属性,可选值3个:
match_parent
,当前控件大小和父布局一样wrap_parent
,当前控件大小刚好能包住里面的内容固定值
,指定固定尺寸,单位是dp
,像素独立单位,可以保证在不同分辨率的设备上显示效果尽可能相同
android:gravity
,指定文字的对齐方式,TextView文字默认是左上角对齐的,所以可能看不出match_parent,这里指定了水平居中和垂直居中。
android:textColor
, 指定文字的颜色
android:textSize
, 指定文字的大小,单位是sp
,这样当用户修改系统字体尺寸时,应用程序中的文字大小也会跟着修改。
4.2.2 Button
在activity_main中加入一个Button控件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="Button"/>
</LinearLayout>
android:textAllCaps
指定为false可以保留指定的原始文字内容。因为Android默认将Button上的英文字母全部转成大写。
在MainActivity中为Button的点击事件注册一个监听器:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 注意不通过findViewById来找到button而直接写,需要引入'android-kotlin-extensions'插件
button.setOnClickListener {
//添加逻辑
}
}
}
上述方法是使用函数式API的方式来中注册监听器,也可以使用实现接口的方式来注册:
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 用这种方式的时候不要忘了设置ClickListener
button.setOnClickListener(this)
}
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.button -> {
//TODO
}
}
}
}
4.2.3 EditText
允许输入和编辑内容的控件,并可以在程序中对这些内容进行处理。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:hint="Type something here"/>
</LinearLayout>
android:hint
是EditText无输入时的提示文本。
android:maxlines
: 随着输入的增多,文本框被不断拉长,这是因为我们指定高度是wrap_content,指定max_lines能固定输入文本框的大小,超过两行时,文本就会向上滚动而不会拉伸EditText。
点击按钮获取EditText中的内容
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.button -> {
// 获取EditText中的内容
val inputText = editText.text.toString()
Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
}
}
}
4.2.4 ImageView
imageView通常是放在以drawable
开头的目录下的,并且要带上具体的分辨率,主流的手机屏幕分辨率大多数是xxhdpi
的,因此可以在res
目录下新建一个drawable-xxhdpi
目录。
修改activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img_1"/>
</LinearLayout>
android:src
指定图片源
还可以在代码中动态更改图片:
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.button -> {
// 按下按钮更改图片源
imageView.setImageResource(R.drawable.img_2)
}
}
}
4.2.5 ProgressBar
顾名思义,ProcessBar即进度条,用来显示进度
修改activity_main.xml
代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
style="?android:attr/progressBarStyleHorizontal"
android:max="60"
/>
</LinearLayout>
android:visibility
设置控件的可见性,是所有控件都有的属性。可选值有
-
visible
, 控件可见,默认属性 -
invisiable
, 控件不可见,但仍占据原来的屏幕空间 -
gone
, 控件不可见,而且不占用屏幕空间
也可以通过代码改变这个属性的值,使用的是setVisiability()
方法,可选值有View.VISIBLE
、View.INVISIABLE
、GONE
style="?android:attr/progressBarStyleHorizontal"
设置进度条样式为水平,此时静止在0,默认的是动态的环形不断旋转的进度条,当设置成水平进度条时,就可以设置一个最大值android:max
,然后在代码内更改进度:
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.button -> {
// 点击一下Button进度加一截
progressBar.progress += 10
}
}
}
4.2.6 AlertDialog
AlertDialog,顾名思义是警告对话框,可以弹出一个对话框,置于所有Activity之上,屏蔽掉其他Activity的交互,用于提醒一些非常重要的内容或者警告信息。
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.button -> {
// AlertDialog
// apply函数
AlertDialog.Builder(this).apply {
setTitle("This is a AlertDialog")
setMessage("Something important")
setCancelable(true)
setPositiveButton("OK") { dialog, which ->
Log.d("AlertDialog", "Choose OK")
}
setNegativeButton("Cancel") { dialog, which ->
Log.d("AlertDialog", "Choose Cancel")
}
show()
}
}
}
}
代码中setPositiveButton("OK") {dialog, which ->}
,dialog,which这部分是高阶函数,也可以写成:
builder.setPositiveButton(android.R.string.yes) { _,_ ->
Toast.makeText(applicationContext,
android.R.string.yes, Toast.LENGTH_SHORT).show()
}
以下是Journal Dev中的讲解:https://www.journaldev.com/309/android-alert-dialog-using-kotlin
The function type is (DialogInterface, Int) -> Unit
. DialogInterface is an instance of the Dialog and Int is the id of the button that is clicked.
In the above code, we’ve represented this function as a Higher Order Kotlin function. The dialog
and which
represents the two arguments.
4.3 详解三种布局
布局是一种可以放置很多控件的容器,除了放控件,还可以放布局,即布局的嵌套。
4.3.1 LinerLayout
LinearLayout是线性布局,可以通过android:orientation
指定线性排列的方向。默认是android:orientation=horizontal
.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button1"
android:layout_gravity="top"
android:text="Button 1"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button2"
android:layout_gravity="center_vertical"
android:text="Button 2"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button3"
android:layout_gravity="bottom"
android:text="Button 3"/>
</LinearLayout>
注意: 如果LinearLayout的排列方向是horizontal
,那么内部的控件就绝对不能将宽度指定为match_parent
。同理,如果排列方向是vertical
, 那么不能将高度指定为match_partent
。
android:layout_gravity
: 用于指定控件在布局中的对齐方式,而android:gravity
用于指定文字在控件中的对齐方式。需要注意:当LinearLayout的排列是horizontal时,只有垂直方向的对齐方式才会生效。
还有一个重要的属性是layout_weight
,允许使用比例的方式指定控件的大小。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:hint="Type Something"/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
因为我们指定了layout_weight
,所以控件的宽度不再由layout_width
指定,所以这里可以给0dp
。android:layout_weight
相当于是权重。也可以让它更好看些,比如Button的大小给他wrap_content
.
4.3.2 RelativeLayout
RelativeLayout是相对布局,通过相对定位的方式让控件出现在布局的任何位置。RelativeLayout的属性非常多。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:text="Button 1"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button2"
android:text="Button 2"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button3"
android:text="Button 3"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button4"
android:text="Button 4"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button5"
android:text="Button 5"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
感觉这个注意下写法就好了。但上面的例子都是相对于父布局进行定位的,当然也可以相对于控件定位。用相对控件定位的时候需要先声明相对的这个控件。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button3"
android:text="Button 3"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button1"
android:text="Button 1"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button2"
android:text="Button 2"
android:layout_above="@id/button3"
android:layout_toRightOf="@id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button4"
android:text="Button 4"
android:layout_below="@id/button3"
android:layout_toLeftOf="@id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/button5"
android:text="Button 5"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
就感觉这种RelativeLayout指定的是相对的大概的位置,没有那么精确。除此之外,还有android:layout_alignLeft
这类的属性,表示左边缘和另一个控件的左边缘对齐,这种相对的一般都要指定两个才知道在哪吧。
4.3.3 FrameLayout
FrameLayout是帧布局,这种布局很简单,所有控件都会默认摆在布局的左上角。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:text="Button"/>
<TextView
android:id="@+id/textView"
android:layout_gravity="left|top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a TextView"/>
</FrameLayout>
除了默认效果之外,也可以使用layout_gravity
来指定控件在布局中的对齐方式。
目前FrameLayout应用场景偏少,但后面在Fragment时还可以用到它。
4.4 创建自定义控件
首先介绍控件和布局的继承结构:
- 所有控件都直接或间接继承自View,所有布局都继承自ViewGroup
- View是Android中基本的UI组件,可以在屏幕上绘制一块矩形,并能响应这块区域的各种事件
- ViewGroup是一种特殊的View,可以包含许多子View和子ViewGroup,是一个放置布局和控件的容器
4.4.1 引入布局
如果我们想让所有的Activity都有相同的标题栏,那么是否可以不重复写代码?当然是可以的,可以通过引入布局的方式来解决这个问题:在layout目录下新建一个title.xml
布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
<!-- 因为这里是要做一个title,所以高度能包着内容就行了,不能设置match_parent -->
android:layout_height="wrap_content"
android:background="@drawable/title_bg">
<Button
android:id="@+id/titleBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:textColor="#fff"
android:text="Back"/>
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
<!-- 文字也要设置居中,不然的话矩形框居中了,但文字没居中就看不出来效果 -->
android:gravity="center"
android:layout_weight="1"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp"/>
<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff"/>
</LinearLayout>
android:backgroud
: 用于为布局或者控件指定背景,可以是颜色,也可以是图片来填充
android:layout_margin
: 指定控件在上下左右方向上的间距,也可以使用android:layout_marginLeft
或者android:layout_marginTop
等属性分别指定。
下面是如何使用这个标题,直接在activity_main.xml上修改如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/title"/>
</LinearLayout>
只需要通过一行include语句引入标题栏布局就可以了。
最后别忘了隐藏掉系统自带的标题栏
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
}
}
这里调用getSupportActionBar()
方法获得ActionBar的实例,再调用hide()
方法将标题栏隐藏起来。有关ActionBar的知识会在12章中讲到。
使用这种方式,不管有多少Activity要添加标题栏,只需要一行include语句就行了。
4.4.2 创建自定义控件
引入布局的技巧使得我们不需要重复编写布局,但如果有一些控件要能响应事件,那还是需要在每个Activity中写一次事件注册的代码。这个时候最好使用自定义控件的方法来解决:
class TitleLayout(context: Context, attrs: AttributeSet): LinearLayout(context, attrs) {
init {
LayoutInflater.from(context).inflate(R.layout.title, this)
val titleBack: Button = findViewById(R.id.titleBack)
val titleEdit: Button = findViewById(R.id.titleEdit)
titleBack.setOnClickListener {
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit.", Toast.LENGTH_SHORT).show()
}
}
}
首先需要声明context
和attrs
这两个参数,然后在init
结构体内声明加载的逻辑:先通过LayoutInflater的from()
方法构建出一个LayoutInflater
对象,调用inflater()
方法动态加载一个布局文件,第一个参数传入布局文件,第二个参数是给加载的布局添加一个父布局。
这里我们给返回和编辑按钮注册了点击事件。注意:TitleLayout接收的context参数实际上是一个Activity的实例,我们需要先将它转成Activity类型,再调用finish()方法销毁当前的Activity(不太理解这句话)。Kotlin中强制类型转换的关键字是as
。
然后就可以在activity_main.xml文件中引入这个布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
4.5 最常用最难用的控件:ListView
手机屏幕大小有限,当有大量数据要展示的时候可以借助ListView,ListView允许用户通过滑动的方式将屏幕外的数据滚动到屏幕内,屏幕上原有的数据滚动出屏幕。比如QQ聊天记录。
4.5.1 ListView的简单用法
首先修改android_main.xml
代码添加ListView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
然后修改Main_Activity.kt中的代码如下:
class MainActivity : AppCompatActivity() {
private val data = listOf("Apple", "Banana", "Orange", "Watermelon", "Pear", "Grape",
"Pineapple", "Strawberry", "Cherry", "Mango", "Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango");
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adpter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)
listView.adapter = adpter
}
}
首先我们有水果名字的集合需要显示,但是集合中的数据无法直接传给ListView,需要借助适配器来实现。Android中提供了很多适配器的实现类,这里使用的是ArrayAdapter.可以通过泛型指定要适配的数据类型。ArrayAdapter构造函数需要传入的参数是:Activity的实例、ListView子项布局的id,以及数据源。这里子项布局使用的是Android内置的布局文件android.R.layout_simple_list_item_1
,里面只有一个TextView,可用于简单显示一段文本。最后调用ListView的setAdapter()方法,将构建好的适配器传进去。
4.5.2 定制ListView的界面
上面写的ListView太过简单。我们想让每一个子项显示图片和文字。
新建一个Fruit
类
class Fruit (val name: String, val imageId: Int) {
}
然后添加一个子项布局,左边是Img,右侧是文字说明
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_margin="10dp"/>
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_margin="10dp"/>
</LinearLayout>
接下来需要创建一个自定义的适配器,
//自定义的适配器继承自ArrayAdapter,重写了getView方法
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruit = getItem(position) //获取当前项的Fruit实例
if (fruit != null) {
view.fruitName.text = fruit.name
view.fruitImage.setImageResource(fruit.imageId)
}
return view
}
}
getView()
在每个子项被滚动到屏幕的时候会被调用,首先使用LayoutInflater加载我们传入的布局,这里第三个参数指定为False表示我们只让在父布局中声明的Layout属性生效,但不会为这个View添加父布局。因为一旦View有了父布局后,就无法添加到ListView中了。(不太理解)再通过getItem()
方法得到当前项的Fruit实例,为其赋予ImageId和name,最后将布局返回。
在MainActivity中:
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化水果的数据
initFruits()
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
listView.adapter = adapter
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear", R.drawable.pear_pic))
fruitList.add(Fruit("Grape", R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
}
}
初始化FruitList,然后构建我们的FruitAdapter适配器,传入ListView当中就可以了。
repeat()
函数是Kotlin中非常常用的标准函数,允许传入一个数值n,然后将Lambda表达式中的内容执行n遍。
4.5.3 提升ListView的运行效率
ListView难用是因为有很多细节可以优化,比如运行效率。上面的FruitAdapter的getView()
方法中,对每一个子项,都要调用getView()
每次都要将布局加载一遍,但在getView()
方法中有一个convertView
参数,可以将之前加载好的布局缓存:
//修改FruitAdapter中的getView方法
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View
// 如果convertView为空则加载布局,否则使用缓存下来的布局
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
} else {
view = convertView
val fruit = getItem(position) //获取当前项的Fruit实例
if (fruit != null) {
view.fruitName.text = fruit.name
view.fruitImage.setImageResource(fruit.imageId)
}
return view
}
同时我们也不需要每次创建itemView的时候都去重复绑定控件,不能都通过view.findViewById()
来查找,可以缓存到一个自定义的类ViewHolder
中。
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
viewHolder = ViewHolder(fruitImage, fruitName)
view.tag = viewHolder
} else {
view = convertView
viewHolder = view.tag as ViewHolder
}
val fruit = getItem(position) //获取当前项的Fruit实例
if (fruit != null) {
viewHolder.fruitName.text = fruit.name
viewHolder.fruitImage.setImageResource(fruit.imageId)
}
return view
}
}
ViewHolder类中用tag对ImageView和TextView进行缓存,然后在需要的时候再取出来。Kotlin中使用inner class
来声明内部的类。
4.5.4 ListView的点击事件
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化水果的数据
initFruits()
val adapter = FruitAdapter(this, R.layout.fruit_item, fruitList)
listView.adapter = adapter
//为ListView注册监听器
listView.setOnItemClickListener { parent, view, position, id ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
没有用到的参数可以用占位符_
代替
4.6 更强大的滚动控件:RecycleView
如果不使用一些技巧优化ListView,那么其效率会很差,扩展性也不够好,只能实现纵向滚动的效果。RecycleView不仅能实现和ListView同样的效果,而且也优化了其不足之处。目前Android官方更推荐使用RecycleView。
4.6.1 RecycleView的基本用法
RecycleView属于新增控件,需要在项目的build.gradle
中添加RecycleView的依赖。
打开app/build.gradle
文件,在dependencies
上添加如下内容:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "androidx.recyclerview:recyclerview:1.2.1"
// For control over item selection of both touch and mouse driven selection
// 现在的RecycleView好像要添加这一项
implementation "androidx.recyclerview:recyclerview-selection:1.1.0"
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
然后修改布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycleView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
接下来需要为RecycleView准备一个适配器,继承自RecycleView.Adapter,并指定泛型为自己内部定义的ViewHolder:
class FruitAdapter(val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
override fun getItemCount(): Int {
return fruitList.size
}
}
上述是RecycleView适配器的标准写法。由于继承自RecycleView.Adapter,因此需要重写onCreateViewHolder()、onBindViewHolder()、getItemCount()
这三个方法。
onCreateViewHolder()
: 用于创建ViewHolder的实例,在这个方法中传入fruit_item的布局
onBindViewHolder()
: 用于对子项数据赋值
getItemCount()
: 告诉RecycleView共有多少项,直接传入List的长度
修改MainActivity的代码如下:
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
val layoutManager = LinearLayoutManager(this)
recycleView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recycleView.adapter = adapter
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear", R.drawable.pear_pic))
fruitList.add(Fruit("Grape", R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
}
}
首先需要初始化所有的水果数据,
再创建一个LinearLayoutManager对象,并将它设置到RecycleView当中。LinearLayoutManager对象指定了RecycleView的布局方式。
接下来创建一个适配器实例,赋给RecycleView。这样RecycleView和数据之间就关联起来了。
4.6.2 实现横向滚动和瀑布流布局
横向滚动
首先需要改一下每一个子项的布局,从水平变为垂直,然后需要在MainActivity中改layoutManager为水平就可以了。
瀑布流布局
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
// 第一个参数指定3列,第二个参数指定布局的排列方向
val layoutManager = StaggeredGridLayoutManager(3,
StaggeredGridLayoutManager.VERTICAL)
recycleView.layoutManager = layoutManager
// 网格布局
// val layoutManager = GridLayoutManager(this, 3)
// recycleView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recycleView.adapter = adapter
}
}
感觉布局还是得自己去调,怎么好看怎么来,这里主要掌握API的用法。
4.6.3 RecycleView的点击事件
ListView提供了一个叫setOnItemClickListener()
的方法,但是RecycleView并没有类似的方法,需要我们自己给子项具体的View去注册点击事件。(这其实是一件好事,尽管麻烦一些)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
val viewHolder = ViewHolder(view)
// 注册ViewHolder的点击事件
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "You clicked Item ${fruit.name}", Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.absoluteAdapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "You clicked Image ${fruit.name}", Toast.LENGTH_SHORT).show()
}
return viewHolder
}
可以看到,我们在onCreateViewHolder()
函数内注册点击事件。这里的itemView
指的是子项的最外层布局。然后我们又找到子项的imageView,为其注册点击事件。
4.7 编写界面的最佳实践
4.7.1 制作9-Patch图片
这个九宫格的图片之前也碰到过,指的是一张图片哪里能被拉伸。
在AndroidStudio中能直接绘制9-Patch图片,左键点击黑色的就是能被拉伸的区域,按住Shift拖动可以擦除。
4.7.2 编写聊天界面
首先还是在build.gradle
里添加recycleView的依赖,然后修改activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycleView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/inputText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something here"
android:maxLines="2"/>
<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
</LinearLayout>
添加了一个RecycleView,一个Button,一个EditText。
再创建一个Msg消息类:
class Msg(val content: String, val type: Int) {
companion object {
const val TYPE_RECEIVED = 0
const val TYPE_SEND = 1
}
}
Kotlin中定义常量的关键字是const
,但是只有在单例类、companion object或顶层方法中才可以使用const关键字。
然后还需要编写RecycleView的子项布局:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left"
android:background="@drawable/message_left">
<TextView
android:id="@+id/leftMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_margin="10dp"
android:textColor="#fff"/>
</LinearLayout>
</FrameLayout>
关于padding和margin的区别:
为了使用RecycleView,需要定义适配器,适配器的写法和前面差不多,不过这里会判断是接收到的消息还是发送的消息,从而inflate不同的布局。
class MsgAdapter(val msgList: ArrayList<Msg>):RecyclerView.Adapter<RecyclerView.ViewHolder>() {
inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view){
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}
inner class RightViewHold(view: View) : RecyclerView.ViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
override fun getItemViewType(position: Int): Int {
val msg = msgList[position]
return msg.type
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == Msg.TYPE_RECEIVED) {
val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item,
parent, false)
return LeftViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,
parent, false)
return RightViewHold(view)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHold -> holder.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
override fun getItemCount(): Int {
return msgList.size
}
}
最后修改MainActivity中的代码:
class MainActivity : AppCompatActivity() {
private val msgList = ArrayList<Msg>()
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化消息列表
initMsg()
//加载布局管理器
val layoutManager = LinearLayoutManager(this)
recycleView.layoutManager = layoutManager
//适配器
adapter = MsgAdapter(msgList)
recycleView.adapter = adapter
//为Button添加响应
send.setOnClickListener {
val content = inputText.text.toString()
if (content != null) {
val msg = Msg(content, Msg.TYPE_SEND)
msgList.add(msg)
//刷新RecycleView中的显示
adapter?.notifyItemInserted(msgList.size - 1)
//定位到最后一行
recycleView.scrollToPosition(msgList.size - 1)
//清空输入框
inputText.setText("")
}
}
}
private fun initMsg() {
val msg1 = Msg("Hello smallSheep.", Msg.TYPE_RECEIVED)
val msg2 = Msg("Hello, Who is that?", Msg.TYPE_SEND)
val msg3 = Msg("This is BigZ, Nice to meet u.", Msg.TYPE_RECEIVED)
msgList.add(msg1)
msgList.add(msg2)
msgList.add(msg3)
}
}
这里有几个方法值得注意:
adapter.notifyItemInserted()
: 用于通知列表有新数据插入,这样新增的消息才会在RecycleView中显示出来,或者也可以调用notifyDataSetChanged()
方法,这样增删改数据都可以显示出来,但效率稍低。
recycleView.scrollToPosition
: 将显示的数据定位到最后一行,保证新增的数据看得到。