提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方,欢迎各位在评论中指出。
Android Studio会在创建一个新Activity时自动帮我们生成相应的layout布局文件并在Activity内引用这个布局。但是我们这次选择手动创建和加载布局。
创建first_layout.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"
android:orientation="vertical">
<!-- 添加一个按钮Button -->
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 1" />
</LinearLayout>
在Activity中手动加载这个布局:
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//引用相应的布局
setContentView(R.layout.first_layout)
}
在这里调用了 setContentView() 方法,并传入一个first_layout.xml布局文件的id作为参数,这样就可以给FirstActivity加载一个布局了。
在Android项目中,R是一个特殊的类,用于引用所有的资源,如布局(layout)、图片(drawable)、字符串(string)等,我们在项目中添加的任何资源都会在R文件中生成一个相应的资源id。
在程序运行前,我们需要告诉应用程序应该先启动哪个Activity。下面是如何为程序配置主Activity的示例代码:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
···>
<activity
android:name=".FirstActivity"
android:label="This is First Activity!" // 指定Activity标题栏内容
android:exported="true">
// Begin 为程序配置主Activity
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
// End 为程序配置主Activity
</activity>
</application>
</manifest>
这样FirstActivity就是我们这个程序的主Activity了,当我们打开App时最先显示的就是这个Activity。
①findViewById():返回一个继承自View的泛型对象,所以Kotlin无法推导出该对象是什么控件,所以我们需要显式的将变量声明成Button类型。
②makeText():通过makeText()方法 可以创建一个Toast对象,然后调用show()方法将Toast显示出来。makeText()方法需要传入三个参数:①Context上下文②要显示的内容③显示的时长。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
//为按钮绑定view控件
val button1: Button = findViewById(R.id.button1)
//为按钮设置点击触发事件
button1.setOnClickListener {
上下文 Toast要显示的内容 显示时长 记得调用show()不然Toast无法显示
Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
}
}
在res目录下新建一个menu文件夹:res—>New—>Directory—>输入文件名menu—>点击OK
在menu文件夹下新建一个main菜单文件:menu—>New—>Menu resource file—>输入main—>点击OK
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="Add" />
<item
android:id="@+id/remove_item"
android:title="Remove" />
</menu>
Android Studio中重写的快捷键是Ctrl + O
我们在FirstActivity中重写了onCreateOptionsMenu() 方法,这里面menuInflater实际上是调用了父类的getMenuInflater方法得到一个MenuInflater对象,然后再调用它的inflate()方法,就可以给当前Activity创建菜单了。inflate()方法需要接收两个参数:①通过哪个资源文件创建菜单 ②将菜单添加到哪个Menu对象中
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menu资源文件 menu对象
menuInflater.inflate(R.menu.main, menu)
return true
}
接下来就可以自定义菜单的响应事件了,重写 onOptionsItemSelected() 方法来实现:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
调用show()显示Toast
R.id.add_item -> Toast.makeText(this, "You clicked Add.",Toast.LENGTH_SHORT).show()
R.id.remove_item -> Toast.makeText(this, "You clicked Remove.",Toast.LENGTH_SHORT).show()
}
return true
}
Intent是Android程序中各组件之间进行交互的一种重要方式。它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent又分为两种:
我们先来学习一下显式Intent,我们这次让Android Studio帮我们新建一个SecondActivity(自动生成layout并在AndroidManifest中注册):
<?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"
android:orientation="vertical">
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 2" />
</LinearLayout>
像下面这种,直接表明我们想要从当前Activity跳转到SecondActivity的Intent就叫做显式Intent,因为我们直接显式的声明了我们想要从A页面跳转到B页面。接下来,我们为FirstActivity中的按钮添加点击事件:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
// button1的点击事件
button1.setOnClickListener {
上下文 要启动的目标Activity
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
}
在上面的代码中我们声明了一个Intent对象,Intent对象要求传入两个参数:①上下文和②目标Activity。这里面Kotlin语言的SecondActivity::class.java相当于Java语言中的SecondActivity.class。然后我们通过startActivity()方法来实现页面的跳转,并向其中传入intent参数。
相比于显式Intent,隐式Intent就要含蓄的多。隐式Intent并没有明确指明目标组件,而是通过action和category等信息来让系统判断要启动哪个Activity。需要注意的是,使用隐式Intent时需要确保至少存在一个能够处理该Intent的组件,否则会导致应用崩溃。
AndroidManifest.xml中给SecondActivity配置隐式Intent的方法:
注意:只有Activity的< action >和< category >中的内容同时匹配Intent中指定的action和category时,这个Activity才能响应该Intent。
<application
· · ·>
<activity
android:name=".SecondActivity"
android:exported="true" //注意这里必须为true
android:label="This is SecondActivity">
<intent-filter>
//只有Activity中的action和category都和Intent匹配成功才能响应
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
FirstActivity.java中修改按钮点击逻辑:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
button1.setOnClickListener {
//为当前intent配置action
val intent = Intent("com.example.activitytest.ACTION_START")
startActivity(intent)
}
这次你点击FirstActivity中的Button按钮,实现跳转到SecondActivity页面就是通过隐式Intent来实现的。一个Intent只能配置一个action,但是却可以配置多个category。我们在上面的例子中使用了默认的category:“android.intent.category.DEFAULT”。下面我们额外增加一个category,看一下会发生什么:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout)
button1.setOnClickListener {
//该intent配置了一个ACTION_START的action和一个MY_CATEGORY的category
val intent = Intent("com.example.activitytest.ACTION_START")
intent.addCategory("com.example.activitytest.MY_CATEGORY")
startActivity(intent)
}
这时如果你点击FirstActivity中的Button1按钮并不会实现跳转,而是会导致crash。这是因为我们刚才在Intent中添加了一个自定义的category:“com.example.activitytest.MY_CATEGORY”。而SecondActivity中的< intent-filter >标签中没有声明可以响应这个自定义的category:
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
在前面我们讲过,Activity中的action和category必须都和Intent中的action和category匹配成功,Activity才能响应这个Intent。这里只成功匹配了action,但是却没有可以与之相匹配的category,所以会导致程序crash。现在我们为SecondActivity配置这个自定义的category:
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
//为SecondActivity添加一个category声明
<category android:name="com.example.activitytest.MY_CATEGORY" />
</intent-filter>
这次SecondActivity就可以响应action(ACTION_START)和category(DEFAULT/MY_CATEGORY)了。当我们再次点击FirstActivity中的Button1按钮时就可以成功跳转到SecondActivity了。
隐式Intent不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity。接下来我们尝试调用系统的浏览器打开一个网页。修改FirstActivity中的按钮点击逻辑:
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.baidu.com")
startActivity(intent)
}
这里我们指定这个Intent的action是“Intent.ACTION_VIEW”,并通过Uri.parse()将地址字符串解析成一个uri对象,然后将这个uri对象传递给Intent。这次在FirstActivity中点击Button1按钮,可以看到我们成功跳转到系统浏览器并打开百度了。
除了打开指定的页面,我们还可以在AndroidManifest文件中为Activity的< intent-filter>标签额外配置一个< data>标签用来更精准的指定当前Activity能够响应的数据。< data>标签中主要配置以下内容:
①我们新建一个ThirdActivity.java文件用来实验< data >标签中的scheme配置。它的third_layout布局文件:
<?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"
android:orientation="vertical">
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 3" />
</LinearLayout>
在AndroidManifest.xml中:
<activity
android:name=".ThirdActivity"
android:exported="false"
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
//指定数据协议必须为"https"
<data android:scheme="https" />
</intent-filter>
</activity>
这次我们在FirstActivity中点击Button3时,系统就会弹出提示框要我们选择使用哪个App来打开百度网页。由于我们在清单文件中配置了ThirdActivity可以响应https网页请求的操作,所以我们的ActivityTest应用也会出现在选择框里供用户选择。(这里只是演示data标签的使用,实际上ThirdActivity并不具备可以响应网页请求的功能。)
②接下来我们看一下如何在我们的程序中调用系统拨号盘:
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
这样我们在点击FirstActivity的Button1按钮时就会调用系统默认的拨号盘并自动帮我们填入10086号码了。
Intent不仅可以用来启动Activity,也可以在启动Activity的时候传递数据。
Intent提供了一系列 putExtra() 方法的重载,使得我们可以将数据以键值对的形式暂存在Intent中,在启动另一个Activity后再将这些数据从Intent中取出来。例如我们想将FirstActivity中的data字符串传递给SecondActivity,就可以这么写:
button1.setOnClickListener {
//需要传递给SecondActivity的数据data
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)
//将要传递的数据通过putExtra方法以键值对的形式暂存在Intent中
key value
intent.putExtra("extra_data", data)
startActivity(intent)
}
接下来,我们需要在SecondActivity中取出FirstActivity传递过来的数据:
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
//通过key来接收另一个Activity传递过来的数据
val extraData = intent.getStringExtra("extra_data")
Log.d("SecondActivity", "extra_data is : $extraData")
}
}
这次我们点击FirstActivity页面的button就会跳转到SecondActivity页面中,并且在打印出FirstActivity传递过来的字符串:
除了getStringExtra的方法,我们还可以是用getIntExtra、getBooleanExtra等方法来向Intent中保存不同类型的数据。
Intent不仅可以传递数据给下一个Activity,也可以返回数据给上一个Activity。返回数据给上一个页面有两种触发方式:
①在Activity销毁时返回结果给上一个Activity:
Activity类中还有一个 startActivityForResult() 方法,它会在目标Activity销毁时返回一些数据给当前Activity。startActivityForResult()方法接收两个参数:①Intent ②请求码(必须>=0,用于判断数据来源的)。 我们修改FirstActivity中的按钮的点击事件:
button1.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
请求码(标识跳转的目标Activity)
startActivityForResult(intent, 1001)
}
接下来我们需要实现SecondActivity中按钮的点击事件:
SecondActivity中的这个Intent没有任何"意图",它的作用仅仅是用于传递数据。setResult()方法非常重要,专门用于向上一个Activity返回数据。setResult()方法接收两个参数:①向上一个Activity返回处理结果(RESULT_OK或RESULT_CANCELED) ②带有数据的Intent。
button2.setOnClickListener {
val intent = Intent()
//向上一个Activity返回的数据
key value
intent.putExtra("data_return", "Hello FirstActivity")
处理结果 带有数据的intent
setResult(RESULT_OK, intent)
finish()
}
现在SecondActivity中点击Button3就能够销毁当前页面向FirstActivity传递数据了。由于我们是使用startActivityForResult()方法启动SecondActivity的,当SecondActivity被销毁后会调用上一个Activity的onActivityResult()方法。因此我们需要在FirstActivity中重写这个方法来得到返回的数据:
请求码 处理结果 带有返回数据的Intent
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//通过请求码确定返回的数据是否来自SecondActivity
when (requestCode) {
//若数据来自SecondActivity并且处理结果为OK
1001 -> if (resultCode == RESULT_OK) {
//将返回的数据保存至returnedData中
val returnedData = data?.getStringExtra("data_return")
Log.d("FirstActivity", "returned data is: $returnedData")
}
}
}
②按下Back返回键时返回数据给上一个Activity:
在上面的例子中我们是通过在SecondActivity中点击Button2按钮来触发页面销毁并返回FirstActivity的。那么当用户点击Back返回按钮返回到FirstActivity时该怎么返回数据给FirstActivity呢?我们可以通过在SecondActivity中重写onBackPressed()方法来解决:
override fun onBackPressed() {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
setResult(RESULT_OK, intent)
finish()
}
这样当用户按下Back按钮就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。
我们可以通过startActivityForResult()方法打开另一个Activity,并在另一个Activity销毁时返回一些数据给上一个Activity。遗憾的是startActivityForResult()方法在低版本Android上已经被弃用,转而用registerForActivityResult()方法进行替代。
2024年2月2日更新:
①在Activity销毁时返回结果给上一个Activity:
由于startActivityForResult()方法在高版本Android上被弃用了,所以我们来学习一下如何使用他的替代方法。下面的示例中使用到了ViewBinding来绑定控件,如果你还没使用过ViewBinding可以参考我的第23篇文章。
我们期望有MainActivity——>SecondActivity后,销毁SecondActivity并返回数据给MainActivity。下面是MainActivity.kt中的代码:
class MainActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityMainBinding
private val requestDataLauncher =
//注册ActivityResult的观察者 当启动的Activity返回结果时 这个观察者会被触发
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val data = result.data?.getStringExtra("mData")
Toast.makeText(this,data,Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
mBinding.mButton1.setOnClickListener {
val intent = Intent(this, ThirdActivity::class.java)
//通过ActivityResult观察者启动一个Activity 并在其销毁后返回结果
requestDataLauncher.launch(intent)
}
}
}
下面是SecondActivity中的代码:
class ThirdActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityThirdBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityThirdBinding.inflate(layoutInflater)
setContentView(mBinding.root)
mBinding.mButton2.setOnClickListener {
val data = "This is data for MainActivity!"
val intent = Intent()
// 将数据存放到intent中
intent.putExtra("mData", data)
setResult(RESULT_OK, intent)
finish()
}
}
}
这样当我们在SecondActivity中点击按钮finish当前Activity时,就会将数据返回给MainActivity。
②按下Back返回键时返回数据给上一个Activity:
在旧版本中我们是通过重写onBackPressed()方法来实现返回按键的监听的,但是在Android 13版本中onBackPressed()方法被弃用了,我们可以使用AndroidX 的API来实现滑动手势功能的回调。
首先在build.gradle.kts文件中中添加以下依赖:
dependencies {
implementation("androidx.activity:activity-ktx:1.9.0-alpha02")
· · ·
}
然后在AndroidManifest.xml清单文件中将enableOnBackInvokedCallback属性设置为true:
<application
android:enableOnBackInvokedCallback="true"
· · ·
</application>
最后注册OnBackPressedCallback()方法来处理返回手势:
class SecondActivity: AppCompatActivity() {
private lateinit var mBinding: ActivityThirdBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityThirdBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// 在Activity中处理返回手势
onBackPressed(true){
val data = "This is data for MainActivity!"
val intent = Intent()
intent.putExtra("mData", data)
setResult(RESULT_OK, intent)
finish()
}
}
}
fun AppCompatActivity.onBackPressed(isEnabled: Boolean, callback: () -> Unit) {
onBackPressedDispatcher.addCallback(this,
object : OnBackPressedCallback(isEnabled) {
override fun handleOnBackPressed() {
callback()
}
})
}
这样当用户按下Back按钮就会执行onBackPressed()方法中的代码,我们在这里添加返回数据的逻辑就可以了。
在Android中Activity是可以层叠的:
想象以下场景:应用中有一个页面A,用户在页面A的基础上启动了页面B,此时页面A就进入了停止状态。若这个时候系统内存不足,就很可能将页面A回收掉了。当用户按下Back键返回页面A时,其实还是会正常显示页面A的。但是会重新执行页面A的onCreate()方法而不是onRestart()方法,因为页面A在被回收后会被重新创建一次。需要注意的是,页面A中可能存在临时数据(例如文本框中已经输入的文字)。在上面的情况下,当用户按下Back键返回页面A时会发现页面A中的临时数据(例如文本框中已经输入的文字)都没了,这可太糟糕了!
为此,Activity专门提供了一个 onSaveInstanceState()方法 来解决这个问题。onSaveInstanceState()方法可以保证在页面被回收之前一定会被调用,因此我们可以通过这个方法来解决临时数据的保存问题。onSaveInstanceState()方法携带一个Bundle类型的参数,我们可以用putString()、putInt()等方法将数据以键值对的形式保存到Bundle中。
例如:MainActivity——>NormalActivity(此时MainActivity被回收、临时数据丢失)——>MainActivity。我们就可以在MainActivity中添加以下代码来保存当前页面中的临时数据:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "Something you wish to save"
key value
outState.putString("data_key", tempData)
}
这样就将临时数据保存下来了,那么该如何恢复呢?Activity的onCreate()方法其实也有一个Bundle类型的参数:
override fun onCreate(savedInstanceState: Bundle?) {···}
这个Bundle参数一般情况下都是null。但是如果你在Activity被回收之前,通过onSaveInstanceState()方法保存了数据,那么这个Bundle参数就会带有之前保存的全部数据。为了恢复MainActivity页面中的数据,我们只需要将数据提取出来即可:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
//通过key将保存的临时数据取出
val tempData = savedInstanceState.getString("data_key")
Log.d("MainActivity", "tempData is :$tempData")
}
}
取出值后再做相应的恢复操作就可以啦,比如说将文本内容再重新赋值给文本输入框等等,这里只是简单的打印一下。
在Kotlin中,Activity的启动模式决定了Activity与返回栈之间的关联方式。Activity的启动模式一共有四种:① standard模式 、② singleTop模式 、③ singleTask模式、 ④ singleInstance模式。我们可以在AndroidManifest.xml文件中通过给< activity>标签指定android:launchMode=”XXX”来选择启动模式。
standard模式是Activity的默认启动方式。在standard模式下,每当启动一个新的Activity,他就会在返回栈中入栈,并且处于栈顶的位置。例如我们在A页面中有一个按钮,当我们点击这个按钮时会再次打开一个A页面。如果我们点击3次按钮,那么就会创建出三个新的A页面实例(实例标识是不一样的)。此时我们需要连续按3次Back键才能退出程序。
在singleTop模式下,若启动Activity时发现该Activity已经位于返回栈的栈顶,则认为可以直接复用它,不会再创建新的Activity实例。
<activity
android:name=".SingleTopActivity"
//singleTop模式
android:launchMode="singleTop"
android:label="This is SingleTopActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
这次如果我们重复之前的动作,不管我们点击多少次按钮,都不会再创建新的Activity实例。因为当前A页面已经处于返回栈的栈顶,每当再次启动一个A页面时,都会直接使用处于栈顶的Activity。因此A页面只会有一个实例,我们只需要按一次Back返回键就能退出程序。
不过,若我们由A页面跳转到B页面,然后再由B页面跳转到A页面。这时栈顶发生了变化(A1——>B——>A2),所以这两个A页面的实例是两个完全不同的实例。当你按下Back返回键,会从A2页面回到B页面,再按一次Back返回键会从B页面回到A1页面,再次按下Back键才会退出程序(A2——>B——>A1——>Exit)。
在启动新的Activity时,系统会先在返回栈中查找是否已经存在该Activity的实例:
<activity
android:name=".SingleTaskActivity"
//singleTask模式
android:launchMode="singleTask">
···
</activity>
如果我们还是由A页面跳转到B页面,然后再由B页面跳转到A页面,这时栈顶的状态是:A1——>B——>A1。当我们在B页面中启动A页面时,会发现返回栈中已经存在一个A页面的实例,并且是位于B页面之下。这时B页面会从返回栈中出栈,而A页面会重新成为栈顶。此时A页面会调用onRestart()方法,B页面会调用onDestroy()方法。
singleInstance模式不同于以上三种模式。系统会为这种模式的Activity创建一个新的返回栈来管理这个Activity,并且这个返回栈中只会有这个Activity的实例。这么做的意义是什么呢?
设想一下,我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,用前面三种模式是没办法实现的。由于每个应用程序都有自己的返回栈,在前面三种模式的情况下,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。
而使用singleInstance模式就可以解决这个问题。在这种模式下,系统会创建一个单独的返回栈来管理这个Activity。不管是哪个应用程序来访问这个Activity,都共用这个单独的返回栈,这样就解决了共享Activity实例的问题。
<activity
android:name=".SingleInstanceActivity"
//singleInstance模式
android:launchMode="SingleInstanceActivity">
···
</activity>
我们来举个简单的例子,一共有A、B、C三个页面。我们只设置B页面的启动模式为singleInstance,然后进行页面跳转:A页面——>B页面——>C页面。这时你会发现,A页面的和C页面是存放在同一个返回栈中。由于B页面我们设置它的启动模式为singleInstance模式,所以B页面实例是存放在一个单独的返回栈中。
这时如果我们在C页面按下Back返回键,那么是会直接回到A页面的,这是因为A、C两页面的实例是存放在同一个返回栈中的。当我们在A页面中再按下Back返回键时,会直接回到B页面,这是因为A、C页面的返回栈已经空了,于是就显示了另一个返回栈的栈顶,也就是B页面所在的返回栈,这时我们再按下Back返回键,这时所有返回栈都为空了,也就退出了程序(C页面——>A页面——>B页面——>Exit)。
通过前面的学习你已经掌握了Activity的大部分基础内容了。为了方便你更好的在项目中使用和管理Activity,你还需要掌握几个小技巧。这些小技巧会在你的项目或者工作中起到很大的作用,在后续的实战文章(例如第13、16章)中也会有涉及到。
当我们的项目越来越复杂,会有很多Activity,例如我们从A界面——>B界面——>C界面。这个时候,如果我们想退出应用程序是非常不方便的,我们需要连按多次Back键才行(按下Home键只是把程序挂起,并没有退出程序)。通过下面这个小技巧,就可以轻松实现注销或退出功能。
我们新建一个名为ActivityController的单例类用来管理全局的Activity。通过ActivityController,不论在那个界面,只需要调用finishAllActivities()方法就可以关闭所有界面。ActivityController.kt的代码如下:
/**
* 用于管理全局Activity的单例类
*/
object ActivityController {
//用于管理所有Activity的集合
private val activities = ArrayList<Activity>()
//往集合中添加Activity
fun addActivity(targetActivity: Activity) {
activities.add(targetActivity)
}
//从集合中移除Activity
fun removeActivity(targetActivity: Activity) {
activities.remove(targetActivity)
}
//关闭集合中的所有的Activity
fun finishAllActivities() {
for (activity in activities) {
//判断Activity是否正在销毁中
if (!activity.isFinishing) {
activity.finish()
}
}
//销毁所有Activity后清空集合
activities.clear()
}
}
我们将ActivityController声明为单例类,这样类中的所有方法都可以直接通过类名来访问。首先声明了一个ArrayList< Activity>集合用来存放所有的Activity,然后提供了addActivity()、removeActivity()、finishAllActivities()方法用来管理这个Activity集合。在finishAllActivities()方法中,我们遍历Activity集合中的每一个Activity,然后调用finish()方法将它们逐一销毁,最后调用clear()方法清空这个Activity集合。
在创建好单例工具类后,紧接着创建一个BaseActivity作为所有Activity的父类,这样便于我们对全局Activity进行管理。我们在BaseActivity的onCreate()方法和onDestroy()方法中实现了Activity集合元素的添加和移除。当某个Activity继承自BaseActivity后,在该Activity的生命周期中会自动调用父类BaseActivity的onCreate()和onDestroy()方法,进而实现Activity集合的添加和移除:
/**
* 全局Activity的父类
*/
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
//将当前Activity添加到全局Activity列表中的
ActivityController.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
//从全局Activity列表中移除当前Activity
ActivityController.removeActivity(this)
}
}
这样我们若想实现退出应用程序或者注销登陆操作,只需要调用ActivityController.finishAllActivities() 方法就可以了。
//销毁Activity列表中所有的Activity
ActivityController.finishAllActivities()
如果你想在销毁所有Activity后再杀掉当前应用程序的进程,以保证程序完全退出。你可以在调用finishAllActivities()方法后加上以下代码:
//kill当前应用程序进程
android.os.Process.killProcess(android.os.Process.myPid())
需要注意的是killProcess()方法只能用于杀掉当前程序的进程,不能用于杀掉其他程序(不然那就太离谱了)。
在实际项目或者工作中有很多Activity可能并不是你负责开发的,而你却要启动这个Activity。这个时候你很可能不知道要向该Activity中传递那些数据。我们可以通过下面这个方法轻松解决:
class NewsContentActivity : AppCompatActivity() {
· · ·
// 通过companion object定义一个静态方法
companion object {
fun actionStart(context: Context, title: String, content: String) {
val intent = Intent(context, NewsContentActivity::class.java).apply {
putExtra("news_title", title)
putExtra("news_content", content)
}
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
· · ·
}
· · ·
}
假如我们需要跳转到NewsContentActivity,并且要求传入新闻的标题和新闻内容这两个数据。我们可以通过在companion object{ }结构声明一个静态方法actionStart(),然后通过intent将NewsContentActivity所需要的所有数据传递过来。这样我们如果想要在其他页面启动NewsContentActivity,可以使用actionStart()方法,Android Studio会要求我们传入所需要的数据才能启动NewsContentActivity。
NewsContentActivity.actionStart(parent.context,news.title,news.content)