Kotlin 中所有类都继承该 Any 类,它是所有类的超类,对于没有超类型声明的类是默认超类
如果子类有主构造函数, 则基类必须在主构造函数中立即初始化
子类继承父类时,不能有跟父类同名的变量,除非父类中该变量为 private,或者父类中该变量为 open 并且子类用 override 关键字重写
inflate 方法的原理:https://blog.csdn.net/guolin_blog/article/details/12921889
Android的Activity 、 Window 、 View之间的关系:https://www.jianshu.com/p/982ec37832bf
MVC 模式:对应到 Android 开发中,View 约等于 Layout 中的 xml ,Controller 就是 Activity,Model 同上。
有时,我们为了APP中节省空间,在能用颜色替代的地方就不要用图片,而如何将颜色组织成想要的形状及如何为指定的颜色添加描边、渐变等来模拟图片就显的极为重要了,这些就是靠shape来完成的。
https://blog.csdn.net/harvic880925/article/details/41850723
一个module被编译时,会生成一个当前module的R文件
HelloWorld工程中的R.java文件:
public final class R {
public static final class attr {
}
public static final class drawable {
public static final int icon=0x7f020000;
}
public static final class layout {
public static final int main=0x7f030000;
}
public static final class string {
public static final int app_name=0x7f040001;
public static final int hello=0x7f040000;
}
}
val:声明不可变变量,相当于 Java 中的 final
var:声明可变变量
声明时显示指定类型:val a:Int =10
as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,使用as?运算符就会返回值null。在Kotlin中,父类是禁止转换为子类型的。
if 条件语句:
和 Java 相比有返回值:返回值是每一个条件语句中最后一行代码的返回值
例如:
fun largerNumber(num1:Int,num2:Int):Int{
val num=if(num1>num2) num1
else num2
return num
}
fun main(){
println(largerNumber(2,8));
}
返回值是 8
对 else 语句稍做修改:
fun largerNumber(num1:Int,num2:Int):Int{
val num=if(num1>num2) num1
else
{
num2
10
}
return num
}
返回值是 10,即 else 语句的最后一行代码
以上代码可以用 kotlin 语法糖进行简化:
fun largerNumber(num1:Int,num2:Int)=if(num1>num2) num1 else num2
和 Java 不同的是在 kotlin 中,任何一个非抽象类都是不可继承的。抽象类和 Java 无区别
在类前面加上关键字 open
就可以让一个类可以被继承,如:
open class Person {
}
class Student(name:String,age:Int):Person(name,age) {
}
注意:以上 Student 类的主构造函数中的两个变量不能声明为 val 或者 var 。因为声明为 var 或者 val 的话,参数就会自动成为该类的字段,这就会导致和父类中同名的 name 和 age 字段造成冲突。不使用任何关键字修饰的话,他的作用范围就仅仅在主构造函数之中(XXXXX)
括号的意义: 子类中的构造函数必须调用父类中的构造函数,但是主构造函数没有函数体,只能使用 init 结构体去调用父类的构造函数,但大多时候,我们并不使用 init ,于是直接在继承的时候通过括号指定调用父类中的哪个构造函数。
主构造函数的意义?(XXXXX)
Tip:
1、当一个类没有显示声明主构造函数且定义了次构造函数时,它就是没有主构造函数的,次构造函数直接调用父类的构造函数即可
2、为什么要分为主构造函数和次构造函数?(XXXXX)
在类前面声明 data 关键字,即表明这是一个数据类,他就会自动生成如 equals()、hashCode()、toString() 等固定且无实际逻辑意义的方法,如下:
data class Cellphone(var brand:String,var price:Double)
把一个类的 class 关键字改成 object ,即表明这是一个单例类,如下:
object Singleton {
}
在 kotlin 中,集合主要有 List、Set、Map
List 的创建方法和 Set 差不多,有两种方式,可变的和不可变的创建。不可变是指集合一旦创建就不能再往里面添加、修改、删除元素
LIst :
不可变的写法:
fun main(){
//println(largerNumber(2,8));
var list=listOf("a","b",10)
for(i in list) println(i)
}
可变的写法:
fun main(){
//println(largerNumber(2,8));
var list=mutableListOf("a","b","10")
list.add("c")
for(i in list) println(i)
}
set 和 list 差不多写法,对应:setOf() 和 mutableOf()
map 和 list、set 有较大的不同,和 Java 的区别就是可以写成如下形式:
fun main(){
var map=HashMap<String,Int>()
map["a"]=1
map["b"]=2
for((s,i) in map) println(s+" "+i)
}
声明方式也可以如下所示:
fun main(){
var map= mapOf<String,Int>("a" to 1,"b" to 2)
for((s,i) in map) println(s+" "+i)
}
lambda 表达式的语法结构如下:
参数名1:参数类型,参数名2:参数类型 -> 函数体
以上的语法可以简化,lambda 表达式是方法的最后一个参数的时候,可以把大括号提出去,如果是唯一一个参数,还可以省略方法的括号。lambda 表达式的参数可以省略类型,只有一个参数的时候,还可以用 it 代替
常用的集合 API:
list.maxBy() // 取 list 中 length 最长的数据项
list.map() // 将 list 集合中的数据项映射成另一种形式
list.filter() // 按 filter 传入的条件进行过滤
Kotlin 在调用 Java 方法的时候,也可以使用 lambda 表达式,不过只有 Java 的函数式接口能使用 lambda 表达式。例如 Java 中的 Runnable 接口
Java 中常规写法:
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("Running");
}
}).start();
在 kotlin 中的常规写法:
Thread(object : Runnable{
override fun run(){
println("Running");
}
}).start();
kotlin 简化写法:
在调用Java中的函数式接口的时候,将Java接口名作为 lambda 表达式的方法名,如下:
Thread(Runnable{
System.out.println("Running");
}).start();
因为 Thread 可以只有 Runnable 这一个参数,所以 Runnable 也可以省略,如下:
Thread({
System.out.println("Running");
}).start();
且因为 Runnable 是 Thread 方法的最后一个参数,所以优化如下,去掉 Thread 方法的括号
Thread{
System.out.println("Running");
}.start();
在 Java 中,空指针异常会在运行时作为异常抛出,但是在 Kotlin 中,对空指针异常的检测提前到了编译器,也就是在编译期间,如果你某个属性可能会 null 值,编译就不会通过
如果某个类型可以为空,那么你可以用 ?
来告诉编译器,这是一个可以为 null 的属性。例如:a: String?
.?
相当于如下代码,如果 a 对象为空,则什么都不做,否则,doSometing
if(a!=null) a.doSomething()
等价于
a?.doSomething()
?:
相当于如下代码,如果左边表达式不为空的,就返回左边表达式的值,否则返回右边表达式的值
var c=if(a!=null) a else b
等价于
var c=a?:b
let
是一个函数,语法如下:
obj.let{
obj2-> 具体代码
}
注意,obj2 和 obj 就是同一个对象
使用 .? 的方式相当于对每个语句进行 if 判断,这样降低代码效率。 let 的出现就是为了栏多个语句进行一次判空操作即可
obj.?obj.doSome1()
obj.?obj.doSome2()
等价于
if(obj!=null) obj.doSome1()
if(obj!=null) obj.doSome2()
obj?.let{
it.dosome1()
it.dosome2()
}
等价于
if(obj!=null)
{
obj.doSome1()
obj.doSome2()
}
使用 ${}
表达式,在运行的时候,${} 的执行结果会替换掉这个表达式,如:
"hello ${obj.name}"
如果传入参数,obj.name 为 张三
那么,上述表达式等价于
"hello 张三"
但表达式中只有一个变量的时候,{}
可省略,即 $obj.name
即可。
次构造函数的主要功能是为函数的参数设置默认值,但是并不常用。函数的参数默认值可以使用键值对的方式直接指定。
赋值的时候,如果指定了默认值得那个参数可以不用传值,跳过。如果指定默认值的哪个参数不在最后一位,用键值对的方式赋值
在 xml 中,@id/button1
是引用一个 id,@id/button1
则是定义一个 id
<Button
android:id="@+id/button1" 唯一标识符
android:layout_width="match_parent" 当前元素和父元素一样宽
android:layout_height="wrap_content" 刚好把文字包裹住
android:text="button1" 指定元素中显示的中文内容
/>
演示效果如下:
在 Activity 中加载这个布局:
在 FirstActivity 中
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//添加这句话,意思是,给当前 Activity 添加一个布局
setContentView(R.layout.first_layout) //任何资源都会在 R 文件中生成一个 id
}
}
接下来,所有的 Activity 都应该在 AndroidMainfest 中进行注册,但是 AS(以下 Android Studio 简称 AS)帮我们写好了,不用管,如下蓝色的语句就是进行注册,.FirstActivity 中 .
的前面是包路径,只是缩写了:
接下来,把 FirstActivity 设置成主 Activity,也即程序跑起来默认的那个 Activity,如下:
演示效果如下:
Toast 就是在页面中弹出提示信息,并且不占用任何屏幕空间。
1、首先要有 Toast 的出发点,上面例子中的 button1 就可以
2、在上面例子的基础上修改代码如下
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//添加这句话,意思是,给当前 Activity 添加一个布局
setContentView(R.layout.first_layout) //任何资源都会在 R 文件中生成一个 id
//findViewById :获取布局文件中定义的元素,需要显示的声明 button1 的类型为 Button,因为推断不出来
var button1: Button =findViewById(R.id.button1)
button1.setOnClickListener{
//三个参数分别是:Toast要求的上下文;Toast 显示的文本内容;Toast 显示的时长
// 第三个参数一般有两个取值:Toast.LENGTH_SHORT 和 Toast.LENGTH_LONG
Toast.makeText(this,"点击了 button1",Toast.LENGTH_SHORT).show()
}
}
}
演示效果如下:
弹出了提示:点击了 button1
关于 findViewById()
这个方法,Kotlin 对他进行了一些操作,让他变得简洁,不用每次都写一次这句话来找到某个组件。kotlin 在 gradle 文件的头部默认引入了一个插件,这个插件会根据布局文件中定义的控件 id 自动生成一个具有相同名称的变量,因此我们可以直接在 Activity 中直接使用这个变量,而不用每次都去调用 findViewById() 方法
改进后代码如下:
注意:在使用 kotlin-android-extensions 插件的时候,需要在 builld.grade(模块下那个)添加以下语句:
最后一句话的意思是,把某些关闭了的功能都打开
1、新建 menu 文件夹
2、在 menu 文件夹下新建Menu resource file
3、在 main.xml 中添加代码
直接 control+O 选择 onCreateOptionsMenu 方法,就可以对他进行重写
直接调用 finish() 方法即可
Intent 的作用是从一个 Activity 跳转到另一个 Activity
1、新建一个空的 Actvity 文件,并修改自动生成的 xml 文件为如下内容
2、在 FirstActivity 中代码如下:
演示结果:
点击BUTTON1之后,自动跳转到如下页面:
使用 :intent.data=Uri.parse("tel:10086")
可以调用系统的拨号界面
使用 intent.putExtra("键","值")
方法向下传递,另一个页面使用 intent.getStringExtra("键")
进行获取对应的 value
在 FirstAvtivity 中:
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout) //任何资源都会在 R 文件中生成一个 id
button1.setOnClickListener{
val intent=Intent(this,SecondActivity::class.java)
//期望在 Activity 销毁的实施能够返回一个结果给上一个 Activity
// 参数:1、传递数据的 intent 2、请求吗,用于回调的时候判断数据的来源
startActivityForResult(intent,1)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
//1、requestCode 就是上面那个 1 ;2、第二个页面传回来的处理结果,状态码 3、携带数据返回的 Intent
// 第二个参数一般只有两个取值: RESULT_OK 或者 RESULT_CANCELED
super.onActivityResult(requestCode, resultCode, data)
when(requestCode){
1->if(requestCode== RESULT_OK) {
val returnedData = data?.getStringExtra("data_return")
Log.d("FirstActivity","返回的数据是 $returnedData")
}
}
}
}
在 SecondActivity 中
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
button2.setOnClickListener{
val intent = Intent()
intent.putExtra("data_return","向上传回给FirstActivity")
//这个方法专门用于向上一个 Activity 返回数据
//参数:1、返回结果 2、把带有数据的 intent 返回回去
setResult(Activity.RESULT_OK,intent)
finish()
}
}
}
那个 1 就相当于标识 firstActivity ,因为一个 Activity 可能回调多个 ,1 就是用来确定,你确实是回调给我的
演示效果:点击 button1 跳转到 SecondActivity ,再点击 button2 就跳转回来,并在控制台打印那句话Lod.d()里面那句
Android 是使用任务(task)来管理 Activity 的,一个任务就是一组存放在栈里的 Activity 集合。每当我们点击 Back 键或者调用 finish() 方法的时候,栈顶的 Activity 就会出栈,前一个入栈的 Activity 就会出现在栈顶的位置。系统总是显示处于栈顶的 Activity 给用户
每个 Activity 的生命周期中最多会有 4 种状态:
1、运行状态:位于返回栈栈顶的 Activity 就会处于运行状态
2、暂停状态:Activity 不在栈顶的位置,但是仍然处于用户可见的状态。比如界面弹出了一个对话框,但是对话框下的哪个页面我们一般还是看得见的,对话框下面哪个页面就是暂停状态
3、停止状态:Activity 不在栈顶的位置,且用户不可见的状态就是停止状态。处于停止状态的 Activity 可能会被系统回收
4、销毁状态:当 Activity 从返回栈中被移除后就变成了销毁状态,系统最倾向于回收处于这种状态的 Activity
Activity 类中定义了 7 个回调方法,覆盖了 Activity 生命周期的每一个环节
内容对应如下:
NormalActivity 和 DialogActivity 不做修改,创建好就行
MainActivity 的内容如下:
class MainActivity : AppCompatActivity() {
private val tag="MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag,"onCreate")
setContentView(R.layout.activity_main)
startNormal.setOnClickListener{
val intent= Intent(this,NormalActivity::class.java)
startActivity(intent)
}
startDialog.setOnClickListener{
val intent= Intent(this,DialogActivity::class.java)
startActivity(intent)
}
}
override fun onStart() {
super.onStart()
Log.d(tag,"onStart")
}
override fun onResume() {
super.onResume()
Log.d(tag,"onResume")
}
override fun onPause() {
super.onPause()
Log.d(tag,"onPause")
}
override fun onStop() {
super.onStop()
Log.d(tag,"onStop")
}
override fun onDestroy() {
super.onDestroy()
Log.d(tag,"onDestory")
}
override fun onRestart() {
super.onRestart()
Log.d(tag,"onDestory")
}
}
对应的 xml 文件:
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">
<Button
android:id="@+id/startNormal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="start normal"
/>
<Button
android:id="@+id/startDialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="start dialog"
/>
LinearLayout>
演示效果:
点击按钮 start normal:
他会先出现 onPause ,过一会可能会出现 onStop
点击Back ,返回上一个页面:当前页面会被销毁(NormalActivity 会出栈,MainActivity 会出现在栈顶,所以 onStart,并且运行onResume)
当你第一次点击 start dialog 的时候:
只有 onPause ,没有 onStop ,也就是说 MainActivity 还是可见的,而且没有 onStart
当一个 Activity 进入了停止状态,有可能会被系统回收,但是有的时候,用户点击回退,是希望这个页面数据还在的,由于Activity被回收了,数据就不在了,需要 onCreate ,用户重新填写,影响体验
在 Activity 中有一个回调方法 onSaveInstanceState()
,该方法携带 Bundle 类型的参数,Bundle 提供了一系列方法用于保存数据。比如可以使用 putString 保存字符串,使用 putInt 保存整型数据等等。每个保存方法需要传入两个参数,第一个是键,第二个是值,用于后面从 Bundle 中取值
在 MainActivity 中添加如下代码就可以将数据保存下来了:
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState, outPersistentState)
val tempData="需要被保存下来的数据"
outState.putString("data_key",tempData)
}
数据恢复:
注意到每个 onCreate 方法都有一个 Bundle 类型的参数,这个参数一般情况下为 null ,但是如果在 Activity 回收之前,你通过 onSaveInstanceState 方法保存了数据,这个参数就会带有之前保存的全部数据,通过这个参数就可以进行数据恢复了
修改 MainActivity 中的 onCreate 方法,就实现了数据恢复了:
Activity 的启动模式有:
案例演示:
在 AndroidManifest.xml 中设置 SecondActivity 的启动模式如下:
你先点击 FirstActivity -> SecondActivity -> ThirdActivity ,再从 ThirdActivity 点击回退,你会发现是回退到 FirstActivity,而不是 SecondActivity,这就是因为 SecondActivity 设置成了 singleInstance 模式,单独存在于一个返回栈中
相当于 Java 中的 obj.getClass().getName() ,知晓当前实例的类名
BaseActivity::class.java 表示获取 BaseActivity类的 Class 对象
javaClass.simpleName 表示获取当前实例对象的类名,simpleName 相当于 Java 的 getName()
如何一次性退出所有 Activity,创建一个单例类,把所有的 Activity 同一管理即可,再在每个 Activity 的 onCreate() 初始化的时候,把自己加到你统一管理的哪个集合中就好了
杀掉进程的代码
android.os.Process.killProcess(android.os.Process.myPid())
myPid() 用来获取当前线程的 id
新的语法结构 companion object
,使用这个结构,就可以很清楚的知道当前 Activity 在启动的时候必须传入的参数
案例演示:
SecondActivity 需要的数据都是通过 actionStart() 方法的参数传过来的,在 SecondActivity 中添加如下代码:
再在 firstActivity 中添加如下代码,就可以启动 SecondActivity 了,而且就算 SecondActivity 是别人写的,你也可以很容易知道 SecondActivity 所需要的参数了,看 companion object 结构中的 actionStart 方法的参数即可:
Kotlin 标准函数指的是 Standard.kt 文件中定义的函数,任何 Kotlin 的代码都可以自由地调动所有标准函数
标准函数:
两个参数,第一个参数可以是任意对象,第二个参数是一个 lambda 表达式,因为是方法里面的最后一个参数,所以可以将大括号提出来,lambda 表达式的作用是提供第一个参数对象的上下文
语法结构:
val result = with(obj){
//这里是 obj 的上下文
"value" // with 函数的返回值
}
案例演示:
在 Java 里面,你定义了 StringBuilder s=new StringBuilder() 之后,你每次想要调用 append 方法的时候,都得加上对象名,如 s.append(i),但是用了 with 之后,就相当于把 s 这个对象名给提取出来了,然后直接用 append() 就好了
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val list=listOf("A","B","C")
//相当于 new StringBuilder() :因为 kotlin 里面完全没有 new 这个关键字
val result = with(StringBuilder()){
append("start show")
for(i in list){
append(i).append("\n")
}
}
toString()
}
}
run 方法和 with 方法类似,只不过只有一个 lambda 参数,使用的时候是 obj.函数名,而不是把 obj 作为参数放在函数里面
run 方法的语法结构:
val result=obj.run{
//这里是 obj 的上下文
"value" // run 函数的返回值
}
案例演示:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val list=listOf("A","B","C")
val result = StringBuilder().run{ //和 with 方法主要是这句话的差异
append("start show")
for(i in list){
append(i).append("\n")
}
toString()
}
}
}
和 run 方法类似,区别就是无法指定返回值,而是会自动返回调用对象本身
语法结构:
val result=obj.apply{
//这里是 obj 的上下文
}
//result==obj 也就是说,返回值是调用对象本身,看下面例子
案例演示:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val list=listOf("A","B","C")
val result = StringBuilder().apply{
append("start show")
for(i in list){
append(i).append("\n")
}
//不能返回 toString() 了,因为是 StringBuilder 调用的,所以返回值是那个 StringBuilder对象
}
//只能这样获取到一个 String 对象,前面几个方法都是直接返回的
println(result.toString())
}
kotlin 极度弱化了静态方法这个概念,想要在 kotlin 里面定义一个静态方法不像在 Java 里一样很容易。因为在 Kotlin 里面有比 静态方法更好的语法特性 – 单例类
单例类,可以直接类名.方法名调用,且存在唯一性,则就和 Java 里面静态方法的性质是一样的,所以可以替代
不过,单例类中的方法都有静态方法的性质,如果你只是想让某一个方法具有静态方法的性质,就可以使用前面提到过的 companion object
结构,示例如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Util.doSome2()
}
}
class Util{
fun doSome1(){
println("doSome1")
}
//在这个结构里面的方法可以通过类名.方法名直接调用,而且也只会有一个,相当于 Java 中的静态方法
companion object{
fun doSome2(){
println("do some2")
}
}
}
当然 kotlin 里面也有真正的静态方法,有两种实现方式:注解和顶层方法 , Kotlin 会将这两种方法编译成真正的静态方法
1、使用 @JvmStatic 注解
2、顶层方法
顶层方法就是不被类包裹着的方法,比如:
doSome2() 方法是写在 MainActivity 类的外面的
android:gravity="center"
可以让文字居中对齐,效果图如下:
另外,还可以对TextView 里面的文字进行修改,添加类似于以下代码即可
android:textColor="#00ff00"
android:textSize="24sp"
Button 上的文字默认是大写,想要保留原来的样子,使用 android:textAllCaps="false"
除了使用函数式 API 的方式来注册监听器,也可以使用实现接口的方式进行注册,如下所示:
class FirstActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout) //任何资源都会在 R 文件中生成一个 id
// button1.setOnClickListener{
// SecondActivity.actionStart(this,"data1","data2")
// }
button1.setOnClickListener(this)
}
//接口中定义的方法,在这个方法里面写业务逻辑
override fun onClick(v: View?) {
when(v?.id){
R.id.button1 -> {
SecondActivity.actionStart(this,"data1","data2")
Log.d("first:","kkkkk")
}
}
}
}
相当于前端的输入框
相当于输入框中的提示文字
android:hint="提示文字"
android:maxLines="2" 指定行数为 2 行,当超过两行的时候,会自动加滚动条
用于在界面上展示图片,图片一般放在 drawable 文件夹下面
android:src="@drawable/图片名"
所有的 Android 控件都有一个可见属性,可选值有 3 种, visiable、invisible、gone
用于在界面显示一个进度条
利用 progressBar.visibility==View.visiable 等等,类似的写法去判断控件的可见状态
style 属性可以给进度条指定样式
可以在界面弹出一个对话框,该对话框置顶于所有界面元素之上,能够屏蔽其他控件的交互能力
AlertDialog.Builder(this).apply{
...
}
LinearLayout:线性布局,包括垂直和水平
RelativeLayout:相对布局
FrameLayout : 帧布局,可以用 gravity 指定控件在布局中的对齐方式
Android 常用控件和布局继承结构:
所有控件直接或间接继承自View,所有的布局直接或间接继承自 ViewGroup ,View 是 Android 中最基本的一种 UI 组件,他可以在屏幕上绘制一块矩形区域,并能响应这块区的各种事件
很多的 Android 程序顶部都会有一个标题栏,这样每个 Activity 都要去实现这样一个标题栏,就很没有必要。可以把标题栏的代码单独放在一个布局里面,其他布局直接引用这个布局,就可以实现代码复用了
1、在 layout 目录下新建一个 title.xml 布局,代码如下:
定义两个 Button 用于 Back 和 Edit
定义了 一个 TextView 显示标题文本
<?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="wrap_content"
android:background="#0277BD"
>
<Button
android:id="@+id/titleBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="5dp"
android:text="BACK"
android:textColor="#fff"
/>
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:layout_gravity="center"
android:text="Title Text"
android:textColor="#2196F3"
android:textSize="24sp"
/>
<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginRight="5dp"
android:text="EDIT"
android:textColor="#fff"
/>
</LinearLayout>
2、如现在在 FirstActivity 布局中引用这个标题栏,代码如下
3、隐藏 FirstActivity 自带的标题栏
android:layout_margin=“5dp”:指定控件在上下左右方的间距。还可以 marginLeft 和 marginRight
演示效果:
为标题栏中的控件添加点击事件:
1、新建 TitleLayout (一个 Activity 对应一个 xml 文件) 继承自 LinearLayout,让他成为我们自定义的标题栏控件,代码如下
class TitleLayout(context: Context,attrs:AttributeSet) : LinearLayout(context,attrs){
init {
//通过 from 方法构造出 LayoutInflater 对象,再调用 inflate 方法动态加载一个布局文件
// inflate方法两个参数:1、布局文件的 id ;2、给加载好的布局再添加一个父布局,这里我们想要指定为 TitleLayout
LayoutInflater.from(context).inflate(R.layout.title,this)
}
}
2、上面就创建好了一个自定义控件,接下来在 firstActivity.xml 这个布局文件中添加这个自定义控件
演示效果和上面一样的:
3、为标题栏中的按钮添加监听事件
class TitleLayout(context: Context,attrs:AttributeSet) : LinearLayout(context,attrs){
init {
LayoutInflater.from(context).inflate(R.layout.title,this)
titleBack.setOnClickListener(){
val activity=context as Activity
activity.finish()
}
titleEdit.setOnClickListener(){
Toast.makeText(context,"可以编辑",Toast.LENGTH_SHORT).show()
}
}
}
注意到,在 xml 文件中配置的所有属性,都可以在 AttriButeSet 中获取到
ListView 允许用户通过手指滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕外,比如查看 qq 聊天记录等等
1、新建一个 ListViewTest 项目,并修改 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"
>
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
2、在 MainActivity 中添加代码如下
class MainActivity : AppCompatActivity() {
private var data= mutableListOf("A","B")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//构造数据
for(i in 1..100){
data.add("$i")
}
//三个参数:1、Activity 实例;2、子项布局的 id (这个是 Android 内置的一个布局);数据源(要展示的数据)
val adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,data)
listView.adapter =adapter
}
}
演示效果如下:
1、定义一个实体类,作为 ListView 适配器的适配类型
class Fruit(val name:String,val imagedId:Int)
2、为 ListView 的子项指定一个我们自定义的布局,在 layout 文件下新建 fruit_item.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"
>
<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>
3、创建自定义适配器,新建类 FruitAdapter
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 {
/**
* 使用 LayoutInflater 来为这个子项加载我们传入的布局
*/
val view = LayoutInflater.from(context).inflate(resourceId,parent,false)
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName : TextView = view.findViewById(R.id.fruitName)
val fruit=getItem(position) // 获取当前项的 Fruit 实例
if(fruit!=null){
fruitImage.setImageResource(fruit.imagedId)
fruitName.text=fruit.name
}
return view;
}
}
4、在 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(){
// 将 lambda 表达式执行两遍
repeat(2){
for(i in 1..10) fruitList.add(Fruit("$i",R.drawable.download))
}
}
}
android:layout_gravity="center_vertical"
:垂直方向上居中显示inflate 方法参数:第一个参数就是要加载的布局id,第二个参数是指给该布局的外部再嵌套一层父布局
getView 方法的作用是,在每个子项被滚动到屏幕内的时候会被调用
上面写的这个程序运行效率是很低的,因为在 FruitAdaptr 的 getView() 方法中,每次都将布局重新加载了一遍。在 getView() 方法中,convertView 这个参数可以用来将之前加载好的布局进行缓存,以便之后进行重用。
convertView 可以让程序不会去重复加载布局,大大提升程序效率。但是每次调用 getView 方法的时候还是得用 finViewVyId() 去找组件,这样很影响效率,我们可以用 viewHolder 来对 ImageView 和 TextView 控件进行缓存,代码如下:
演示效果就是点击子项,会有对应的文字弹出
Tip :对于方法中用不到的参数可以使用下划线代替
写的好的链接:https://blog.csdn.net/qq_33275597/article/details/93849695
ListView 不能横向滚动,RecyclerView 可以,并且 RecyclerView 相对于 ListView 做了更多优化
RecyclerView只是一个ViewGroup,它只认识View,所以需要一定的规则来使 Datas 中的数据展示在 RecyclerView 上面,所以适配器的功能就是在做这个事情
如上所示,RecyclerView 表示只会和 ViewHolder 接触,而 Adapter 的工作就是将 Data 转换为 RecycleView 认识的 ViewHolder ,因此 RecyclerView 就间接认识了 Datas
但是 RecycleView 并不像自己去管理 View,所以他会把完成布局的任务交给 LayoutManager
LayoutManager 协助 RecycleView 来完成布局,但是 LayoutManager 只知道如何将一个一个的 View 布局再 RecyclerView 上,并不想自己管理 View,所以会把管理 View 的任务交给 Recycler ,LayoutManager 在需要 View 的时候向 Recycler 进行说去,当 LayoutManager 不需要 View 的时候,就直接将废弃的 View 丢给 Recycler ,图示如下:
1、新建一个 RecyclerViewTest 项目
2、添加 RecyclerView 的依赖
implementation 'androidx.recyclerview:recyclerview:1.0.0'
3、修改 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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
4、为 RecyclerView 准备一个适配器,新建 FruitAdapter 类,继承自 RecyclerView.Adapter,代码如下
class Fruit(val name:String,val imagedId:Int)
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)
}
// 创建 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)
}
override fun getItemCount()=fruitList.size
//用于对 RecyclerView 中的子项进行赋值,在每个子项滚动到屏幕内的时候执行
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imagedId)
holder.fruitName.text=fruit.name
}
}
5、修改 MainActivity 中的代码
class MainActivity : AppCompatActivity() {
private val fruitList =ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits() // 初始化水果数据
//创建一个 LinearLayoutManager 对象,并把它设置到 recylerView 中
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager=layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter=adapter
}
private fun initFruits(){
repeat(2){
for(i in 1..20)
fruitList.add(Fruit("$i",R.drawable.download))
}
}
}
演示效果和上面一样
1、修改 first_item.xml 的布局
2、修改 MainActivity
为什么 ListView 很难或者根本无法实现的效果在 RecyclerView 上这么轻松就实现了呢?
因为 ListView 的布局排列是由自身管理的,而 RecycleView 则将这个工作交给了 LayoutManager 。 LayoutManager 制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能指定出各种不同排列方式的布局了
和 ListView 不同,RecylerView 没有提供类似于 setOnInteClickListerner() 这样的注册监听器方法,而是需要我们自己给子项具体的 View 去注册点击事件,这样做的好处就是当只需要点击某一个子项的时候,不点击其他子项的时候就很方便
// 创建 viewHolder 实例
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)
// itemView 是 View 类型,是 ViewHolder 类的一个成员变量
viewHolder.itemView.setOnClickListener{
val position = viewHolder.adapterPosition
var fruit = fruitList[position]
Toast.makeText(parent.context,"你点击了${fruit.name}",Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener{
val position = viewHolder.adapterPosition
var fruit = fruitList[position]
Toast.makeText(parent.context,"你点击了图片",Toast.LENGTH_SHORT).show()
}
return viewHolder
}
有时候,类中存在很多全局变量实例,为了使他们满足 Kotlin 的空指针检查机制,你不得不做很多费控判断保护才行,即使你知道他们不会为空
对全局变量延迟初始化就可以解决上述问题
格式:
定义一个变量的时候:lateinit var 变量名:变量类型
判断一个延迟加载的全局变量时候已经完成初始化操作:
if(::变量名.isInitialized){
println("已经初始化了")
}
密封类的关键字是 sealed class
例如:sealed class Result
密封内及其所有子类只能定义在同一个文件的顶层位置
密封类的作用就是,可以识别出他的所有子类,在你写条件语句的时候,不用判断 other
Fragment 是一种可以嵌入在 Activity 当中的 UI 片段,在平板上应用广泛,可以看成迷你型的 Activity
案例要求:在一个 Activity 当中添加两个 Fragment,并让这两个 Fragment 平分 Activity 空间
1、 新建一个左侧 Fragment 的布局 left_fragment.xml
<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="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
/>
LinearLayout>
2、新建一个右侧 Fragment 的布局 right_fragment.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="#00ff00"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="24sp"
android:text="这是右边部分"
/>
</LinearLayout>
3、新建一个 LestFragment 类继承自 Fragment
class LeftFragment : Fragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.left_fragment, container, false)
}
}
4、新建一个 RightFragment
class RightFragment : Fragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.right_fragment, container, false)
}
}
5、修改 activity_main.xml
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
Fragment 强大的地方在于他可以动态的添加到 Activity 中
1、新建一个 another_right_fragment.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="#ffff00"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="24sp"
android:text="这是右边部分"
/>
LinearLayout>
2、新建 AnotherFragment 作为另外一个 Fragment
class AnotherRightFragment : Fragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.another_right_fragment, container, false)
}
}
3、在 activity_main.xml 添加如下代码,将 Fragment 替换成了 FrameLayout
4、在 MainActivity 修改代码如下
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener{
replaceFragment(AnotherRightFragment())
}
replaceFragment(RightFragment())
}
private fun replaceFragment(fragment: Fragment){
val fragmentManger =supportFragmentManager
val transaction = fragmentManger.beginTransaction()
transaction.replace(R.id.rightLayout,fragment)
transaction.commit()
}
}
总结动态添加 Fragment 主要分为以下 5 步:
1、创建待添加的 Fragment 实例
2、获取 FragmentMannager
3、开启一个事物
4、向容器添加或替换 Fragment ,一般使用 replace() 方法实现,需要传入容器的 ID 和待添加的 Fragment 实例
5、提交事物
演示效果:
点击按钮之前:
通过按钮添加了一个 Fragment 之后,按下 Back 键并不会回到上一个 Fragment ,而是会直接退出。
addToBackStack() 方法,可以将一个事物添加到返回栈中
修改 MainActivity 中的代码:
为了方便 Fragment 和 Activity 之间的交互 ,FragmentManager 提供了一个类似于 findViewById() 的方法,专门用于从布局文件中获取 Fragment 实例
不同的 Fragment 通过 Activity 进行调用
Fragment 提供的附加的回调方法
动态加载布局的技巧:让程序根据设备的分辨率或屏幕大小,在运行时决定加载哪个布局
1、修改 activity_main.xml 文件,代码如下:
2、在 res 下新建一个layout-large文件夹,在这个文件夹下新建一个 activity_main.xml 文件,内容如下
3、修改 MainActivity
手机:
可以看到,layout/activity_main 布局只包含了一个 Fragment ,即单页模式,而 layout-large/activity_main 布局包含了两个 Fragment ,即双页模式。其中 large 就是一个限定符,那些屏幕被认为是 large 的设备就会自动加载 layout-large 文件夹下的布局,小的设备还是会加载 layout 文件夹下的布局
新建 layout-sw600dp 文件夹,其他和上面一样
扩展函数:即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类中添加新的函数
语法结构:
fun ClassName.methodName(parm1: Int,parm2" Int){
return 0
}
运算符重载使用的是 operator 关键字,只要在指定函数前加上这个关键字即可,这个指定函数对应的重载函数式固定的,比如加号运算符重载对应 plus(),减号运算符重载对应 minus()
高阶函数定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数
定义一个函数类型,语法规则:
(String,Int) -> Unit
案例演示:
fun example(func:(Int,Int) -> ){ //接收了一个函数类型的参数
func(1,2) // 调用函数类型的参数
}
只要传入不同的函数类型参数,那么他的执行逻辑和最终返回结果就可能是完全不同的,比如,上面那个 example 函数,你可以传一个 add(Int,Int) 函数,你也可以传一个 minus(Int,Int)
Tip:Lambda 表达式是最常见的高阶函数调用方式
高阶函数的原理实际上是我们每一次调用 lambda 表达式,都会创建一个新的匿名类实现
内联函数的用法就是在定义高阶函数的时候加上 inline 关键字的声明即可
内联函数的工作原理是:在 Kotlin 编译的时候会把内联函数中的代码在编译的时候自动替换到调用它的地方,这样就不存在运行时开销了
noinline :排除内联功能
noinline 意义:内联的函数参数类型参数在编译的时候会被进行代码替换,因此他没有真正的参数属性。非内联的函数类型参数可以只有地传递给其他任何函数,因为他就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这就是他最大的局限性。
Tip:内联函数所引用的 lambda 表达式中可以使用 return 关键字来进行函数返回,而非内联函数只能进行局部返回,因为内联函数是字节替换成主体代码的一部分,return 就像是字节写在主体里面的
crossinline:解决矛盾的:内联函数中的 lambda 表达式中允许使用 return 关键字,而高阶函数的匿名类实现中不允许使用 return 关键字造成了冲突
Android 系统中主要提供了 3 种方式用于简单的实现数据持久化功能:文件存储、 SharedPreferences 存储以及数据库存储
文件存储是 Android 最基本的数据存储方式,不对存储的内容进行任何格式化处理,所有数据都是原封不动的保存到文件当中的,因而他比较适合存储一些简单的文本数据或二进制数据
Context 类中提供了一个 openFileOutput() 方法,用于将数据存储到指定的文件中。这个方法接收两个参数:
openFileOutput() 方法返回的是一个 FileOutputStream 对象
use 扩展函数 是 Kotlin 提供的一个内置扩展函数,它会保证在 lambda 表达式中的代码全部执行完之后自动将外层的流关闭
案例演示:
1、新建一个 FilePersistenceTest 项目,修改 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"
>
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="在这里编辑"
/>
</LinearLayout>
2、在Activity 被销毁之前,把编辑框中输入的内容持久化
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onDestroy() {
super.onDestroy()
val inputText = editText.text.toString()
save(inputText)
}
private fun save(inputText:String){
try{
val output=openFileOutput("data", Context.MODE_APPEND)
val write = BufferedWriter(OutputStreamWriter(output))
write.use{
it.write(inputText)
}
}catch(e:IOException){
e.printStackTrace()
}
}
}
当你按下 Back 关闭程序的时候,我们输入的内容就保存到文件中了,可以借助 Device File Explorer 工具进行查看,这个工具相当于一个设备文件浏览器
Context 类提供的 openFileInput() 方法泳衣从文件中读取数据,该方法只有一个参数,即要读取的文件名,系统会自动的到 /data/data/
/files/目录下加载这个文件,并返回一个 FileInputStream 对象
forEachLine 是 kotlin 提供的一个内置扩展函数,它会将读到的每行内容都回调到 lambda 表达式中
SharedPreferences 使用键值对的方式来存储数据,也就是说,当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可以通过这个键把对应的值取出来。而且 SharedPreference 支持多种不同的数据类型存储
获取 SharedPreference 对象的两种方法:
Context 类提供的 getSharedPerferences()
该方法提供两个参数:1、指定 SharedPreference 文件的名称,如果指定的文件不存在则会创建一个 2、用于指定操作模式,目前只有默认的 mode_private 这一种模式可选
Context 类中提供的 get Perferences()
该方法只有一个参数,因为这个方法会自动将 Activity 的类名作为 SharedPreference 的文件名
得到 SharedPreference 对象,就可以开始向 SharedPreference 文件中存储数据了
案例演示:
1、新建一个 SharedPreferenceTest 项目,修改 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"
>
<Button
android:id="@+id/saveBUtton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="保存"
/>
LinearLayout>
2、修改 MainActivity 中的代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
saveBUtton.setOnClickListener{
//1、调用 SharedPreferences 对象的 edit 方法获取一个 SharedPreferences.Editor 对象
val editor = getSharedPreferences("data2", Context.MODE_PRIVATE).edit()
editor.putString("name","张三")
editor.putInt("age",18)
//调用 apply 方法将数据提交
editor.apply()
}
}
}
可以看到,我们刚刚在按钮的点击事件中添加的所有数据都已经成功保存下来, SharedPreferences 文件是使用 XML 格式来对数据进行管理的
直接使用 SharedPreference 对象的 getXXX() 方法即可
SQLite 是 Android 系统内置的数据库,不仅支持标准的 SQL 语句,还遵循数据库的 ACID 事物
Android 为了让我们更加方便的管理数据库,专门提供了一个 SQLiteOpenHelper 帮助类,借助这个类可以很方便的对数据库进行创建和升级
SQLiteOpenHelper 是一个抽象类,有两个抽象方法:onCreate() 和 onUpgrade() 分别对应创建和升级数据库
SQLiteOpenHelper 有两个重要的实例方法:
SQLiteOpenHelper 还有两个构造方法可供重写:
/**
* Context:有他才能对数据库进行操作
* name:数据库名,创建数据库时使用的就是这里指定的名称
* factory:允许我们在查询数据的时候返回一个自定义的 Cursor ,一般传入 Null
* version:表示当前数据库的版本号,可对数据库进行升级操作
*/
public SQLiteOpenHelper(Context context, String name,CursorFactory factory, int version) {
this(context, name, factory, version, null);
}
案例要求:创建一个名为 BookStore.db 的数据库,然后在这个数据库中新建一张 Book 表,表中有 id,作者、价格等列
案例演示:
1、新建 MyDatabaseHelper 类继承自 SQLiteOpenHelper,代码如下:
class MyDatabaseHelper (val context: Context, name:String, version:Int):
SQLiteOpenHelper(context,name,null,version){
override fun onCreate(db: SQLiteDatabase?) {
if (db != null) {
db.execSQL(createBook)
Toast.makeText(context,"创建成功!",Toast.LENGTH_SHORT).show()
}
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
private val createBook ="create table Book(" +
"id integer primary key autoincrement," +
"author text," +
"price real)"
}
2、修改 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"
>
<Button
android:id="@+id/createDatabase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="创建数据库"
/>
</LinearLayout>
3、在 MainActivity 中添加如下内容
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val dbHelper = MyDatabaseHelper(this,"BookStore.db",1)
createDatabase.setOnClickListener{
//两个重要的实例方法中的其中一个
dbHelper.writableDatabase
}
}
}
运行结果:
当需要对数据库进行修改的时候,就可以使用 onUpgrade 方法,比如,在 Book 表中新增一个字段之类的
当你传参的数据库版本号大于之前的版本号,onUpgrade 方法就会被执行
contentValues ,提供了一系列 put() 方法重载,用于向 ContentValues 中添加数据,只要将表中的每个列名以及相应的待添加数据传入即可
val db=dbHelper.writableDatabase
db.beginTransaction() 开启事务
db.setTransactionSuccesful() // 事务已经执行成功
db.endTransaction() // 结束事务
使用高阶函数简化 SharedPreferences 的用法:
1、新建一个 SharedPreferences.kt 文件,然后在里面加入如下代码:
2、修改 activity_main 中的代码
演示效果和上面是一样的:
生成了 data.xml
data.xml 中的数据就是我们 put 进去的:
其实在 Google 提供的 KTX 扩展库中已经包含了上述 SharedPreferences 的简化用法,这个扩展库会在 Android Studio 创建项目的时候自动引入依赖,如下:
我们在使用的时候,直接代用 edit 方法就好了,也就是将上面的 open 换成 edit 就好了,如下:
什么是泛型: 在一般的编程模式下,我们需要给任何一个变量指定一个具体的那类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码拥有更好的扩展性
定义泛型的两种方式:一种是定义泛型类,一种是定义泛型方法,使用的语法结构都是
,括号内的 T q其实可以是任意字母,但是我们一般使用 T
Kotlin 允许我们对泛型的类型进行限制,比如:
泛型的上界默认是 Any
class MyClass{
// 这里的意思就是将 method 方法的泛型上界设置成 Number 类型
fun <T : Number> method(param:T):T{
return param
}
}
委托实际上是一种设计模式,他的基本理念是 : 操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。就像 Java 里面的 HashSet 类,HashSet 其实是基于 HashMap 实现的,HashSet 直接封装了 HashMap ,很多方法也就直接调用的 HashMap 类里面的。在这里 HashMap 就是一个辅助对象。
委托模式的意义就在于:对辅助对象稍加修改,就可以形成另一个全新的数据结构,比如 HashSet 和 HashMap
但是 Java 中并没有委托的实现,比如,HashSet 想使用 HashMap 的方法,他选择的是 HashMap 作为参数,但是还可以通过继承的方法去实现,如果通过继承的方式去实现的话,假如 HashMap 是个接口,就得重写 HashMap 里面所有的方法, Kotlin 在这方面做了改进
直接在接口声明的后面使用 by 关键字,再接上受委托的辅助对象,就可以免去一大堆模板式的代码了
委托属性的核心思想就是将一个属性(字段)的具体实现委托给另一个类去完成
委托属性语法结构:
class MyClass{
var p by Delegate()
}
这种写法的意思是将 p 属性的具体实现委托给了 Delegate 类去实现,当调用 p 属性的时候会自动调用 Delegate 类的 getValue() 方法,当给 p 属性赋值的时候,会自动调用 Delegate 类的 setValue() 方法
所以 Delegate 类要实现 getValue() 和 SetValue() 方法
懒加载:把想要延迟执行的代码放在 by lazy 代码块中,这样带阿玛在一开始的时候不会执行,只有在初次被调用的时候,代码块中的代码才会执行
by lazy 的语法结构:
var p by lazy{}
其实这个 lazy 就是一个高阶函数,在 lazy 中会创建并返回一个 Delegate 对象,当我们调用 p 属性的时候,实际上调用的是 Delegate 的 getValue() 方法
A to B 可以构建一个键值对, 但是 to 并不是 Kotlin 中的一个关键字,而是由于 Kotlin 提供了一种高级的语法糖特性: infix 函数。比如 A to B ,实际上是 A.to(B)
下面我们就通过两个具体的例子来学习一下 infix 函数的用法,先从简单例子开始
String 类中有一个 startWith() 函数,语法结构如下:
"mmmmm".startsWith("m")
借助 infix 函数,我们可以把他改成更可读的写法:
//定义了一个 String 类的扩展函数,他在内部实现就是调用 String 类的 startsWith() 函数
infix fun String.beginsWith(prefix:String) = startsWith(prefix)
这样之后,上面那段话等价于:
"mmmmm" beginsWith "m"
to() 函数的原理:
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Java 的泛型功能是通过类型擦除机制来实现的,也就是说泛型对于类型的约束只在编译使其存在
所有基于 JVM 的语言,他们的泛型功能都是通过擦除机制来实现的,Kotlin 也是这样。这种机制使得我们不可能使用 a is T 或者 T::class.java 这样的语法,因为 T 的实际类型在运行的时候已经被擦除了,而且是把 T被替换成 Object
,所以 object.getClass() 没什么意义,得不到他实际的 Class 对象
但是在 Kotlin 中,有一个内联函数,这个内联函数的功能就是会把代码在编译的时候自动替换到调用它的地方,这样的话,也就不存在什么泛型擦除的问题了,代码在编译之后会直接使用实际的类型来替代内联函数中的泛型声明
泛型实化的时候该函数必须是内联函数才行,在声明泛型的地方必须加上 reified 关键字来表示该泛型要进行实化,语法结构如下:
inline fun <reified T> getGenericType(){
}
inline fun <reified T> getGenericType()=T::class.java
getGenericType 函数直接返回了当前指定泛型的实际类型,使得 T.class 变得合法
注意:Student 是 Person 的子类,但是 List
并不是 List
的子类
泛型协变的定义:假如定义了一个 MyClass
的泛型类,其中 A 是 B 的子类,同时 MyClass
又是 MyCLass
的子类型,那么我们就可以称 MyClass 在 T 这个泛型上是协变的
但是 MyClass
如果是 MyCLass
的子类型这种,存在安全隐患,解决办法是,让泛型类在其泛型类型的数据上是只读,而要实现这一点,则需要让MyClass
类中的所有的方法都不能接收 T 类型的参数,换句话说,T 只能出现在 out 位置上,而不能出现在 in 位置上
泛型逆变的定义:假如定义了一个 MyClass
的泛型类,其中 A 是 B 的子类(AB),同时 MyClass
又是 MyCLass
的子类型(BA),那么我们就可以称 MyClass 在 T 这个泛型上是协变的
也就是 T 只能出现在 in 的位置,不能出现在 out 的位置
协程可以看成是轻量级的线程,他可以让我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关
Kotlin 并没有将协程纳入标准库的 API 中,而是以依赖库的形式提供的,所以在使用协程前要先导入依赖:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
使用 GlobalScope.launch 开启一个协程
fun main() {
//开启一个协程
GlobalScope.launch{
println("xxxxxx")
//delay(1500)
}
}
delay() 函数可以让当前协程延迟指定实践后再运行,delay() 函数只能在协程的作用域或者其他挂起函数中调用
runBlocking(){} 函数同样会创建一个协程的作用域,但是他可以保证在协程作用域内的所有代码和子协程没有全部执行之前一直阻塞当前线程。但是需要注意的是,runBlocking 函数通常应该在测试环境下使用
单独的 launch 函数只能在协程的作用域中才能调用,如下:
演示效果:
这里的 launch 和 GlobalScope.launch 不一样, GlobalScope.launch 创建的是顶级协程。launch 创建的是子协程,子协程的特点就是在外层作用域协程结束之后,该作用域下的所有子协程也会一同结束
suspend 关键字,可以将任意函数声明为挂起函数,挂起函数之间可以互相调用
但是 suspend 只能将任意函数声明为挂起函数,但是是无法给她提供协程作用域的
coroutineScope 函数的特点是会继承外部协程作用域并创建一个子作用域
怎么取消一个协程?不管是 GlobalScope.launch 还是 launch ,都会返回一个 job 对象,调用 job.cancle() 就可以取消一个协程
asyc 函数
withContext函数
fun <T : Comparable<T> > max(vararg nums:T):T{
if(nums.isEmpty()) throw RuntimeException("参数不能为空")
var maxNum=nums[0]
for(num in nums) maxNum=max(maxNum,num)
return maxNum
}
给 String 类新增一个 showToast 函数,就可以直接调用 ”字符串“.showToast(context) 了
借助 webView 控件我们就可以在自己的应用程序里嵌入一个浏览器,从而非常轻松的展示各种各样的网页
案例演示:
1、新建一个 WebViewTest,修改 activity_main.xml 中的代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
LinearLayout>
2、修改 MainActivity 中的代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//通过 WebView 的 getSettings() 方法可以设置一些浏览器的属性
webView.settings.setJavaScriptEnabled(true) //支持 JavaScript 脚本
//给 webView 传入一个WebViewClient 实例,这段代码的作用是,
// 当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前的 WebView 中显示
webView.webViewClient = WebViewClient()
webView.loadUrl("https://www.baidu.com")
}
}
3、修改 AndroidManifest.xml 文件,加入权限声明
<uses-permission android:name="android.permission.INTERNET"/>
案例演示:
首先需要创建一个 URL 对象,并传入目标的网络地址,然后调用一下 openConnection() 方法即可,如下所示:
val url = URL("https://www.baidu.com")
val connection =url.openConnection() as HttpURLConnection
得到 connection 之后,就可以设置一些属性了,比如说:
//HTTP 请求时的方法
connection.requestMethod="GET"
//读取超时的毫秒数
connection.readTimeout=80000
之后再调用 getInputStream()
方法就可以获取到服务器返回的输入流了,剩下的就是对输入流进行读取
connection.inputStream
最后可以调用 disconnect() 方法将这个流关闭
connection.disconnect()
ScrollView 控件,可以以滚动的形式查看屏幕外的内容
案例演示:
1、新建一个 NetworkTest 项目,修改 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"
>
<Button
android:id="@+id/sendRequestBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送数据"
/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/responseText"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
ScrollView>
LinearLayout>
2、修改 MainActivity 中的代码如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sendRequestBtn.setOnClickListener{
sendRequestWithHTTPURLConnection()
}
}
private fun sendRequestWithHTTPURLConnection(){
//开启线程发起网络请求
thread{
var connection:HttpURLConnection?=null
try {
val response=StringBuilder()
val url= URL("https://www.baidu.com")
connection =url.openConnection() as HttpURLConnection
connection.connectTimeout=8000
connection.readTimeout=8000
val input=connection.inputStream
//对获取到的输入流进行读取
val reader = BufferedReader(InputStreamReader(input))
reader.use{
reader.forEachLine {
response.append(it)
}
}
showResponse(response.toString())
}catch (e:Exception){
e.printStackTrace()
}
}
}
private fun showResponse(response:String){
runOnUiThread{
//在这里进行 UI 操作,将结果显示到界面上
responseText.text = response
}
}
}
1、在项目中添加 OkHttp 库的依赖
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
2、创建一个 OkHttlClient 的实例
val client= OkHttpClient()
3、如果想要发起一个 HTTP 请求,就需要再创建一个 Request 对象:
val request=Request.Builder().build()
4、调用 OkHttpClient 的 Call() 方法创建一个 call 对象,并调用它的 execute() 方法来请求并获取服务器返回的数据
val response=client.newCall(request).execute()
5、得到返回的具体内容
val responseData=response.body?.string()
如果是 POST 方法还要先构造一个请求体
这是由 Androidx库提供的,和 ActionBar 有相似的功能。ActionBar 就是前面提到过的系统自带的原生的那个标题栏
ToolBar 的强大之处在于他不仅继承了 ActionBar 的所有功能,而且灵活性很高,可以配合其他控件实现一些 Material Design 效果
每个项目默认都是会显示 ActionBar 的,这个 ActionBar 从哪来呢?其实是根据项目中指定的主题来显示的。打开 AndroidManifest.xml 文件,可以看到:
这个主题是在 res/values/styles.xml 文件下定义的,代码如下:
现在我们准备用 ToolBar 来替代 ActionBar ,因此需要指定一个不带 ActionBar 的主题,通常有 Theme.AppCompat.NoActionBar 和 Theme.AppCpmpat.Light.NoActionBar 这两种主题可选。其中 Theme.AppCompat.NoActionBar 表示深色主题,他会将界面的主题颜色设为深色,陪衬颜色设置为浅色。 而 Theme.AppCpmpat.Light.NoActionBar 表示浅色主题,他会将主题颜色设置为浅色,陪衬颜色设置为深色
1、修改 activity_main.xml 的代码如下
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
指定了一个新的命名空间,为了兼容老系统
xmlns:aop="http://schemas.android.com/apk/res-auto">
定义了一个 Toolbar 控件,这个控件是由 appcompat 库提供的
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
单独将弹出的菜单项指定成浅色主题
aop:popupTheme="@style/ThemeOverlay.AppCompat.Light"
/>
FrameLayout>
2、修改 MainActivity 布局
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
}
3、
演示效果:
还可以在标题栏上添加一些图片:
步骤如下:
1、新建 menu 文件夹,并在 menu 文件夹下新建 toolbar.xml 文件,并编写如下代码:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/backup"
android:icon="@drawable/download"
android:title="backup"
app:showAsAction="always"
/>
<item
android:id="@+id/delete"
android:icon="@drawable/download"
android:title="delete"
app:showAsAction="ifRoom"
/>
<item
android:id="@+id/setting"
android:icon="@drawable/download"
android:title="setting"
app:showAsAction="never"
/>
menu>
aop:showAsAction 有三个值可选:always 表示永远显示在 ToolBar 中,如果屏幕空间不够则不显示、ifRoom 表示屏幕空间足够的情况下显示在 ToolBar 中,不够则显示在拆散中,never 则表示永远显示在菜单中。注意:Toolbar 中的 action 按钮只会显示图标,菜单中的 action 按钮只会显示文字
2、修改 MainActivity 中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar,menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.backup -> Toast.makeText(this,"点击了BackUp",Toast.LENGTH_SHORT).show()
}
return true
}
}
所谓滑动菜单就是讲一些菜单选项隐藏起来,而不是放置在主屏幕上
Androidx 提供了一个 DrawerLayout 控件,可以帮助我们实现滑动菜单
DrawerLayout 就是一个布局,在布局中允许放入两个直接子控件:第一个控件是主屏幕上显示的内容,第二个子控件是滑动菜单上显示的内容
TextView 必须指定 gravity 属性,才能告诉 DrawerLayout 滑动菜单在屏幕的左边还是右边
NavigationView 是 Material 库中提供的一个控件,它不仅严格按照 Material Design 的要求来设计的,而且可以将滑动菜单页面的实现变得非常简单
需要导入依赖
implementation 'com.google.android.material:material:1.2.1'
implementation 'de.hdodenhof:circleimageview:3.0.1'
悬浮按钮
提示工具,和 Toast 有点像,不过他在提示中加入了一个可交互按钮,当用户点击的时候,可以进行一些额外的逻辑操作
是用于实现卡片式布局效果的重要控件,只是额外提供圆角和阴影效果,看上去会有立体的感觉
相当于垂直布局的 LinearLayout ,只是在 LinearLayout 上加了一些材料设计的概念,它可以让你定制当 某个可滚动View 的滚动手势发生变化时,其内部的子 View 实现何种动作
内部的子 View 布局通过 app:layout_scrollFlags 设置执行的动作
NestedScrollView 可以看成是 ScrollView,可以给他设置 behavior 属性
CollapsingToolBarLayout:是用来对 ToolBar 进行包装的,主要是实现折叠(伸缩)效果,需要放在 AppBarLayout 布局里面,且作为 AppBarLayout 的直接子 View
CoordinatorLayout 是加强版的 FrameLayout,可以监听其子控件的所有事件,根据我们的定制,可以帮我们协调子 View
CoordinatorLayout 的核心是 Behavior ,Behavior 就是执行你定制的动作, Dependency 这个 view 发生了变化。那么 Child 的这个 view 就要发生相应的变化,具体发生的变化执行的代码就放在 Behavior
在 CoordinatorLayout 内部,每个 child 都必须携带一个 Behavior (其实不携带也可以,不携带就不能被协调),CoordinatorLayout 就根据每个 child 所携带的 Behavior 信息进行协调
注意:Behavior 不仅仅协助联动,而且还是接管了 child 的三大流程,有点类似于 RecyclerView 的 LayoutManager
你可以把它当成垂直布局的LinearLayout来使用,它可以让你定制当某个可滚动View的滚动手势发生变化时,其内部的子View实现何种动作。
内部的子 View 通过在布局中添加 app:layout_scrollFlags
设置执行的动作
CollapsingToolbarLayout 是用来对 ToolBar 进行再次包装的 ViewGroup ,主要是用于实现折叠的 AppBar 效果,CollapsingToolbarLayout 需要作为 AppBarLayout 的直接子View才会有效,他的子 View 通过app:layout_collapseMode="parallax|pin"
来设置效果
JetPack 是一个开发组件工具集,通常定义在 Androidx 中,主要由基础、架构、行为、界面四部分组成。
ViewModel :只要是界面上能看到的数据,他的相关变量都应该放在ViewModel 中,而不是 Activity 中,这样可以在一定程度上减少 Activity 中的逻辑。还有个很重要的功能就是在发生竖屏旋转的时候,Activity 不会被重建
Lifecycles :感受 Activity 的生命周期
LiveData:可以包含任何类型的数据,并在数据发生变化时通知给观察者
Room 主要由 Entity、Dao、DataBase 组成
WorkManager 处理定时的工具
案例要求:实现一个计数器的功能
1、导入依赖
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
通常来讲比较好的编程规范就是给每个 Activity 和 Fragment 都创建一个对应的 ViewModel
2、为 MainActivity 创建一个 对应的 MainViewModel 类,并让他继承自 ViewModel,并放置一个 counter 变量用于计数
class MainViewModel:ViewModel() {
var counter=0
}
3、在界面上添加一个按钮,每点击一次就让他加1,并把最新的计数显示在界面上,修改 activity_main.xml 中的代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/infoText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textSize="32sp"
/>
<Button
android:id="@+id/plusButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Plus one"
/>
LinearLayout>
4、修改 MainActivity 中的代码,实习计数器逻辑
class MainActivity : AppCompatActivity() {
lateinit var viewModel:MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//不可以直接创建 ViewModel 实例,必须通过 ViewModelProvider 去创建
//this是你的 activity 或者 fragment 实例
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
plusButton.setOnClickListener{
viewModel.counter++
refreshCounter()
}
}
private fun refreshCounter(){
infoText.text=viewModel.counter.toString()
}
}
Lifecycles 的作用就是感知 Activity 的生命周期,而且可以在一个 非 Activity 中去感知他的生命周期
方法一:通过在 Activity 中嵌入一个隐藏的 Fragment 来进行感知,或者通过手写监听器的方式来感知,例如:
通过手写监听器的方式来对 Activity 的生命周期进行感知:
可以看到,当 Activity 的生命周期发生变化的时候,就会主动去通知 MyObserver,但是需要我们在 Activity 中编写很多额外的逻辑,而 Lifecycles 组件的出现就是为了解决这个问题的,他可以让任何一个类都能轻松的感知到 Activity 的生命周期,同时又不需要在 Activity 中编写大量的逻辑处理
案例演示:
class MainActivity : AppCompatActivity() {
lateinit var observer: MyObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//每个 Activity 本身就是一个 lifeCycleOwner
lifecycle.addObserver(MyObserver())
}
}
class MyObserver:LifecycleObserver{
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun activityStart(){
Log.d("MyObserver","activityStart")
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun activityStop(){
Log.d("MyObserver","activityStop")
}
}
功能和上面一样,MyObserver 可以感知到 Activity 的生命周期发生了变化,但是却不能主动获知当前的生命周期状态,解决办法如下:
之后,在任何地方调用 lifecycle.currentState 来主动获知当前的生命周期状态,生命状态一共有 5 种,和 Activity 的生命周期回调对应的关系如下图
LiveData 是 Jetpack 提供的一种响应式编程组件,他可以包含任意类型的数据,并在数据发生变化的时候通知给观察者,适合结合 ViewModel 来使用
比如将前面的计数器中的 counter 使用 LiveData 包装起来,然后再 Activity 中去观察他,就可以主动的将数据变化通知给 Activity 了
写得好的链接:https://www.runoob.com/w3cnote/android-tutorial-viewpager.html
这个类可以让用户左右切换当前的 View
继承了 ViewGroup 类,所以是一个容器类,可以在其中添加其他 View 类
需要 PageAdapter 提供数据
简介中提到了PagerAdapter,PagerAdapter 是一个基类适配器,我们经常用它来实现 app 引导图,它的子类有 FragmentPagerAdapter 和 FragmentStatePagerAdapter
FragmentPageAdapter:和PagerAdapter一样,只会缓存当前的Fragment以及左边一个,右边 一个,即总共会缓存3个Fragment而已
FragmentStatePagerAdapter:当Fragment对用户不 见得时,整个 Fragment 会被销毁, 只会保存Fragment 的状态!而在页面需要重新显示的时候,会生成新的页面!
FragmentPagerAdapter适用于页面比较少的情况,FragmentStatePagerAdapter适用于页面比较多的情况,因此不同的场合选择合适的适配器才是正确的做法
源码中我们可以看出FragmentStatePagerAdapter中fragment实例在destroyItem的时候被真正释放,所以FragmentStatePagerAdapter省内存。FragmentPagerAdapter中的fragment实例在destroyItem的时候并没有真正释放fragment对象只是detach
实现翻页动画的关键就是重写transformPage方法,方法里有两个参数view和position,理解这两个参数非常重要。假设有三个页面view1,view2,view3从左至右在viewPager中显示
往左滑动时:view1,view2,view3的position都是不断变小的。
view1的position: 0 → -1 → 负无穷大
view2的position: 1 → 0 → -1
view3的position: 1 → 0
往右滑动时:view1,view2,view3的position都是不断变大的。
view1的position: -1 → 0
view2的position: -1 → 0 → 1
view3的position: 0 → 1→ 正无穷大
当position是正负无穷大时view就离开屏幕视野了。因此最核心的控制逻辑是在[-1,0]和(0,1]这两个区间,通过设置透明度,平移,旋转,缩放等动画组合可以实现各式各样的页面变化效果。
案例演示:
https://www.runoob.com/w3cnote/android-tutorial-viewpager.html
代码地址:
merge必须放在布局文件的根节点上。
merge并不是一个ViewGroup,也不是一个View,它相当于声明了一些视图,等待被添加。
merge标签被添加到A容器下,那么merge下的所有视图将被添加到A容器下。
因为merge标签并不是View,所以在通过LayoutInflate.inflate方法渲染的时候, 第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。
因为merge不是View,所以对merge标签设置的所有属性都是无效的。
https://juejin.cn/post/6844903442901630983