什么是Activity?
Activity 是一个为用户提供了可交互的操作界面的组件。几乎所有用户能看到和操作的内容,都是基于 Activity 的。
一个应用中可以包含零个或多个Activity,虽然应用可以没有Activity(如提供后台服务的应用),但这样用户就无法看到界面,甚至打不开应用了。
Activity的基本用法
之前的 Activity 是 Android Studio 自动帮我们创建的,现在我们来手动创建一下,以加深对 Activity 的理解。
首先,新建一个Android项目,然后模板选择 “No Activity”:
项目名你可以填 “NoActivity”,最后点击 Finish,完成项目的创建。
手动创建Activity类
右键自动生成的 com.example.noactivity 包,点击 New -> Activity -> Empty Views Activiy,进入创建 Activity 的界面。我们将 Activity 命名为 FirstActivity,然后不要勾选 Generate a Layout File 和 Launcher Activity 这两个选项。
这是因为勾选后,会自动给 FirstActivity 创建对应的布局文件,并且会自动将 FirstActivity 设置为当前项目的主 Activity。而这些操作我们是要手动一步步来完成从而加深理解的,所以不勾选。
创建完成后,任何 Activity 都应该重写 onCreate() 方法,Android Studio 已经自动帮我们重写了这个方法:
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
只不过方法体内还只是简单地调用了一下父类的 onCreate() 方法,这是默认的实现,接下来会加入很多自己的逻辑。
创建并加载布局
Android程序的设计讲究逻辑和视图分离,最好是一个 Activity 都能对应一个布局。布局是用来显示界面内容的,我们来为 FirstActivity 手动创建一个布局文件。
右键 app\src\main\res 目录,点击 New -> Directory,新建一个名为 layout 的目录。然后右键刚刚创建的 layout 目录,点击 New -> Layout Resource File,新建一个布局资源文件,文件命名为first_layout,然后根元素修改为 LinearLayout。
新建的 first_layout 布局资源文件的代码如下:
<?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">
</LinearLayout>
我们往布局中添加一个按钮:
<?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/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button 1" />
</LinearLayout>
我们在这里添加了一个按钮元素,并在它内部定义了几个属性:
-
android:id是给当前的元素定义一个唯一的标识符,之后我们可以在代码中通过该标识符获取到这个元素,然后操作这个元素。如果你要在 XML 中引用一个已存在的id,就使用
@id/id_name;而要在XML中定义一个新的id,就要使用@+id/id_name。 -
android:layout_width指定了当前元素的宽度,match_parent表示让当前元素和父元素一样宽。 -
android:layout_height指定了当前元素的高度,wrap_content表示当前元素的高度刚好包裹其内容。 -
android:text指定了元素显示的文字。
按钮添加完了,我们来预览一下界面的布局:
布局完成了,我们在 Activity 中加载这个布局。回到 FirstActivity,在 onCreate() 方法中:
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 加载first_layout布局
setContentView(R.layout.first_layout)
}
}
通过 setContentView() 方法来给当前的 Activity 加载一个布局。方法中传入的是布局资源文件的id,项目中任何资源都会在 R 文件中生成一个资源id。
在AndroidManifest中注册
所有的 Activity 都必须在 AndroidManifest.xml 文件中进行注册,否则应用在尝试启动它时会崩溃。
实际上我们创建的 FirstActivity 已经注册过了,这是 Android Studio 自动帮我们完成的。我们可以在 app/src/main/AndroidManifest.xml 文件中看到:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.noactivity">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NoActivity"
tools:targetApi="31">
<activity
android:name=".FirstActivity"
android:exported="false" />
</application>
</manifest>
Activity 的注册在 <application> 标签内,我们通过 <activity> 标签来完成的。
在 <activity> 标签中,我们通过 android:name 属性来指定注册的是哪一个Activity,填入的 “.FirstActivity” 是 com.example.noactivity.FirstActivity 的缩写,这是因为在外层的 <manifest> 标签通过 package 属性声明了程序的包名是 com.example.noactivity,所以在注册 Activity 时,这部分可以省略。
仅仅注册还不能使程序能够正常运行,还需要指定程序的主 Activity,不然程序不知道在启动时要显示哪个界面。
只需在 <activity> 标签的内部添加 <intent-filter> 标签,并在这个标签的内部添加两句声明:<action android:name="android.intent.action.MAIN" /> 和 <category android:name="android.intent.category.LAUNCHER" />。
并且要将 android:exported 属性设为 true,变为可被外部访问的状态,这样才能被系统的启动器(Launcher)从应用外部访问和启动。
然后我们还可以在 <activity> 标签中通过 android:label 属性指定 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"
package="com.example.noactivity">
<application
...>
<activity
android:name=".FirstActivity"
android:exported="true"
android:label="This is FirstActivity">
<!--新增声明-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
注意:
如果程序中没有声明任何主Activity,这个程序仍然是可以正常安装的,只是你无法在启动器(桌面)中看到或者打开这个程序。这种程序一般是作为第三方服务供其他应用在内部进行调用的。
我们运行一下,结果如下所示:
至此,我们已经成功完成了手动创建 Activity,我们再来看看在 Activity 中还能干什么。
在Activity中使用Toast
Toast 是 Android 系统提供的一种非常好的提醒方式,我们可以使用它将一些简短的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间。
现在,我们给界面中的按钮添加点击回调,在回调中弹出一个 Toast 提示,修改 FirstActivity 的 onCreate() 方法:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.first_layout) // 加载布局
val button1 = findViewById<Button>(R.id.button1)
button1.setOnClickListener {
val toast = Toast
.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT)
toast.show()
}
}
findViewById() 方法可以通过元素的标识符id来获取在布局文件中定义的元素。我们传入的 R.id.button1 正是按钮的 id。该方法会返回一个继承自 View 类型的对象,我们可以通过泛型手动指定它的具体类型为 Button。
得到按钮后,我们通过 setOnClickListener() 为按钮注册一个点击监听器,每当按钮被点击时,就会执行
我们传入的 Lambda 表达式中的代码。
然后我们在 Lambda 表达式中通过 makeText() 方法创建了一个 Toast 对象,然后调用该对象的 show() 方法弹出提示。
makeText() 方法的第一个参数类型是 Context,就是 Toast 要求的上下文。由于 Activity 继承了Context 类,本身就是一个 Context 对象,所以这里直接传入 this 就可以了。第二个参数是显示的文本内容。第三个参数是 Toast 提示显示的时长,有两个常量可供选择:Toast.LENGTH_SHORT 和 Toast.LENGTH_LONG。
再次运行程序并点击一次按钮,结果如下:
视图绑定 (View Binding)
虽然 findViewById() 方法很直观,但在大多数情况下,我们会使用视图绑定(View binding) 来替代它。视图绑定可以让我们更好地与视图进行交互,它提供了编译时的类型安全(避免因手动进行类型转换导致的错误)和空安全(避免因id拼写错误导致的空指针异常),让代码更简洁、更健壮。
首先在需要启用视图绑定的模块的 build.gradle 文件中,将 viewBinding 属性设置为 true,如下所示:
android {
...
buildFeatures {
viewBinding = true
}
}
这样就可以启用视图绑定,然后视图绑定会给该模块中的每一个 XML 布局文件自动生成一个绑定类,我们可以通过这个绑定类的实例来获取相应的布局中的根元素以及拥有标识符的元素。生成的绑定类的名称是 XML 布局文件的名称转为大驼峰形式,然后在末尾加上 “Binding”。就比如我们的 first_layout 布局文件所生成的绑定类名就是 FirstLayoutBinding。
在 Activit 中使用视图绑定也很简单:首先通过绑定类的 inflate() 静态方法实例化绑定类。然后调用getRoot() 方法获取根元素,将根元素传递给 onCreate() 方法内的 setContentView() 方法中。最后从绑定类实例中获取并操作视图元素实例。
知道了这些,我们来对之前的代码进行修改,修改后的代码如下所示:
class FirstActivity : AppCompatActivity() {
// 声明绑定类变量
private lateinit var binding: FirstLayoutBinding // FirstLayoutBinding 是自动生成的绑定类的名称
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 通过 inflate 方法实例化绑定类
binding = FirstLayoutBinding.inflate(layoutInflater)
// 2. 获取视图的根元素
val view = binding.root
// 3. 将根元素传递给 setContentView 方法
setContentView(view)
// 4. 通过绑定类实例获取布局中的按钮,并设置点击监听器
binding.button1.setOnClickListener {
val toast = Toast
.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT)
toast.show()
}
}
}
这种写法其实是官方推荐的写法,除非有特殊情况,否则我们都应该优先使用 View Binding 来替代 findViewById() 方法。
在Activity中使用Menu
为 Activity 添加菜单也非常简单。
首先在 res 目录下新建一个名称为 menu 的文件夹,然后右键该文件夹,点击 New -> Menu Resource File,创建一个菜单资源文件,命名为 main。
创建完了之后,在 main.xml 文件中加上两个菜单项:
<?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>
其中 <item> 标签是用来创建菜单项的,通过 android:id 属性可以给这个菜单项指定一个唯一的标识符,通过 android:title 给这个菜单项指定名称。
接着回到 FirstActivity 中,重写 onCreateOptionsMenu() 方法(快捷键:Ctrl + O),代码如下:
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// 将菜单资源加载到 Activity 中
menuInflater.inflate(R.menu.main, menu)
return true // 允许菜单在界面显示
}
其中 menuInflater 的获取实际上是调用了父类的 getMenuInflater() 方法,这是 Kotlin 提供的语法糖。方法返回了一个 MenuInflater 对象,再调用它的 inflate 方法就能给当前的 Activit 创建菜单了。
inflate() 方法接收两个参数:第一个参数用于指定通过哪一个资源文件来创建菜单,这里传入R.menu.main;第二个参数用于指定我们的菜单项将添加到哪一个Menu对象当中,这里直接使用menu参数。最后方法的返回值是决定是否显示菜单,返回true就会将菜单显示到界面;反之则无法显示菜单。
然后光有菜单的样子还不行,还要完成它的底子,就是实现菜单项的功能。我们在 FirstActivity 中重写 onOptionsItemSelected() 方法,代码如下:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
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
}
每当我们点击了菜单下的菜单项后,就会调用此方法。方法会传入被点击的菜单项,然后我们可以根据菜单项的id,实现不同的点击逻辑处理,这里我们就简单地弹出一个通知。
重新运行程序,标题栏右侧就会有一个菜单按钮:
点击会弹出菜单项:
点击菜单项,会有对应 Toast 提示:
销毁一个Activity
怎么销毁一个 Activity 呢?
只需用户按下返回键就行了,系统会默认销毁当前的 Activity。在代码中,就是主动调用 Activit 提供的 finish() 方法。
修改 button1 按钮的点击回调来销毁 FirstActivity:
binding.button1.setOnClickListener {
finish() // 销毁当前 Activity
}
再次运行程序,点击按钮,就会销毁当前 Activity,接着退出应用,因为这是应用中唯一一个 Activity。