目录
常用控件的使用方法
TextView
TextView是Android中最简单的一个控件
<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标签内容
android:id:给当前控件定义了一个唯一标识符
android:layout_width、android:layout_height:指定了控件的宽度和高度
可选值有三种:
- match_parent:让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小
- wrap_content:让当前控件的大小能够刚好包含住里面的内容,也就是由控件内容决定当前控件的大小
- 固定值:表示给控件指定一个固定的尺寸,单位一般用dp,这是一种屏幕密度无关的尺寸单位,可以保证在不同分辨率的手机上显示效果尽可能地一致,如50 dp就是一个有效的固定值
android:gravity:指定文字的对齐方式,可选值有top、bottom、start、end、center等,可以用“|”来同时指定多个值
android:textColor:指定文字的颜色
android:textSize:指定文字的大小。文字大小要使用sp作为单位,这样当用户在系统中修改了文字显示尺寸时,应用程序中的文字大小也会跟着变化
运行效果:
Button
Button是程序用于和用户进行交互的一个重要控件,它可配置的属性和TextView是差不多的
<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>
在MainActivity中为Button的点击事件注册一个监听器
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
// 在此处添加逻辑
}
}
调用button的setOnClickListener()方法时利用了Java单抽象方法接口的特性,从而可以使用函数式API的写法来监听按钮的点击事件。这样每当点击按钮时,就会执行Lambda表达式中的代码,我们只需要在Lambda表达式中添加待实现的逻辑就行了
使用函数式API的方式来注册监听器
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
// 在此处添加逻辑
}
}
}
}
让MainActivity实现了View.OnClickListener接口,并重写了onClick()方法,然后在调用button的setOnClickListener()方法时将MainActivity的实例传了进去。这样每当点击按钮时,就会执行onClick()方法中的代码了
运行效果:
EditText
EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理
<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>
android:hint:指定了一段提示性的文本
android:maxLines:指定了EditText的最大行数为两行,这样当输入的内容超过两行时,文本就会向上滚动,EditText则不会再继续拉伸
运行效果:
通过点击按钮获取EditText中输入的内容
class MainActivity : AppCompatActivity(), View.OnClickListener {
...
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
val inputText = editText.text.toString()
Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
}
}
}
}
在按钮的点击事件里调用EditText的getText()方法获取输入的内容,再调用toString()方法将内容转换成字符串,最后使用Toast将输入的内容显示出来
ImageView
ImageView是用于在界面上展示图片的一个控件,它可以让程序界面变得更加丰富多彩
<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属性给ImageView指定了一张图片。由于图片的宽和高都是未知的,所以将ImageView的宽和高都设定为wrap_content,这样就保证了不管图片的尺寸是多少,都可以完整地展示出来
运行效果
动态地更改ImageView中的图片
class MainActivity : AppCompatActivity(), View.OnClickListener {
...
override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
imageView.setImageResource(R.drawable.img_2)
}
}
}
}
在按钮的点击事件里,通过调用ImageView的setImageResource()方法将显示的图片改成img_2
运行效果
ProgressBar
ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据
<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>
style:可以将进度条指定成水平进度条
android:max:给进度条设置一个最大值
android:visibility:
- 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) {
progressBar.visibility = View.GONE
} else {
progressBar.visibility = View.VISIBLE
}
}
}
}
}
在按钮的点击事件中,通过getVisibility()方法来判断ProgressBar是否可见,如果可见就将ProgressBar隐藏掉,如果不可见就将ProgressBar显示出来
AlertDialog
AlertDialog可以在当前界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽其他控件的交互能力,因此AlertDialog一般用于提示一些非常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框
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)
setPositiveButton("OK") { dialog, which ->}
setNegativeButton("Cancel") { dialog, which ->}
show()
}
}
}
}
}
首先通过AlertDialog.Builder构建一个对话框
使用Kotlin标准函数中的apply函数。在apply函数中为这个对话框设置标题、内容、可否使用Back键关闭对话框等属性
接下来调用setPositiveButton()方法为对话框设置确定按钮的点击事件
调用setNegativeButton()方法设置取消按钮的点击事件
最后调用show()方法将对话框显示出来就可以了
运行效果
CheckBox
这是一个复选框控件,用户可以通过点击的方式进行选中和取消
<CheckBox
android:id="@+id/rememberPass"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
ScrollView
由于手机屏幕的空间一般比较小,有些时候过多的内容一屏是显示不下的,借助ScrollView控件,就可以以滚动的形式查看屏幕外的内容
基本布局
LinearLayout
LinearLayout又称作线性布局,是一种非常常用的布局。正如它的名字所描述的一样,这个布局会将它所包含的控件在线性方向上依次排列
android:orientation:
- vertical:控件在垂直方向排列
- horizontal:控件在水平方向上排列,默认值
注意:
如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent,否则,单独一个控件就会将整个水平方向占满,其他的控件就没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
<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>
android:layout_gravity:用于指定控件在布局中的对齐方式
- top:置顶
- center_vertical:置中
- bottom:置尾
注意:
当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效
<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>
android:layout_weight:允许使用比例的方式来指定控件的大小
由于使用了android:layout_weight属性,此时控件的宽度就不应该再由android:layout_width来决定了,这里指定成0 dp是一种比较规范的写法
系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值
RelativeLayout
RelativeLayout又称作相对布局,也是一种非常常用的布局。和LinearLayout的排列规则不同,RelativeLayout显得更加随意,它可以通过相对定位的方式让控件出现在布局的任何位置
与父控件对齐:
android:layout_alignParentTop:如果为true,将该控件的顶部与其父控件的底部对齐
android:layout_alignParentBottom:如果为true,将该控件的底部与其父控件的底部对齐
android:layout_alignParentLeft:如果为true,将该控件的左部与其父控件的左部对齐
android:layout_alignParentRight:如果为true,将该控件的右部与其父控件的右部对齐
android:layout_centerHorizontal: 如果为true,将该控件水平居中
android:layout_centerVertical:如果为true,将该控件垂直居中
android:layout_centerInParent:如果为true,将该控件置于父控件的中央
上面属性中的每个控件都是相对于父布局进行定位的
边缘对齐但不会覆盖:
android:layout_above:属性可以让一个控件位于另一个控件的上方
android: layout_below:表示让一个控件位于另一个控件的下方
android:layout_toLeftOf:表示让一个控件位于另一个控件的左侧
android:layout_toRightOf:表示让一个控件位于另一个控件的右侧
上面属性中的每个控件都是相对于控件进行定位
边缘对齐但是有可能覆盖:
android:layout_alignLeft:表示让一个控件的左边缘和另一个控件的左边缘对齐android:layout_alignRight:表示让一个控件的右边缘和另一个控件的右边缘对齐
android:layout_alignTop:表示让一个控件的上边缘和另一个控件的上边缘对齐
android:layout_alignBottom:表示让一个控件的下边缘和另一个控件的下边缘对齐
FrameLayout
FrameLayout又称作帧布局,它相比于前面两种布局简单太多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角
layout_gravity:
left:左对齐
right:右对齐
top:上对齐
bottom:下对齐
特殊控件
自定义控件
新建Activity继承LinearLayout,这样就可以成为自定义的标题栏控件
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
LayoutInflater.from(context).inflate(R.layout.title, this)
}
}
在主构造函数中声明了Context和AttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数。
在init结构体中需要对标题栏布局进行动态加载,需要借助LayoutInflflater来实现
通过LayoutInflflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件
inflate()方法接收两个参数:第一个参数是要加载的布局文件的id,这里我们传入R.layout.title;第二个参数是给加载好的布局再添加一个父布局,这里要指定为TitleLayout,于是直接传入this
在布局文件中添加这个自定义控件,添加自定义控件和添加普通控件的方式基本是一样的,只不过在添自定义控件的时候,需要指明控件的完整类名,包名在这里是不可以省略的
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
最常用和最难用的控件:ListView
ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕。你其实每天都在使用这个控件,比如查看QQ聊天记录,翻阅微博最新消息,等等
加入ListView控件和之前的类似,先为ListView指定一个id,然后将宽度和高度都设置为match_parent
修改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,需要借助适配器来完成,这里使用ArrayAdapter适配器
ArrayAdapter有多个构造函数的重载,根据实际情况选择最合适的一种,这里指定String
在ArrayAdapter的构造函数中依次传入Activity的实例、ListView子项布局的id,以及数据源
android.R.layout.simple_list_item_1:是ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本
例:
实体类:
class Fruit(val name:String, val imageId: Int)
为ListView的子项指定一个自定义的布局:
<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类:
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) // 获取当前项的Fruit实例
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageId)
viewHolder.fruitName.text = fruit.name
}
return view
}
}
FruitAdapter定义了一个主构造函数,用于将Activity的实例、ListView子项布局的id和数据源传递进来
重写getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
LayoutInflater的inflate()方法接收3个参数:ListView子项布局的id、控件、第三个表示只让我们在父布局中声明的layout属性生效,但不会为这个View添加父布局
通过getItem()方法得到当前项的Fruit实例,并分别调用它的setImageResource()和setText()方法设置显示的图片和文字,最后将布局返回
提升ListView的运行效率:
增加内部类ViewHolder,用于对ImageView和TextView的控件实例进行缓存
在getView()方法中进行了判断:
当convertView为null:使用LayoutInflater去加载布局;并且创建一个ViewHolder对象,将控件的实例存放在ViewHolder里,然后调用View的setTag()方法,将ViewHolder对象存储在View中
当convertView不为null:则直接对convertView进行重用;然后调用View的getTag()方法,把ViewHolder重新取出
修改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
listView.setOnItemClickListener { _, _, position, _ ->
val fruit = fruitList[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
}
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函数:是Kotlin中常用的标准函数,它允许你传入一个数值_n_,然后会把Lambda表达式中的内容执行_n_遍
在onCreate()方法中创建了FruitAdapter对象,并将它作为适配器传递给ListView
增加ListView的点击事件:
使用setOnItemClickListener()方法为ListView注册了一个监听器
当用户点击了ListView中的任何一个子项时,就会回调到Lambda表达式中
通过position参数判断用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果的名字显示出来
在Lambda表达式中声明4个参数,但实际上却只用到了position这一个参数而已。针对这种情况,可以将没有用到的参数使用下划线(_)来替,不过参数之间的位置不能改变
:::
更强大的滚动控件:RecyclerView
RecyclerView:是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView存在的各种不足之处
添加RecyclerView库的依赖:
implementation 'androidx.recyclerview:recyclerview:1.2.1
加入RecyclerView控件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
RecyclerView指定一个id
宽度和高度都设置为match_parent,这样RecyclerView就占满了整个布局的空间
由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写出来
为RecyclerView准备一个适配器,这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.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() = fruitList.size
}
FruitAdapter中主构造函数,它用于把要展示的数据源传进来
定义一个内部类ViewHolder,继承RecyclerView.ViewHolder;ViewHolder的主构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局
通过findViewById()方法来获取布局中ImageView和TextView的实例了
FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法
onCreateViewHolder():用于创建ViewHolder实例的,在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入构造函数当中,最后将ViewHolder的实例返回
onBindViewHolder():用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行,这里通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可
getItemCount():用于告诉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 = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}
private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear", R.drawable.pear_pic))
fruitList.add(Fruit("Grape", R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
}
}
使用了一个initFruits()方法,用于初始化所有的水果数据
onCreate()方法中先创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中
LayoutManager用于指定RecyclerView的布局方式,这里使用的LinearLayoutManager是线性布局,可以实现和ListView类似的效果
创建FruitAdapter实例,并将水果数据传入FruitAdapter的构造函数中
调用RecyclerView的setAdapter()方法来完成适配器设置
LayoutManager制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局
布局横行排列:
调用LinearLayoutManager的setOrientation()方法设置布局的排列方向
默认是纵向排列的,传入LinearLayoutManager.HORIZONTAL表示让布局横行排列,这样RecyclerView就可以横向滚动了
瀑布流布局:
创建一个StaggeredGridLayoutManager实例
StaggeredGridLayoutManager的构造函数接收两个参数:
第一个参数用于指定布局的列数,传入3表示会把布局分为3列
第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列
增加点击事件:修改FruitAdapter中的onCreateViewHolder()方法代码
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked view ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked image ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
return viewHolder
}
分别为最外层布局和ImageView都注册了点击事件,itemView表示的就是最外层布局
在两个点击事 件中先获取了用户点击的position,通过position拿到相应的Fruit实例
使用Toast分别弹出两种不同的内容以示区别
编写界面的最佳实践
编写主界面,修改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"
android:background="#d8e0e8" >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
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>
创建一个RecyclerView用于显示聊天的消息内容
创建一个EditText用于输入消息,还创建一个Button用于发送消息
定义消息的实体类,新建Msg:
class Msg(val content: String, val type: Int) {
companion object {
const val TYPE_RECEIVED = 0
const val TYPE_SENT = 1
}
}
content表示消息的内容,type表示消息的类型
消息类型有两个值可选:TYPE_RECEIVED表示这是一条收到的消息,TYPE_SENT表示这是一条发出的消息
编写RecyclerView的子项布局,新建msg_left_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" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="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>
这是接收消息的子项布局。让收到的消息居左对齐
新建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" >
<LinearLayout
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_gravity="center"
android:layout_margin="10dp"
android:textColor="#000" />
</LinearLayout>
</FrameLayout>
这是发送消息的子项布局。让发出的消息居右对齐
创建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.from(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.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
override fun getItemCount() = msgList.size
}
首先定义了LeftViewHolder和RightViewHolder这两个ViewHolder,分别用于缓存 msg_left_item.xml和msg_right_item.xml布局中的控件
然后重写getItemViewType()方法,并在这个方法中返回当前position对应的消息类型
在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)
setContentView(R.layout.activity_main)
initMsg()
val layoutManager = LinearLayoutManager(this)
val recyclerView : RecyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = layoutManager
adapter = MsgAdapter(msgList)
recyclerView.adapter = adapter
val send : Button = findViewById(R.id.send)
send.setOnClickListener(this)
}
override fun onClick(v: View?) {
val send : Button = findViewById(R.id.send)
val recyclerView : RecyclerView = findViewById(R.id.recyclerView)
val inputText : EditText = findViewById(R.id.inputText)
when (v) {
send -> {
val content = inputText.text.toString()
if (content.isNotEmpty()) {
val msg = Msg(content, Msg.TYPE_SENT)
msgList.add(msg)
adapter?.notifyItemInserted(msgList.size - 1) // 当有新消息时,刷新RecyclerView中的显示
recyclerView.scrollToPosition(msgList.size - 1) // 将RecyclerView 定位到最后一行
inputText.setText("") // 清空输入框中的内容
}
}
}
}
private fun initMsg() {
val msg1 = Msg("Hello guy.", Msg.TYPE_RECEIVED)
msgList.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)
}
}
在initMsg()方法中初始化了几条数据用于在RecyclerView中显示
在发送按钮的点击事件里获取了EditText中的内容,如果内容不为空字符串,则创建一个新的Msg对象并添加到msgList列表中去
调用适配器的notifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示出来。或者你也可以调用适配器的notifyDataSetChanged()方法,它会将RecyclerView中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一些
调用RecyclerView的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息
调用EditText的setText()方法将输入的内容清空
运行程序: