UI开发
基础知识:
- dp,这是一种屏幕密度无关的尺寸单位,可以保证在不同分辨率的手机上显示效果尽可能地一致。
- 文字大小要使用sp作为单位,这样当用户在系统中修改了文字显示尺寸时,应用程序中的文字大小也会跟着变化;
- Android控件的可见属性,可以通过android:visibility进行指定,可选值有3种:visible、invisible和gone。
- visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的;
- invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了;
- gone则表示控件不仅不可见,而且不再占用任何屏幕空间。
控件
基础知识:
- android:gravity来指定文字的对齐方式,可选值有top、bottom、start、end、center等,可以用“|”来同时指定多个值;
TextView
无赘述。
Button
Android系统默认会将按钮上的英文字母全部转换成大写,可能是认为按钮上的内容都比较重要吧。如果这不是你想要的效果,可以在XML中添加android:textAllCaps=”false” 这个属性。
注册监听器的两种方式:
Java函数式API;
1
2
3
4
5
6
7
8
9class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
// 在此处添加逻辑
}
}
}使用实现接口的方式来进行注册。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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
- 可以使用android:hint属性指定一段提示性的文本;
- 可以使用android:maxLines指定最大行数。
ImageView
现在最主流的手机屏幕分辨率大多是xxhdpi的,需要在res目录下新建一个drawable-xxhdpi目录。
ProgressBar
- android:max属性给进度条设置一个最大值;
- style属性可以指定成进度条样式。
AlertDialog
AlertDialog可以在当前界面弹出一个对话框。调用setPositiveButton()方法为对话框设置确定按钮的点击事件,调用setNegativeButton()方法设置取消按钮的点击事件。
布局
LinearLayout
LinearLayout又称作线性布局。
- 如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent;如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
- 当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。
android:layout_weight这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用,本质是按照数值和进行归一化分配比例。如果是水平布局,使用android:layout_weight后,控件的宽度就不应该再由android:layout_width来决定了,直接赋值0dp即可;垂直布局同理。
RelativeLayout
RelativeLayout又称作相对布局。每个控件都是相对于父布局进行定位的,也可以相对于控件进行定位。
一个有两组属性,一组按照边缘定位(如:android:layout_alignLeft),一组按照中心(如: android:layout_toLeftOf)定位。
当一个控件去引用另一个控件的id时,该控件一定要定义在引用控件的后面,不然会出现找不到id的情况。
FrameLayout
FrameLayout又称作帧布局,简单且应用场景少。以使用layout_gravity属性来指定控件在布局中的对齐方式。
自定义控件
所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,各种控件其实就是在View的基础上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。
引入布局
如果在每个Activity的布局中都编写一遍同样的布局代码,明显就会导致代码的大量重复。这时我们就可以使用引入布局的方式来解决这个问题。
只需要通过一行include语句引入布局。
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
创建自定义控件
这里我们在TitleLayout的主构造函数中声明了Context(本质就是Activity的实例)和AttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件。
1 | class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { |
添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,我们需要指明控件的完整类名,包名在这里是不可以省略的。
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
注意,TitleLayout中接收的context参数实际上是一个Activity的实例,在返回按钮的点击事件里,我们要先将它转换成Activity类型,然后再调用finish()方法销毁当前的Activity。Kotlin中的类型强制转换使用的关键字是as。
1 | class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { |
ListView
简单用法
数据是无法直接传递给ListView的,需要借助适配器来完成。Android中提供了很多适配器的实现类,如ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。在ArrayAdapter的构造函数中依次传入Activity的实例、ListView子项布局的id,以及数据源。
还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成。
定制界面
创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为数据类。定义一个主构造函数,用于将Activity的实例、ListView子项布局的id和数据源传递进来(类似标准ArrayAdapter)。另外又重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
接着在onCreate()方法中创建 了适配器对象,并将它传递给ListView,这样定制ListView界面的任务就完成了。
提升运行效率
getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后进行重用,我们可以借助这个参数来进行性能优化,避免重复加载布局。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class 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: View
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
} else {
view = convertView
}
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
val fruit = getItem(position) // 获取当前项的Fruit实例
if (fruit != null) {
fruitImage.setImageResource(fruit.imageId)
fruitName.text = fruit.name
}
return view
}
}借助ViewHolder来对性能进行优化,避免重复调用View的findViewById()方法来获取一次控件的实例。
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
26class 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
}
}新增一个内部类ViewHolder,用于对ImageView和TextView的控件实例进行缓存。然后调用View的setTag()方法,将ViewHolder对象存储在View中。
点击事件
1 | class MainActivity : AppCompatActivity() { |
使用setOnItemClickListener()方法为ListView注册了一个监听器,这是一个Java单抽象方法接口,故使用Java函数式API编程。Kotlin允许我们将没有用到的参数使用下划线来替代。
RecyclerView
基本用法
RecyclerView是一个增强版的 ListView。RecyclerView属于新增控件,考虑到兼容性,Google将RecyclerView控件定义在了AndroidX 当中,我们需要在项目的build.gradle中添加RecyclerView库的依赖。
1 | dependencies { |
为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。其中,ViewHolder是我们在FruitAdapter中定义的一个内部类。
1 | class FruitAdapter(val fruitList: List<Fruit>) : |
由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法。onCreateViewHolder()方法是用于创建ViewHolder实例;onBindViewHolder()方法用于对RecyclerView子项的数据进行赋值,会在每个子项被滚动到屏幕内的时候执行;getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
在onCreate()方法中先创建了一个LinearLayoutManager对象,并将它设置到RecyclerView当中。最后调用 RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成。
实现横向滚动
MainActivity中只加入了一行代码,调用LinearLayoutManager的setOrientation()方法设置布局的排列方向。默认是纵向排列的。
其他布局
ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager。LayoutManager制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。
不赘述。
点击事件
RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法。RecyclerView直接摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View去注册,在onCreateViewHolder()方法中注册点击事件。
1 | class FruitAdapter(val fruitList: List<Fruit>) : |
编写页面
9-Patch图片
它是一种被特殊处理过的png图片,能够指定哪些区域可以被拉伸、哪些区域不可以。
制作过程不赘述。
要将原来的.png图片删除,只保留制作好的.9.png图片。Android项目中不允许同一文件夹下有两张相同名称的图片(即使后缀名不同也不行)。
聊天界面
定义常量的关键字是const,注意只有在单例类、companion object或顶层方法中才可以使用const关键字。
根据不同的viewType创建不同的界面。要重写 getItemViewType()方法,并在这个方法中返回当前position对应的消息类型。
使用适配器的notifyItemInserted()方法,用于通知列表有新的数据插入,这样新增的一条消息才能够在RecyclerView中显示出来。或者调用适配器的notifyDataSetChanged()方法,它会将RecyclerView中所有可见的元素全部刷新,这样不管是新增、删除、还是修改元素,界面上都会显示最新的数据,但缺点是效率会相对差一些。
调用RecyclerView的scrollToPosition()方法将显示的数据定位到最后一行,以保证一定可以看得到最后发出的一条消息。最后调用EditText的setText()方法将输入的内容清空。
具体代码不赘述。
Kotlin课堂
主要讲解延迟初始化和密封类。
延迟初始化
延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了,同时类型声明也不用添加?,也不再需要进行判空处理。
使用lateinit的风险:变量还没有初始化的情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个UninitializedPropertyAccessException异常。
::adapter.isInitialized
可用于判断adapter变量是否已经初始化,这是固定写法。
密封类
由于Kotlin的语法规定,if语句必须要以else结束,否则编译器会认为这里缺少条件分支,产生代码冗余;同时写else条件还有一个潜在的风险,如果我们现在新增了一个Unknown类并实现接口,用于表示未知的执行结果,但是忘记在方法中添加相应的条件分支,编译器在这种情况下是不会提醒我们的,而是会在运行的时候进入else条件里面,从而抛出异常并导致程序崩溃。
密封类的关键字是sealed class,密封类是一个可继承的类,因此在继承它的时候需要在后面加上一对括号。
当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。
另外再多说一句,密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。