
第一行代码学习日志-第四章
课本
书籍资源进入官网下载 ,PC端进入
第四章- 软件也要拼脸蛋,UI开发的点点滴滴
常见控件写法
常见公共属性
1 | 控件ID |
常用控件-文本(TextView)
在安卓中显示文本使用的控件是TextView
.若要使用它,在activity的布局文件中添加<TextView/>
标签即可
1 | <!-- |
常用控件-按钮(Button)
在安卓中显示文本使用的控件是Button
.若要使用它,在activity的布局文件中添加<Button/>
标签即可
1 | <!-- |
使用函数式API注册监听事件
1 | class MainActivity : AppCompatActivity() { |
使用接口实现监听
1 | // 使Activity也实现View.OnClickListener接口 |
常见控件-可编辑文本框(EditText)
EditText 它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理
1 | <!-- |
通过点击按钮来获取EditText文本
1 | class MainActivity : AppCompatActivity(), View.OnClickListener{ |
常见控件-图片(ImageView)
ImageView 是用于在界面上展示图片
的一个控件,它可以让我们的程序界面变得更加丰富多彩.图片通常是放在以drawable
开头的目录下的,并且要带上具体的分辨率。现在最主流的手机屏幕分辨率大多是xxhdpi
的,所以我们在res
目录下再新建一个drawable-xxhdpi
目录,然后将事先准备好的两张图片img_1.png 和img_2.png (在随书资源的源码\第4章\UIWidgetTest\app\src\main\res\drawable-xxhdpi
目录下)
制到该目录当中。
1 | <!-- |
使用代码更改src
1 | class MainActivity : AppCompatActivity(), View.OnClickListener{ |
常见控件-进度条(ProgressBar)
Progr essBar 用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。
1 | <ProgressBar |
使用代码控制进度条的可见性
1 | class MainActivity : AppCompatActivity(), View.OnClickListener{ |
此时,这个并不是进度条而是循环圆圈,我们可以给它加上style来变成进度条
1 | <!-- |
使用代码来更改进度,每次点击按钮加10
1 | class MainActivity : AppCompatActivity(), View.OnClickListener{ |
常见控件-消息弹窗(AlertDialog)
AlertDialog 可以在当前界面弹出一个对话框,这个对话框是置顶于所有界面元素之上
的,能够屏蔽其他控件的交互能力
,因此AlertDialog 一般用于提示一些非常重要的内容或者警告信息
。
1 | class MainActivity : AppCompatActivity(), View.OnClickListener{ |
其它控件
到此为止第一行代码三中的全部控件已经讲解完毕,其他的控件前往安卓官网指南 ->界面->外观和风格进行了解
基本布局
控件和布局的关系
线性布局-LinearLayout
这个布局会将它所包含的控件在线性方向(垂直或水平)上依次排列
基本结构
1 |
|
宽度平分-layout_weight
给在同一水平方向宽度为0dp
的控件加上此属性,会将在这一方向添加此属性的控件layout_weight的值总和N,除以每个控件layout_weight的值M,得到单个宽度.若同一方向有设置固定宽度或者wrap_content的控件,则在计算时只会占用剩余的空间
例如下面的layout_weight总和为5,按钮1和按钮3分别占2/5,按钮2占1/5.
1 | <?xml version="1.0" encoding="utf-8"?> |
相对布局-RelativeLayout
它可以通过相对定位
的方式让控件出现在布局的任何位置
。也正因为如此,RelativeLayout 中的属性非常多,不过这些属性都是有规律可循的,其实并不难理解和记忆。
根据父标签对齐
下面是一个简单的根据父标签上下左右居中对齐,属性见名知其意就不再赘述
1 |
|

根据同级标签对齐
下面是一个简单的根据同级标签上下左右居中对齐,属性见名知其意就不再赘述
注意: 被引用的标签一定要在最前面
1 |
|
约束布局-ConstraintLayout
由于ConstraintLayout 的特殊性,很难展示如何通过xml进行操作,所以使用可视化编辑器来对界面进行动态操作
要使用constraintLayout布局,先将根标签修改为如下:
1 |
|
为了更加简单的开发,将不再使用Android Studio的Code模式,将改为Design模式

由于无法进行文字描述,请前往哔哩哔哩学习
自定义控件
控件和布局的继承结构
所有布局
都是直接或间接继承
自ViewGroup
的.控件
其实就是在View
的基础上又添加
了各自特有的功能.而ViewGroup则是一种特殊
的View ,它可以包含
很多子View和子ViewGroup,是一个用于放置控件和布局的容器
。
引入布局
当一个控件在不同的地方被重复调用,当系统自带的控件并不能满足我们的需求时,可以利用上面的继承结构
来创建自定义控件
以实现控件的复用,就类似于Vue的Component.下面我们就来学习一下创建自定义控件的两种简单方法。先将准备工作做好,创建一个UICustomViews 项目,实现自定义标题栏控件.
- 在
res
目录下创建图片文件夹drawable-xxhdpi
目录,将配套资源中的源码\第4章\UICustomViews\app\src\main\res\drawable-xxhdpi\
复制 - 在
layout
目录下新建一个title.xml
布局
代码如下:title.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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="返回"
android:textColor="#fff"/>
<TextView
android:id="@+id/textText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center"
android:gravity="center"
android: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="编辑"
android:textColor="#fff"/>
</LinearLayout> - 引用
title.xml
若要引用改文件,则在对应xml文件中键入一下代码这里以activity_main.xml为例子1
<include layout="@layout/title"/>
1
2
3
4
5
6
7
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> - 覆盖原有标题栏
某一些设备可能存在自带的标题栏在对应activity.kt文件中使用如下代码隐藏掉.以MainActivity为例1
supportActionBar?.hide()
1
2
3
4
5
6
7class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportActionBar?.hide()
}
} - 更改主题
如果不更改主题可能会出现看不清等意外情况,打开res/values/themes.xml
文件.更改parent
为Theme.AppCompat.Light.DarkActionBar
.如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.UIConstomViews" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources> - 最终效果
自定义控件
引入布局
的技巧确实解决了重复编写布局代码的问题
,但无法响应事件
.
我们还是需要在每个Activity
中为这些控件单独编写
一次事件注册的代码
,不管是在哪一个Activity中,这个控件的功能都是相同的
,这就是称为自定义控件.
- 新建
TitleLayout.kt
文件让它继承LinearLayout
这里我们在TitleLayout的主构造函数中声明了1
2class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
}Context
和AttributeSet
这两个参数,在布局中引入TitleLayout 控件时就会调用这个构造函数。 - 然后在init结构体中需要对标题栏布局进行
动态加载
,这就要借助LayoutInflater
. 通过LayoutInflater 的from()
方法可以构建
出一个LayoutInflater对象
,然后调用inflate()
方法就可以动态加载
一个布局
文件。inflate()方法接收两个参数:- 第一个参数是要加载的
布局文件的id
,这里我们传入R.layout.title; - 第二个参数是给加载好的布局再添加一个
父布局
,这里我们想要指定为TitleLayout ,于是直接传入this。1
2
3
4
5class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
init {
LayoutInflater.from(context).inflate(R.layout.title,this)
}
}
- 第一个参数是要加载的
引用控件,在这里以
activity_main.xml
为例1
2
3
4
5
6
7
8
9
10
11
12
13
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<!--引用方式为全路径-->
<com.example.uiconstomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>为标题栏的返回按钮注册点击事件,修改`TitleLayout中的代码
1
2
3
4
5
6
7
8
9
10
11
12
13class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
init {
LayoutInflater.from(context).inflate(R.layout.title,this)
// 关键代码开始
val titleBack:Button = findViewById(R.id.titleBack)
titleBack.setOnClickListener{
// 将Context转为Activity类型
val activity = context as Activity
activity.finish()
}
// 关键代码结束
}
}当点击返回按钮时销毁当前Activity
- 为标题栏的编辑按钮注册点击事件,修改`TitleLayout中的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context,attrs){
init {
LayoutInflater.from(context).inflate(R.layout.title,this)
val titleBack:Button = findViewById(R.id.titleBack)
titleBack.setOnClickListener{
// 将Context转为Activity类型
val activity = context as Activity
activity.finish()
}
// 关键代码开始
val titleEdit = findViewById<Button>(R.id.titleEdit)
titleEdit.setOnClickListener{
Toast.makeText(context,"你点击了编辑按钮",Toast.LENGTH_LONG).show()
}
// 关键代码结束
}
}
注意
,TitleLayout 中接收的context参数实际上是一个Activity 的实例,在返回按钮的点击事件里,我们要先将它转换成Activity 类型,然后再调用finish()方法销毁当前的Activity 。Kotlin 中的类型强制转换使用的关键字是as,由于是第一次用到,可以看这里。
列表-ListView(最常用和最难用的控件)
ListView 的简单用法
- 首先新建一个ListViewTest 项目,然后修改
activity_main.xml
中的代码,如下所示:1
2
3
4
5
6
7
8
9
10
<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="wrap_content"/>
</LinearLayout> - 接下来修改
MainActivity
中的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class MainActivity : AppCompatActivity() {
// 这是提供给listView的数据,
// 这些数据可以从网上下载,也可以从数据库中读取,应该视具体的应用程序场景而定。
// 这里我们就简单使用一个data集合来进行测试
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)
/*
* 集合中的数据是无法直接传递给ListView 的,我们还需要借助适配器来完成
* Android中提供了很多适配器的实现类,其中第一行代码中认为最好用的就是ArrayAdapter
* 它可以通过 泛型 来指定要适配的数据类型,然后在构造函数中把要 适配的数据 传入。
* 在这里因为全是String所以泛型为String
* 然后在ArrayAdapter 的构造函数中依次传入Activity的实例、
* ListView 子项布局的id,以及数据源。
* 注意,我们使用了android.R.layout.simple_list_item_1作为ListView 子项布局的id,
* 这是一个Android内置的布局文件,里面只有一个TextView ,可用于简单地显示一段文本。
* */
val adapter = ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
val listView = findViewById<ListView>(R.id.listView)
// 最后,还需要调用ListView 的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView 和数据之间的关联就建立完成了。
listView.adapter = adapter
}
}
定制ListVie的界面
- 首先需要准备好一组图片资源,见配套资源
源码\第4章\ListViewTest\app\src\main\res\drawable-xxhdpi\
.复制里面的图片到res/drawable-xxhdpi文件夹. - 定义实体类
Fruit
用于作为适配器的适配类型
1
2
3// Fruit类中只有两个字段:name表示水果的名字,imageId表示水果对应图片的资源id。
class Fruit(name:String,imageId:Int) {
} - 要为ListView的子项指定一个我们
自定义的布局
,在layout 目录下新建fruit_item.xml
,代码如下所示:在这个布局中,我们定义了一个ImageView 用于显示水果的图片,又定义了一个TextView 用于显示水果的名称,并让ImageView 和TextView 都在垂直方向上居中显示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<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> - 要创建一个自定义的适配器,这个适配器继承自
ArrayAdapter
,并将泛型指定为Fruit
类。新建类FruitAdapter
,代码如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// FruitAdapter定义了一个主构造函数,用于将 Activity的实例 、ListView子项 布局 的id和 数据源 传递进来。
class FruitAdapter(activity: Activity,val resouceId: Int, data: List<Fruit>) :ArrayAdapter<Fruit>(activity, resouceId, data) {
// 重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
/*
*LayoutInflater的inflate()方法接收3个参数,
* 第一个表示int resource 代表需要加载资源的id、
* 第二个ViewGroup root 代表资源需要被添加的地方
* 第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不会为这个View 添加父布局。
* */
val view = LayoutInflater.from(context).inflate(resouceId,parent,false)
//获得单个列表的文本框
val fruitImage = view.findViewById<ImageView>(R.id.fruitImage)
//获得单个列表的图片
val fruitName = view.findViewById<TextView>(R.id.fruitName)
// 获得当前的Fruit实例
val fruit = getItem(position)
if(fruit!=null){
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
// 最后返回布局
return view
}
} - 接下来就是使用FruitAdapter,在MainActivity中修改模拟数据,将适配器换成FruitAdapter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class 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")
// 创建一个Fruit集合,将数据添加进去
private val fruitList = ArrayList<Fruit>().apply {
// repeat函数用于将代码重复执行
repeat(2) {
add(Fruit("Apple", R.drawable.apple_pic))
add(Fruit("Banana", R.drawable.banana_pic))
add(Fruit("Orange", R.drawable.orange_pic))
add(Fruit("Watermelon", R.drawable.watermelon_pic))
add(Fruit("Pear", R.drawable.pear_pic))
add(Fruit("Grape", R.drawable.grape_pic))
add(Fruit("Pineapple", R.drawable.pineapple_pic))
add(Fruit("Strawberry", R.drawable.strawberry_pic))
add(Fruit("Cherry", R.drawable.cherry_pic))
add(Fruit("Mango", R.drawable.mango_pic))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 换成自己的Fruit适配器
val adapter = FruitAdapter(this,R.layout.fruit_item,fruitList)
val listView = findViewById<ListView>(R.id.listView)
listView.adapter = adapter
}
}
提升ListView 的运行效率
之所以说ListView 这个控件很难
用,是因为它有很多细节可以优化
,其中运行效率就是很重要的一点。目前我们ListView 的运行效率是很低的,因为在FruitAdapter的getView()方法中,每次都将布局重新加载了一遍
,当ListView 快速滚动的时候,这就会成为性能的瓶颈。
在getView()方法中还有一个convertView
参数,这个参数会将之前加载好的view进行缓存
,以便之后进行复用
.我们修改FruitAdapter中的代码进行优化.
1 | class FruitAdapter(activity: Activity,val resouceId: Int, data: List<Fruit>) :ArrayAdapter<Fruit>(activity, resouceId, data) { |
ListView的点击事件
修改MainActivity 中的代码,如下所示:
1 | class MainActivity : AppCompatActivity() { |
更强大的滚动控件-RecyclerView
基本用法
- 检查是否存在依赖
最新版的Android Studio已经集成了RecyclerView,请在布局文件中输入RecyclerView查看是否有提示
若没有请参考这篇文章 添加依赖 - 修改
activity_main.xml
中的代码,如下所示:需要注意的是,由于RecyclerV iew 并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。1
2
3
4
5
6
7
8
9
10
<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="recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout> - 由于实现的效果一样,所以将ListView项目中的图片复制过来,同时也将Fruit类和fruit_item.xml复制过来.
- 为RecyclerView指定适配器,新建
FruitAdapter
类继承RecyclerView.Adapter
且泛型指定为FruitAdapter.ViewHolder
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// FruitAdapter的参数是传入数据源。
class FruitAdapter(val fruitList:List<Fruit>):RecyclerView.Adapter<FruitAdapter.ViewHolder>(){
// View参数通常是RecyclerView子项的最外层布局,这样就能在内部使用findViewById()找到ImageView和TextView.
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view){
val fruitImage : ImageView = view.findViewById(R.id.fruitImage)
val fruitName : TextView = view.findViewById(R.id.fruitName)
}
// 该方法用于将布局传入构造函数最后将加载好控件的ViewHolder实例返回
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item,parent,false)
return ViewHolder(view)
}
// 每个子项被滚到屏幕内时执行,通过position获得当前子项index.然后设置image和name
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 = fruitList.size
} - 适配器准备好了之后,我们就可以开始使用RecyclerView了,修改
MainActivity
中的代码,如
下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>().apply {
repeat(2) {
add(Fruit("Apple", R.drawable.apple_pic))
add(Fruit("Banana", R.drawable.banana_pic))
add(Fruit("Orange", R.drawable.orange_pic))
add(Fruit("Watermelon", R.drawable.watermelon_pic))
add(Fruit("Pear", R.drawable.pear_pic))
add(Fruit("Grape", R.drawable.grape_pic))
add(Fruit("Pineapple", R.drawable.pineapple_pic))
add(Fruit("Strawberry", R.drawable.strawberry_pic))
add(Fruit("Cherry", R.drawable.cherry_pic))
add(Fruit("Mango", R.drawable.mango_pic))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 创建一个线性布局对象,作用是设定布局方式为线性布局
val layoutManager = LinearLayoutManager(this)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
// 设置布局方式为线性布局
recyclerView.layoutManager = layoutManager
// 传入数据源
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}
} - 现在运行一下程序,效果如图所示。
可以看到,我们使用RecyclerView 实现了和ListView几乎一模一样的效果,虽说在代码量方面
并没有明显的减少,但是逻辑变得更加清晰了。
实现横向滚到和瀑布流布局
若要实现为了实现横向滚动的话,ListView就做不到了.这时候就需要使用RecyclerView来实现了.
- 修改
fruit_item.xml
,将LinearLayout中的对齐方式(orientation)修改为垂直排列.将LinearLayout 改成垂直方向排列,并把宽度设为80 dp 。这里将宽度指定为固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView 的子项就会有长有短,非常不美观,而如果用match_parent的话,就会导致宽度过长,一个子项占满整个屏幕。然后我们将ImageV iew 和Te xtView 都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一定距离1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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_marginLeft="10dp"/>
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="10dp"/>
</LinearLayout> - 修改
MainActivity
中的代码,如下所示:MainActivity 中只加入了一行代码,调用LinearLayoutManager 的setOrientation()方法设置布局的排列方向。默认是纵向排列的,我们传入LinearLayoutManager.HORIZONTAL表示让布局横行排列,这样RecyclerV iew 就可以横向滚动了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>().apply {
repeat(2) {
add(Fruit("Apple", R.drawable.apple_pic))
add(Fruit("Banana", R.drawable.banana_pic))
add(Fruit("Orange", R.drawable.orange_pic))
add(Fruit("Watermelon", R.drawable.watermelon_pic))
add(Fruit("Pear", R.drawable.pear_pic))
add(Fruit("Grape", R.drawable.grape_pic))
add(Fruit("Pineapple", R.drawable.pineapple_pic))
add(Fruit("Strawberry", R.drawable.strawberry_pic))
add(Fruit("Cherry", R.drawable.cherry_pic))
add(Fruit("Mango", R.drawable.mango_pic))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val layoutManager = LinearLayoutManager(this)
// 设置排列方向是垂直
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}
} 实现瀑布流布局
修改一下
fruit_item.xml
中的代码,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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="10dp">
<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="10dp"/>
<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginLeft="10dp"/>
</LinearLayout>这里做了几处小的调整,首先将LinearLayout 的宽度由80 dp 改成了
match_parent
,因为瀑
布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。其次我们使用了layout_margin
属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一些。最后
还将TextView 的对齐属性改成了居左对齐
,因为待会我们会将文字的长度变长,如果还是居中
显示就会感觉怪怪的.接着修改
MainActivity
中的代码,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46package com.example.recyclerviewtest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
class MainActivity : AppCompatActivity() {
private val fruitList = ArrayList<Fruit>().apply {
repeat(2) {
add(Fruit(getRandomLengthString(getRandomLengthString("Apple")), R.drawable.apple_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Banana")), R.drawable.banana_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Orange")), R.drawable.orange_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Watermelon")), R.drawable.watermelon_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Pear")), R.drawable.pear_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Grape")), R.drawable.grape_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Pineapple")), R.drawable.pineapple_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Strawberry")), R.drawable.strawberry_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Cherry")), R.drawable.cherry_pic))
add(Fruit(getRandomLengthString(getRandomLengthString("Mango")), R.drawable.mango_pic))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 更改为网格布局
val layoutManager = StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}
// 将水果名字随机重复。
private fun getRandomLengthString(str:String):String{
val n = (1..20).random()
val builder = StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}
}StaggeredGridLayoutManager的构造函数接收两个参数:
- 第一个参数用于指定布局的列s数,传入3表示会把布局分为3列
- 第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列。
这时候运行项目你大概会看到这样的效果
RecyclerView的点击事件
RecyclerView 并没有
提供类似于setOnItemClickListener()这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件
。这相比于ListView 来说,实现起来要复杂一些。
RecyclerView摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View 去注册.
- 下面我们来具体学习一下如何在RecyclerView 中注册点击事件,修改
FruitAdapter
中的代码,如下所示:可以看到,这里我们是在onCreateViewHolder()方法中注册点击事件。上述代码分别为最外层布局和ImageView 都注册了点击事件,itemView 表示的就是最外层布局。RecyclerView的强大之处也在于此,它可以轻松实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47package com.example.recyclerviewtest
import android.media.Image
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
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)
// 将view转换为viewHolder
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener {
// 提取当前被点击view的position
val position = viewHolder.adapterPosition
// 提取出当前fruit对象
val fruit = fruitList[position]
Toast.makeText(parent.context,"你点击了文本${fruit.name}",Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context,"你点击了图片${fruit.name}",Toast.LENGTH_SHORT).show()
}
return viewHolder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}
override fun getItemCount(): Int = fruitList.size
}子项中任意控件或布局的点击事件
。我们在两个点击事件中先获取了用户点击的position ,然后通过position 拿到相应的Fruit实例,再使用Toast 分别弹出两种不同的内容以示区别。
编写界面的最佳实践
既然已经学习了那么多UI开发的知识,是时候实战一下了。这次我们要综合运用前面所学的大量内容来编写出一个较为复杂且相当美观的聊天界面,你准备好了吗?要先创建一个UIBestPractice
项目才算准备好了哦。
制作9-Patch (点9)图片
9-Patch你之前可能没有听说过这个名词,它是一种被特殊处理过的png 图片,能够指定
哪些区域
可以被拉伸
、哪些区域不可以。接下来还是通过一个例子来了解一下它吧!
- 复制资料的
源码\第4章\UIBestPractice\app\src\main\res\drawable-xxhdpi\
下的message_left_original.png
和message_right_original.png
文件到res\drawable-xxhdpi
,如图所示. - 制作作9-Patch 图片其实并不复杂,只要掌握好规则就行了,那么现在我们就来学习一下。
在Andr oid Studio 中,我们可以将任何png 类型的图片制作成9-Patch 图片。首先对着message_left_original.png
图片右击→Create 9-P atch file
,会弹出如图所示的对话框。这里保持默认文件名就可以了,其实就相当于创建了一张以9.png 为后缀的同名图片,点击“Save” 完成保存。
这时Andr oid Studio 会显示如图所示的编辑界面。
- 在体魄四个边框按鼠标左键可以进行边框绘制,被标黑的区域表示该方向图片可以被拉伸,按shift进行拖动可以取消标记.
左键标记
shift+左键取消标记
自行修改图片left和right或者将资料中的.9文件复制即可.
编写精美的聊天界面
既然是要编写一个聊天界面,那肯定要有收到的消息和发出的消息。
接下来开始编写主界面,修改
activity_main.xml
中的代码,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!--
使用线性布局作为约束
垂直分部子控件
设置淡灰色作为聊天背景
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="#d8e0e8"
>
<!-- 使用RecyclerView(约束布局)来束缚聊天信息 -->
<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="在这输入消息"
android:maxLines="2"
/>
<!-- 发送按钮-->
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"/>
</LinearLayout>
</LinearLayout>然后定义消息的实体类,新建
Msg.kt
,代码如下所示:1
2
3
4
5
6class 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
,代码如下所示:里我们让收到的消息居1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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:gravity="center"
android:layout_margin="10dp"
android:textColor="#fff"/>
</LinearLayout>
</FrameLayout>左对齐
,并使用message_left.9.png
作为背景图。 - 接下来开始编写个发送消息的子项布局,新建
msg_right_item.xml
,代码如下所示:里我们让收到的消息居1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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:gravity="left"
android:layout_margin="10dp"
android:textColor="#000000"/>
</LinearLayout>
</FrameLayout>右对齐
,并使用message_right.9.png
作为背景图。 - 创建
RecyclerView
的适配器类,新建类MsgAdapter
,代码如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package com.example.uibaseproject
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
class MsgAdapter(val msgList:List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
// 左边控件内部类,用于缓存控件
inner class LeftViewHolder(view: View,val leftMsg : TextView=view.findViewById(R.id.leftMsg)) : RecyclerView.ViewHolder(view)
// 右边控件内部类,用于缓存控件
inner class RightViewHolder(view: View,val rightMsg : TextView=view.findViewById(R.id.rightMsg)) : RecyclerView.ViewHolder(view)
// 获得是接受还是发送
override fun getItemViewType(position: Int): Int = msgList[position].type
// 根据不同类型来加载不同布局
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when(viewType){
Msg.TYPE_RECEIVED -> LeftViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item,parent,false))
else -> RightViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item,parent,false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when(holder){
is LeftViewHolder -> holder.leftMsg.text = msg.context
is RightViewHolder -> holder.rightMsg.text = msg.context
else -> throw IllegalArgumentException()
}
}
override fun getItemCount(): Int = msgList.size
} - 最后修改
MainActivity
中的代码,为RecyclerView初始化一些数据,并给发送按钮加入事件响应,代码如下所示:msgList中初始化了几条数据用于在RecyclerV iew 中显示,接下来按照标准的方式构建RecyclerView ,给它指定一个LayoutManager 和一个适配器。然后在发送按钮的点击事件里获取了EditText 中的内容,如果内容不为空字符串,则创建一个新的Msg对象并添加到msgList 列表中去。之后又调用了适配器的notifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerV iew 中显示出来。或者你也可以调用适配器的notifyDataSetChanged()方法,它会RecyclerV iew 中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一些。接着调用RecyclerV iew 的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息。最后调用EditTe xt 的setText()方法将输入的内容清空。这样所有的工作都完成了,终于可以检验一下我们的成果了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class MainActivity : AppCompatActivity() {
//
private val msgList = ArrayList<Msg>().apply {
add( Msg("Hello",Msg.TYPE_RECEIVED))
add( Msg("HI",Msg.TYPE_SENT))
add( Msg("小明",Msg.TYPE_RECEIVED))
add( Msg("小红",Msg.TYPE_SENT))
add( Msg("在做什么",Msg.TYPE_RECEIVED))
add( Msg("写代码",Msg.TYPE_SENT))
}
private var adapter:MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val layoutManager = LinearLayoutManager(this)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = layoutManager
adapter = MsgAdapter(msgList)
recyclerView.adapter = adapter
val send = findViewById<Button>(R.id.send)
send.setOnClickListener{
val inputText = findViewById<EditText>(R.id.inputText)
val content = inputText.text.toString()
if(content.isNotEmpty()){
val msg = Msg(content,Msg.TYPE_SENT)
msgList.add(msg)
adapter?.notifyItemInserted(msgList.size-1)
recyclerView.scrollToPosition(msgList.size -1)
inputText.setText("")
}
}
}
}
运行程序之后,你将会看到非常美观的聊天界面,并且可以输入和发送消息,如图所示。 - 本篇代码示例
Github
- 标题: 第一行代码学习日志-第四章
- 作者: noDream
- 创建于: 2022-09-21 09:39:12
- 更新于: 2023-05-29 23:36:58
- 链接: https://007666.xyz/2022/09/21/第一行代码学习日志-第四章/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。