说明: 本文是郭霖《第一行代码-第3版》的读书笔记
Activity是包含用户界面的组件,主要用于和用户交互,一个应用程序中可以包含一个或多个Activity
3.2 基本用法
创建一个Empty Activity
,之后自己来添加Activity
手动创建Activity
project模式下,在app/src/main/java/com.example.projectname
目录下新建一个Activity。
项目的任何Activity都应该重写OnCreate()
方法。
创建和加载布局
Android程序设计讲究逻辑和视图分离。最好每一个Activity都能对应一个布局。
如果没有在创建Activity时勾选Generate Layout File
,则需要我们在res目录下手动添加一个layout文件。
在app/src/main/res/
目录下创建layout
文件夹,添加layout resource file
,这是一个xml文件。
在OnCreate()
内通过setContentView()
方法来传入布局文件,参数为布局文件的ID,如:
setContentView(R.layout.first_layout)
项目中添加的任何资源文件都会在R文件中生成一个相应资源的对应ID
在AndroidManifest文件中注册
所有的Activity都需要在AndroidManifest.xml中注册才能生效。但实际上Studio已经帮我们注册了FirstActivity。
除此之外,程序要想正常允许还需要配置主Activity。即在<activity>
标签内添加<intent-filter>
标签,然后加入action
和categoty
的两句声明语句
此时程序就可以正常运行了。
Toast
Toast
是Android提供的一种提醒方式,可以将一些短小的信息通知给用户。自动消失、不占屏幕空间
现在想点击button触发toast,首先需要找到定义在Layout文件中的Button,可以根据ID来找:
findViewById()
获取布局文件中控件的实例,但是在Kotlin中,会在app/buid.gradle
文件头部引入一个kotlin-android-extensions
插件,可以根据布局文件中控件的ID自动生成一个具有相同名称的变量,因此可以不需要调用findViewById()
var button1: Button = findViewById(R.id.button1)
//定义button的按下监听事件
button1.setOnClickListener{
//使用Toast,Toast.makeText()静态方法创建一个Toast对象,再调用show()显示
//Toast.makeText()需传入三个参数:1.Context,2.显示的文本内容, 3.Toast显示的时长(内置有Toast.LENGTH_SHORT和LONG)
//Activity本身就是一个Content对象,所以这里可以直接传入this
Toast.makeText(this, "You clicked button1", Toast.LENGTH_SHORT).show()
}
在Activity中使用Menu
首先在res
文件夹下新建一个menu
文件夹,新建一个Android resource file
,这是一个xml文件,选择menu类型。然后可以在menu文件中添加item
。可以定义item
的id
和title
,item
相当于是一个菜单项,id
是它唯一的标识
<?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>
如何添加这个menu呢?
需要在主Activity中重写onCreateOptionsMenu()
方法。这里可以使用Ctrl+O
快捷键得到重写下拉表
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// menuInflater实际上来自getMenuInflater(),是kotlin中的语法糖
// 调用inflate()方法,给当前Activity创建菜单,第一个参数表示要使用哪一个menu文件,第二个参数指定这一个菜单项添加到哪一个
// Menu对象中,这里直接添加到传入的这个menu中
menuInflater.inflate(R.menu.menu, menu)
return true //true表示允许这个menu显示
}
接下来可以定义菜单响应事件,
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// 判断传入的是哪一个Item
when(item.itemId) {
// 如果item.itemId == R.id.add_item
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
}
销毁一个Activity
只需要按一下Back
键就可以销毁当前Activity, 代码实现是调用finish()
方法
3.3 使用Intent在Activity之间穿梭
如果有多个Activity,如何从主Activity跳到另外一个Activity呢?
再建一个Activity和相应的布局文件,Android Studio已经帮我们在AndroidManifest.xml中注册了这个新建的Activity。
另外由于这个Activity不是主Activity,也不需要配置<intent-filter>
。
至此,这个新的Activity已创建完成,接下来是启动它。
Intent
1.显示Intent
Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)
,构建出intent后,再用startActivity()
方法接收intent
参数,来启动Activity
// 这里的SecondActivity::class.java相当于Java中的SecondActivity::class
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
2.隐式Intent
隐式Intent通过指定action
和category
等信息,交由系统分析启动哪个Activity
。
- 只有
<action>
和<category>
中的内容同时匹配Intent
中指定的action
和category
时,该Activity
才能响应Intent
// AndroidManifest.xml
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.activitytest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
// FirstActivity.kt
// 在xml中指定了SecondActivity可以响应"com.example.activitytest.ACTION_START"这个action(自己定义的)
// 而android.intent.category.DEFAULT是默认的category,调用startActivity()方法时会自动将其添加进intent中
val intent = Intent("com.example.activitytest.ACTION_START")
startActivity(intent)
- 每个Intent只能指定一个action,但能指定多个category
此时如果我们在intent上添加一个categoty
val intent = Intent("com.example.activitytest.ACTION_START")
// 添加category
intent.addCategory("com.example.activitytest.MY_CATEGORY")
startActivity(intent)
此时程序就相应不了了,因为我们的SecondActivity并没有这个Category
我感觉是<Intent-filter>
中可以指定多个category,只要能满足intent的category就可以响应该intent,但不能少于intent的category
更多隐式Intent的用法
使用隐式的Intent,不仅可以启动本程序的Activity,也可以启动其他程序的Activity,使多个程序之间的功能共享成为可能。
// 指定Intent的Action,这是Android系统内置的Action
val intent = Intent(Intent.ACTION_VIEW)
// 通过Uri.Parse()方法将网址字符串解析为Uri对象,再调用Intent的setData()方法将Uri对象传进去
intent.data = Uri.parse("https://www.baidu.com/") //指定协议是https
startActivity(intent) //start的是哪个activity?
也可以创建一个新的Activity,指定其Action为android.intent.action.VIEW,以及data来响应这个intent:
<activity android:name=".ThirdActivity">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"/>
</intent-filter>
</activity>
除了https协议外,还可以指定其他协议,比如geo为地理位置,tel为拨打电话。
// 调用系统拨打电话界面
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:1008") //指定协议是tel
startActivity(intent) //start的是哪个activity?
如何向下一个Activity传递数据
思路:Intent中提供了一系列putExtra()
方法的重载,因此可以把数据先暂存在Intent中,启动另一个activity后,再将数据取出。
//传递数据
val data = "Hello, SecondActivity!"
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("extra_data", data)
//------------------------------------------
//另一个Activity中取出数据
val extraData = intent.getStringExtra("extra_data") // intent其实是getIntent(),即启动第二个Activity的Intent
Log.d("SecondActivity","extra data is $extraData")
返回数据给上一个Activity
// 首先需要在启动Activity的时候选择这个方法,这样在此Activity销毁的时候才能够回调onActivityResult方法
val intent = Intent(this, SecondActivity::class.java)
startActivityForResult(intent, 1) // 第二个参数是请求码,是唯一值就可以
//------------------------------------------
// 点击button2让其返回数据
val intent = Intent() //创建一个空的Intent保存数据
intent.putExtra("data_return", "Hello FirstActivity") //将要返回的数据保存到intent中
setResult(Activity.RESULT_OK, intent) //这个方法专门用于向上一个Activity返回数据,第二个参数就是把intent给传递回去
finish() //销毁当前Activity, 从而回调onActivityResult()方法
//------------------------------------------
// 第一个Activity中重写onActivityResult()方法
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// 首先根据请求码判断数据来源,再根据resultCode判断结果是否处理正确
when (requestCode) {
1 -> if (resultCode == Activity.RESULT_OK) {
val returnData = data?.getStringExtra("data_return") //这里不能用intent来获取数据
Log.d("FirstActivity", "returned data is $returnData")
}
}
}
上述的onActivityResult()
方法是在activity销毁的时候才调用的,为了点击Back键也能保存要返回的数据,需要重写OnBackPressed()
方法
override fun onBackPressed() {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
setResult(Activity.RESULT_OK, intent)
//finish()
super.onBackPressed() //如果自己不调用这个父类的处理方法,你就要自己写 finish()
}
3.4 Activity的生命周期
返回栈
Android的Activity是可以层叠的,放在了栈中,启动新的Activity的时候入栈,Back的时候出栈,栈顶的Activity显示在当前界面。
Activity状态
- 运行状态,位于返回栈的栈顶时处于该状态
- 暂停状态,不处于栈顶,但仍然可见时,如对话框弹出的背景Activity。此时Activity是仍然存活的,系统一般不会回收。
- 停止状态,当Activity不处于栈顶,完全不可见的时,有可能被系统回收
- 销毁状态,当Activity从返回栈中pop后就处于该状态,系统最倾向于回收这种状态的Activity
Activity的生存期
小结:
- 当Activity对用户可见时,是在
onStart()
和onStop()
内 - 当Activity准备好和用户交互时(当然是可见的),是在
onResume()
和onPause()
内 onCreate()
和onDestroy()
经历的是完整的生命周期,前者进行各种初始化操作,后者释放资源
- 可以在
onStart()
内加载用户可见的资源,在onStop()
内销毁用户可见的资源 - 可以在
onResume()
内加载和用户交互的资源,在onStop()
内销毁和用户交互的资源
感觉生命周期还是挺重要的,可以帮助管理好资源
使用对话框Activity
<! -- 在AndroidManifest.xml中配置对话框Activity-->
<activity android:name=".DialogActivity"
android:theme="@style/Theme.AppCompat.Dialog">
</activity>
Activity被回收了怎么办
当Activity进入Stop
状态时是有可能被系统回收的,此时再返回该Activity,临时数据和状态就会被销毁。此时执行的是onCreate()
方法。我们可以重写onSaveInstanceState()
方法,此方法在Activity在销毁前一定会被调用。
// 接受Bundle类型的参数,提供了一系列的方法用来保存数据
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "Something you just typed."
val intData = 1024
outState.putString("data_key", tempData)
outState.putInt("data2_key", intData)
}
如何取出数据呢?如果内存被回收,则返回Activity时会调用onCreate()
方法,此时就可以在onCreate()
中加载保存的数据
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(tag, "onCreate() called")
// 加载保存的数据
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
val intData = savedInstanceState.getInt("data2_key")
Log.d(tag, "tempData is $tempData and $intData")
}
}
这里需要注意两点:
- Intent可以和Bundle一起用来传递数据,将数据存入Bundle里,再将Bundle传入Intent中,传给下一个Activity。
- 当手机的屏幕发生旋转时,Activity也会经历一个重新创建的过程,虽然也可用上述的
onSaveInstanceState()
方法来保存数据,但是有更优雅的解决方案。
3.5 Activity的启动模式
Activity有四种启动模式。
- standard,是默认的启动模式。系统不在乎返回栈中是否已有Activity,每次启动都会创建一个该Activity的新实例。
- singleTop,该模式下,若返回栈的栈顶已经是该Activity,则直接使用,不会创建新的实例。
- singleTask, 该模式下,整个返回栈中只会存在该Activity的一个实例,启动该Activity时,系统会先检查返回栈中是否存在该Activity的实例,若存在,则直接使用该实例,并将在其之上的其他Activity统统出栈。
- singleInstance,该模式下,会有一个单独的返回栈管理这个Activity,便于不同应用程序共享Activity。前面三种模式,都是一个应用程序有一个返回栈。按Back返回时,会先返回该返回栈中的Activity,直到本返回栈为空,才返回下一个返回栈中的Activity。当全部返回栈为空时,就退出了应用程序。
- 使用
taskId
查看当前返回栈的id,本质上是getTaskID()
方法的语法糖
启动模式在AndroidManifest.xml
中更改
<activity
android:name=".FirstActivity"
<!-- 设置启动模式 -->
android:launchMode="singleTop"
android:label="This is FirstActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
3.6 Activity的最佳实践
如何判断当前处于哪个Activity
当阅读别人的项目时,有时我们无法知晓各个界面对应的是哪个Activity,这时有一个小技巧。
编写一个BasicClass,让其打印所在类的名称:
package com.example.activitytest
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
open class BasicActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// javaClass表示获取当前实例的Class对象,相当于JAva中的getClass()方法,simpleName获取当前实例类的类名
// 而BaicActivity::class.java表示获取BasicActivity类的Class对象
Log.d("BaseActivity", javaClass.simpleName)
}
}
再让所有的Activity继承这个Basic类而非之前的AppCompatActivity
这样每当我们进入一个界面时就会打印当前界面对应的Activity类名。
随时随地退出程序
当我们打开了多个Activity,需要一个一个Back才能退出,这样会显得很麻烦。可以通过写一个单例类来管理这些Activity并提供一个finishAll()方法。
object ActivityCollector {
private val activities = ArrayList<Activity>()
fun addActivity(activity: Activity) {
activities.add(activity)
}
fun removeActivities(activity: Activity) {
activities.remove(activity)
}
fun finishAll() {
for (activity in activities) {
// 判断Activity是否正在销毁
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}
接下来修改之前写的BasicActivity,在onCreate()
和onDestroy()
方法内添加单例类的add和remove:
open class BasicActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}
override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivities(this)
}
}
现在想随时退出程序,只需要调用ActivityCollector单例类的finnishAll()
方法即可。
button3.setOnClickListener {
//点击button3直接全部退出
ActivityCollector.finishAll()
// 为保证程序安全退出,可以杀掉当前进程,killProcess()只能用于杀掉当前程序的进程。
android.os.Process.killProcess(android.os.Process.myPid())
}
启动Activity的最佳写法
前面学过,通过构建Intent再调用startActicity()
或者startActivityForResult()
来启动Activity,数据传递也可以借用Intent和Bundle完成。
问题:当某个Activity不是由你开发的,但启动它需要传参,怎么传参呢?
要么询问作者,要么自己看代码。为了方便别人,可以给每个Activity启动时都编写启动方法如下:
// 在你写的这个Activity类中加入
// companion object 使得其内的方法变为静态方法
companion object {
fun actionStart(context: Context, data1: String, data2: String) {
val intent = Intent(context, SecondActivity::class.java)
intent.putExtra("param1", data1)
intent.putExtra("param2", data2)
context.startActivity(intent)
}
}
// 那在启动这个Activity的时候可以简化成如下形式,则传参一目了然。
SecondActivity.actionStart(this, "data1", "data2")