情之所起

说起香港,脑海中闪过的第一个念头就是《重庆森林》——重庆大厦里奔走的林青霞、趴在半山扶梯上窥看编号633住处的王菲和她哼唱的《California Dreamin’》。终于要去见识这个憧憬太久的目的地了啊。

阅读全文 »

简介

Kotlin是一种现代但已经成熟的编程语言,它简洁、安全、可与Java和其他语言互操作,并提供了许多在多个平台之间重用代码的方法。它由JetBrains公司于2011年设计和开发,并在2016年正式发布。Kotlin旨在解决Java语言在编码效率和代码质量方面存在的问题,并且与Java语言完全兼容。Kotlin通过简化语法、提供更强大的功能以及减少样板代码的编写,使得开发者能够更高效地编写清晰、简洁而又安全的代码。


Android Studio中创建 Kotlin File文件

  • Kotlin Class 文件:用于定义一个类,适合面向对象编程的场景,如定义 ActivityFragment 等。
  • Kotlin File 文件:用于定义顶级函数、变量等,适合函数式编程的场景,如 Jetpack Compose 的 UI 组件定义。

这两者的选择主要取决于你要定义的内容和你所使用的编程范式。在 Jetpack Compose 中,由于其函数式编程的特点,使用 Kotlin File 是更自然的选择。而在传统的 Android 开发中,定义 Activity 类时,Kotlin Class 是更合适的。

在目标目录下,右键New - Kotlin Class/File选择Class或者File都可以,Android Studio会根据文件内容自动变更

image-20240822150142616 image-20240822150300407

Kotlin文件的运行入口是main()方法:

1
2
3
fun main() {
println("Hello Kotlin!")
}

标准函数和静态方法

标准函数with、run和apply

Kotlin的标准函数指的是Standard.kt文件中定义的函数,任何Kotlin代码都可以自由地调用。

with函数接收两个参数:任意类型的对象,lambda表达式。在lambda表达式中提供第一个参数对象的上下文,并使用lambda表达式中的最后一行代码作为返回值返回

1
2
3
4
val result = with(obj) {
// obj的上下文
"value" // 返回值
}

with函数可以在连续调用同一个对象的多个方法时,让代码更精简。

1
2
3
4
5
6
7
8
9
10
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = with(StringBuilder()) {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
toString()
}
println(result)

run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。首先run函数通常不会直接调用,而是要在某个对象的基础上调用;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。

1
2
3
4
val result = obj.run {
// obj的上下文
"value" // 返回值
}

apply函数和run函数也是极其类似的,都要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。

1
2
3
4
// result == obj
val result = obj.apply {
// 这里是obj的上下文
}

定义静态方法

和绝大多数主流编程语言不同的是,Kotlin却极度弱化了静态方法这个概念,想要在Kotlin中定义一个静态方法反倒不是一件容易的事。
那么Kotlin为什么要这样设计呢?因为Kotlin提供了比静态方法更好用的语法特性,单例类

1
2
3
4
5
object Util {
fun doAction() {
println("do action")
}
}

虽然这里的doAction()方法并不是静态方法,但是我们仍然可以使用Util.doAction()的方式来调用,这就是单例类所带来的便利性。

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,而如果只是希望让类中的某一个方法变成静态方法的调用方式,可以使用companion object

1
2
3
4
5
6
7
8
9
10
class Util {
fun doAction1() {
println("do action1")
}
companion object {
fun doAction2() {
println("do action2")
}
}
}

companion object这个关键字实际上会在Util类的内部创建一个伴生类,而doAction2()方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证Util类始终只会存在一个伴生类对象,因此调用Util.doAction2()方法实际上就是调用了Util类中伴生对象的doAction2()方法。


Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法

如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class Util {
fun doAction1() {
println("do action1")
}

companion object {
@JvmStatic
fun doAction2() {
println("do action2")
}
}
}

@JvmStatic注解只能加在单例类或companion object中的方法上。由于doAction2()方法已经成为了真正的静态方法,那么现在不管是在Kotlin中还是在Java中,都可以使用Util.doAction2()的写法来调用了。


顶层方法指的是没有定义在任何类中的方法,Kotlin编译器会将所有的顶层方法全部编译成静态方法。想要定义一个顶层方法,首先需要创建一个Kotlin文件。对着任意包名右击 → New → Kotlin File/Class,在弹出的对话框中输入文件名即可,创建类型要选择File

例如创建一个Helper.kt文件:

1
2
3
fun doSomething() {
println("do something")
}

在Kotlin代码中,所有的顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例,直接输入方法名即可

但如果是在Java代码中调用,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。Kotlin编译器会自动创建对应的的Java类,doSomething()方法以静态方法的形式定义在HelperKt类里。


扩展函数

扩展函数表示,即使不修改某个类的源码,仍然可以打开这个类,向该类添加新的函数。

1
2
3
fun ClassName.methodName(param1: Int, param2: Int): Int {
return 0
}

定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中。

文件名虽然并没有固定的要求,但是建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于以后查找。当然,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域

例如定义一个函数,用于统计字符串中字母的数量。可以创建一个String.kt文件:

1
2
3
4
5
6
7
8
9
10
fun String.lettersCnt(): Int {
var cnt = 0
// 自动拥有String实例的上下文,可以直接遍历this,this代表字符串本身
for (char in this) {
if (char.isLetter()) {
cnt++
}
}
return cnt
}

运算符重载

Kotlin允许我们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。运算符重载使用的是operator关键字

例如让两个Money对象相加,Money.kt文件:

1
2
3
4
5
6
7
8
9
10
11
12
class Money(val value: Int) {

operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}

operator fun plus(newValue: Int): Money {
val sum = value + newValue
return Money(sum)
}
}

这样,就可以将两个Money对象相加,也能直接和数字相加。

1
2
3
4
5
6
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
val money4 = money3 + 20
println(money3.value)
println(money4.value)

高阶函数

如果一个函数接收另一个函数作为参数,或者返回值类型是另一个函数,该函数就称为高阶函数。

函数类型的基本规则:

1
(String, Int) -> Unit
  • ->的左边用来声明函数接收的参数,多个参数之间逗号分隔,如果不接收任何参数,写一对空括号
  • ->的右边用于声明函数的返回值类型,如果没有返回值就使用Unit

将上述函数类型添加到某个函数的参数声明或者返回值声明上,这个函数就是一个高阶函数:

1
2
3
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}

高阶函数允许让函数类型的参数来决定函数的执行逻辑。Lambda表达式是最常见也是最普遍的高阶函数调用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) {
n1, n2 -> n1 + n2
}
val result2 = num1AndNum2(num1, num2) {
n1, n2 -> n1 - n2
}
println("result1 is $result1")
println("result2 is $result2")
}

apply函数可以用于给Lambda表达式提供一个指定的上下文,当需要连续调用一个对象的多个方法时,apply函数可以让代码更精简。

1
2
3
4
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}

这里给StringBuilder类定义了一个build扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是StringBuilder。

StringBuilder.:在函数类型的前面加上ClassName.,表示这个函数类型是定义在哪个类当中。

现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。这里仍然用吃水果这个功能来举例:

1
2
3
4
5
6
7
8
9
10
11
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

可以看到,build函数的用法和apply函数基本上是一模一样的,只不过我们编写的build函数目前只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的。如果想实现apply函数的这个功能,需要借助于Kotlin的泛型才行

Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行开销完全消除。内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字声明即可

为什么Kotlin要提供一个noinline关键字来排除内联功能呢?内联的函数类型参数只允许传递给另一个内联函数,这是它最大的局限性。而非内联的函数类型参数可以自由地传递给其他任何函数。另外,内联函数和非内联函数有一个重要的区别,内联函数引用的lambda表达式可以使用return关键字进行函数返回非内联函数只能进行局部返回

Android Studio

Google将JDK、Android SDK都集成了,Android官方网就可以下载最新的开发工具:下载 Android Studio


创建Android项目

  1. New Project - Phone and Tablet - Empty Activity
image-20240821123557291
  1. Minimum SDK:设置项目最低能兼容的Android版本

Android模拟器

  1. Device Manager - Create Virtual Device
image-20240821124808648
  1. 选择目标参数的设备
image-20240821125040156
  1. 选择操作系统版本。其余配置全部默认。

    image-20240821125231735

  2. 点击Start按钮,就会启动模拟器。

    image-20240821125446010

  3. Run 'app' 将项目运行到模拟器上

    image-20240821135026657

  4. 项目成功运行,显示了Hollo Android。打开模拟器中的启动器列表,可以看到已经安装了对应的应用。

    image-20240821135414741

项目结构

任何一个新建的项目都会默认使用Android模式的项目结构,但这并不是项目真实的目录结构,而是被Android Studio转换过的。这种项目结构简洁明了,适合进行快速开发,但是对于新手来说可能不易于理解。可以切换成项目模式

image-20240821135823142

image-20240821140107800

  • .gradle.idea

    放置Android Studio自动生成的文件,无需关心。

  • app

    项目中的代码、资源等内容。

    • build

      编译时自动生成的文件,无需过多关心。

    • src

      • androidTest:编写Android Test测试用例,可以对项目进行一些自动化测试。

      • main

        • java:防止Java和Kotlin代码,默认会自动生成一个MainActivity.kt文件

        • res:项目中使用到的图片、布局、字符串等资源。

          • drawable:图片

            虽然Android Studio没有帮我们自动生成,但是应该自行创建drawable-xxhdpi等目录。最好能够给同一张图片提供几个不同分辨率的版本,分别放在这些目录下,然后程序运行的时候,会自动根据当前运行设备分辨率的高低选择加载哪个目录下的图片。但更多的时候美工只会提供一份图片,这时把所有图片都放在drawable-xxhdpi目录下就好了,这是最主流的设备分辨率目录

          • mipmap:图标

          • values:字符串、颜色、样式、尺寸

          • xml:布局、菜单等

      • test:编写单元测试用力,对项目进行自动化测试的另一种方式。

      • .gitignore:将app模块内指定的目录或文件排除在版本控制之外。

      • build.gradle.kts

        app模块的gradle构建脚本,通常这个文件中的内容无需修改。

      • proguard-rules.pro:指定项目代码的混淆规则。当代码开发完成后打包成安装包文件,如果不希望代码被哦破解,通常会将代码进行混淆。

  • gradle

    包含gradle wrapper的配置文件,使用gradle wrapper的方式,不需要提前把gradle下载好,而是根据本地的缓存情况,自动决定是否联网下载

    • libs.versions.toml定义各个依赖库的版本号,使得依赖管理更加清晰和一致。
  • .gitignore

    记录git版本控制之外的文件、目录。

  • build.gradle.kts

    项目全局的gradle构建脚本,通常这个文件中的内容无需修改。

  • gradle.properties

    项目全局的gradle配置文件。

  • gradlewgradlew.bat

    用于在命令行中执行gradle命令。

  • local.properties

    指定本机中的Andriod SDK路径,自动生成。

  • settings.gradle.kts

    指定项目中所有引入的模块。通常情况下都是自动引入。


AndroidManifest.xml

app\src\main\AndroidManifest.xml中有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
<!--  注册MainActivity,没有注册的Activity是不能使用的  -->
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.HelloWorld">
<intent-filter>
<!-- 表示该activity是项目的主activity,点击应用图标,首先启动的就是这个Activity -->
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

MainActivity继承自ComponentActivityComponentActivity继承自Activity,**Activity 是一个提供用户界面的组件**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置应用程序的UI内容,setContent是一个高阶函数,接受一个lambda表达式作为参数
setContent {
// Composable函数,定义了应用程序的主题,HelloWorldTheme包含了整个UI布局
HelloWorldTheme {
// A surface container using the 'background' color from the theme
// Surface 也是一个Composable 函数,它提供了一个绘制表面,用于渲染其他Composable 函数
// fillMaxsize修饰符来填充整个屏幕,并使用了主题中定义的背景色
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// 接收一个字符串参数,在Surface中显示一个问候语
Greeting("Android")
}
}
}
Log.d("MainActivity", "onCreate execute")
}
}

项目中的资源使用

  • app\src\main\res\values\strings.xml

    1
    2
    3
    <resources>
    <string name="app_name">HelloWorld</string>
    </resources>

    这里定义了一个应用程序名的字符串,有以下两种方式可以用来引用:

    1. 在代码中使用通过R.string.app_name
    2. 在XML中使用@string/app_name

    其中string可以被替换,如果是引用的图片资源就可以替换成drawable,如果是引用的应用图标就可以替换成mipmap,如果是引用的布局文件就可以替换成xml,以此类推。


app目录下的 build.gradle.kts

Android Studio采用Gradle构建项目。它使用了一种领域特定语言DSL进行项目布置,摒弃了如Maven等基于XML文件的各种繁琐配置

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 该项目使用的Gradle插件,通过 alias 方法引入,这意味着它们的确切 ID 和版本号在 libs.versions.toml 文件中定义。
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
}

// 项目的Android配置信息
android {
namespace = "com.hunter.helloworld"
compileSdk = 34

defaultConfig {
applicationId = "com.hunter.helloworld"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
// 在低于minSdk的版本上使用支持库来支持向量图标
useSupportLibrary = true
}
}
// 配置不同的构建类型
buildTypes {
// 发布版本的构建类型
release {
// 是否对代码进行混淆
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
// 在 libs.versions.toml 文件中定义的依赖
dependencies {

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

settings.gradle.kts

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
26
27
28
// gradle插件管理
pluginManagement {
// 获取插件的仓库
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
// 依赖解析管理
dependencyResolutionManagement {
// 如果在子项目中自定义了仓库,构建将失败。确保所有子项目使用统一的仓库配置,避免潜在的版本冲突或依赖解析问题。
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}

rootProject.name = "HelloWorld"
// 包含一个名为 :app 的子项目。:app 通常指的是 Android 应用程序模块。
include(":app")

日志工具的使用

Android中的日志工具类是android.util.Log,它提供了5个打印日志的方法:

  1. Log.v():verbose,打印那些最琐碎、意义最小的日志信息。是Android中最低的日志级别。
  2. Log.d():debug,打印一些调试信息。
  3. Log.i():info,打印一些比较重要的数据,可以帮助分析用户行为。
  4. Log.w():warn。
  5. Log.e():error。

例如在MainActivity.ktonCreate()方法中添加一行日志:

1
2
3
4
5
6
7
8
9
10
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
...
}
// 第一个参数是tag,一般传入当前类名,便于对日志进行过滤;第二个参数是msg
Log.d("MainActivity", "onCreate execute")
}
}

运行程序,在Android Studio底部工具栏的Logcat中就能看到打印信息。

image-20240822142408233


Activity

Activity是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个Activity,但不包含任何Activity的应用程序很少见。


基本用法

新建一个Android项目,选择No Activity,其余选项和之前章节中创建Android项目-Empty Activity保持一致。等待Gradle构建完成后,项目就创建成功了。

image-20240822150739473


手动创建Activity

Compose 下的 Empty Activity: 使用 Jetpack Compose 的声明式 UI 编程,UI 是通过 Kotlin 代码直接编写的,没有 XML 文件。

Activity 下的 Empty Views Activity: 使用传统的视图系统,UI 是通过 XML 文件定义的,然后在 Activity 中引用这些 XML 布局。

适用场景:

  • Compose 下的 Activity: 适合希望使用现代 UI 工具包 Jetpack Compose 开发的项目,特别适合新项目或者希望采用最新技术的项目。
  • 传统 Views Activity: 适合已有的项目或需要兼容传统视图系统的项目,或者开发者更熟悉传统的视图系统。

学习曲线:

  • Jetpack Compose: 更现代,但需要学习新的编程范式(声明式 UI)。
  • 传统视图系统: 更传统,许多开发者已经熟悉。

项目创建成功后,仍然会默认使用Android模式的项目结构,手动改成Project模式。


传统的视图系统:Activity 下的 Empty Views Activity

右键选择要创建Activity的包,New - Activity - Empty Views Activity,会弹出一个创建Activity的对话框。

image-20241011224328104

不勾选Generate a Layout File,创建完成后,可以看到生成的是一个Kotlin文件:

1
2
3
4
5
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}

声明式 UI 编程:Compose 下的 Empty Activity

右键选择要创建Activity的包,New - Compose - Empty Activity,会弹出一个创建Activity的对话框。

image-20240822151537061

不勾选Launcher Activity,否则会将创建的Activity设置为当前项目的主Activity

image-20240822151714288

创建完成后,可以看到生成的是一个Kotlin文件:

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
26
27
28
29
30
31
32
class TestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ActivityTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
ActivityTestTheme {
Greeting("Android")
}
}

创建和加载布局

传统的视图系统

Android程序的设计讲究逻辑和视图分离,最好每个Activity都能对应一个布局,布局用来显示界面内容。

右击app/src/main/res目录,New -> Directory,会弹出一个新建目录的窗口,创建一个layout目录。再对着layout目录右键,New -> Layout Resource File,将布局文件命名为first_layout,根元素默认选择为ConstraintLayout,改用LinearLayout

image-20241011220755549

创建出来的xml文件,默认代码为:

1
2
3
4
5
6
7
<?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">

</LinearLayout>

通过<Button>标签,添加一个按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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语法,定义id的时候,需要使用@+id/id_name

  • android:layout_width指定当前宽度。

    match_parent表示和父元素一样宽。

  • android:layout_height="wrap_content"

    指定当前高度,wrap_content表示当前元素的高度能刚好包含里面的内容就行。

  • android:text指定元素中显示的文字内容。

一个简单的布局就编写完成了,然后在Activity中加载该布局:

1
2
3
4
5
6
7
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 调用 setContentView 给当前Activity加载布局,传入布局文件的id
setContentView(R.layout.first_layout)
}
}

项目中添加的任何资源,都会在R文件中生成一个相应的资源id


声明式 UI 编程

当你使用 Jetpack Compose 时,布局不再通过传统的 XML 文件来定义,而是使用 Kotlin 代码进行声明式编程。Jetpack Compose 是一种现代的 Android UI 工具包,允许你以更直观和可组合的方式构建用户界面。

在 Compose 中,UI 通过 @Composable 注释的函数来定义。每个 @Composable 函数都是一个 UI 组件。

1
2
3
4
5
6
7
8
9
10
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}

可以使用 @Preview 注解来预览 Composable 函数的效果,而无需运行整个应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
ActivityTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}

image-20240822162401416


AndroidManifest.xml

所有的Activity都要在AndroidManifest.xml中进行注册才能生效。一般情况下,创建的Activity都会被自动注册进来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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
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.ActivityTest"
tools:targetApi="31" >
<activity
android:name=".FirstActivity"
android:exported="false" />
</application>

</manifest>
  • android:name用于指定具体注册哪一个Activity。.FirstActivity是相对引用。

不过,仅仅是这样注册了Activity,我们的程序仍然不能运行,因为还没有为程序配置主Activity。也就是说,程序运行起来的时候,不知道要首先启动哪个Activity。

1
2
3
4
5
6
7
8
9
<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>

添加<intent-filter>标签并在其中添加<action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" />声明即可。

此外,可以使用android:label指定Activity中标题栏的内容。主Activity指定的label不仅会称为标题栏中的内容,还会成为Launcher中应用程序的名称


使用Toast

Toast是Android提供的一种很好的提醒方式,可以将一些短小的信息通知给用户,并在一段时间后自动消失,不占用屏幕空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FirstActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 调用 setContentView 给当前Activity加载布局,传入布局文件的id
setContentView(R.layout.first_layout)

// 调用 findViewById 获取布局文件中定义的元素,返回一个继承自View的泛型对象
val button1: Button = findViewById(R.id.button1)
// 为按钮注册点击监听器
button1.setOnClickListener {
// 调用 makeText 创建Toast对象,再调用 show 进行展示
Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
}
}
}

如果布局文件中的组件很多,使用findViewById就很繁琐,可以通过View Binding来代替,View Binding可以生成代码来直接访问视图,无需使用findViewById

要使用View Binding,首先需要在build.gradle.kts中添加如下内容:

1
2
3
4
5
6
7
8
9
10
android {
...
// viewBinding 用于避免编写findViewById
// Android Studio会自动为我们所编写的每一个布局文件都生成一个对应的Binding类。
// Binding类的命名规则是将布局文件按驼峰方式重命名后,再加上Binding作为结尾。
buildFeatures {
viewBinding = true
}
...
}

然后在代码中使用View Binding代替findViewById

1
2
3
4
5
6
7
8
9
10
11
12
13
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 调用first_layout.xml布局文件对应的Binding类的inflate函数加载该布局
val binding = FirstLayoutBinding.inflate(layoutInflater)
// 传入根元素的实例,就可以成功显示first_layout.xml这个布局的内容
setContentView(binding.root)

// 为按钮注册点击监听器
binding.button1.setOnClickListener {
// 调用 makeText 创建Toast对象,再调用 show 进行展示
Toast.makeText(this, "You clicked Button 1", Toast.LENGTH_SHORT).show()
}
}

使用Menu

首先在res目录下新建一个menu文件夹,右击res目录→New→Directory,输入文件夹名“menu”,点击“OK”。接着在这个文件夹下新建一个名叫“main”的菜单文件,右击menu文件夹→New→Menu resource file。

在main.xml中添加如下代码:

1
2
3
4
5
6
7
8
9
<?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>

回到FirstActivity中来重写onCreateOptionsMenu()方法,重写方法可以使用Ctrl + O快捷键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// menuInflater是语法糖,实际调用了父类的getMenuInflater方法,能够得到一个MenuInflater对象
// 调用其inflate方法就可以给当前Activity创建菜单。
menuInflater.inflate(R.menu.main, menu)
// 允许创建的菜单显示
return true
}

// 定义菜单响应事件
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
}

销毁一个Activity

只要按一下Back键就可以销毁当前的Activity。不过,如果你不想通过
按键的方式,而是希望在程序中通过代码来销毁Activity,当然也可以,Activity类提供了一个finish()方法,我们只需要调用一下这个方法就可以销毁当前的Activity

1
2
3
4
5
// 为按钮注册点击监听器
binding.button1.setOnClickListener {
// 点击按钮,当前的Activity就会被销毁
finish()
}

使用Intent在Activity之间穿梭

Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可用于启动Activity、启动Service以及发送广播等场景。

使用显式Intent

右键选择要创建Activity的包,New - Activity - Empty Views Activity,会弹出一个创建Activity的对话框,不勾选Generate a Layout File,创建SecondActivity

对着layout目录右键,New -> Layout Resource File,将布局文件命名为second_layout

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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="@string/button_2" />

</LinearLayout>

相应地,重写SecondActivity,应用该布局:

1
2
3
4
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.second_layout)
}

Intent有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<?> cls)

  • Context要求提供一个启动Activity的上下文。
  • Class用于指定想要启动的目标Activity

Activity类提供了一个startActivity()方法,专门用于启动Activity。

重写FirstActivity中的按钮监听代码:

1
2
3
4
5
6
7
// 为按钮注册点击监听器
binding.button1.setOnClickListener {
// 在当前上下文的基础上,打开SecondActivity
val intent = Intent(this, SecondActivity::class.java)
// 执行该intent
startActivity(intent)
}

就能实现在FirstActivity的基础上打开SecondActivity,按一下Back键就可以销毁当前Activity,从而回到上一个Activity


使用隐式Intent

相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出想要启动哪一个Activity,而是指定一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的Activity去启动。

通过在AndroidManifest.xml中的<activity>标签下配置<intent-filter>内容,可以指定当前Activity能够响应的action和category:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name=".SecondActivity"
android:exported="false">
<intent-filter>
<!-- 当前activity可以响应的action -->
<action android:name="com.huntr.myapplication.ACTION_START" />
<!-- 附加信息,更精确地指明当前activity能够响应地intent中还可能带有的category -->
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.hunter.myapplication.MY_CATEGORY" />
</intent-filter>
</activity>

相应地,修改FirstActivity.kt中的按钮点击事件:

1
2
3
4
5
6
7
8
9
// 为按钮注册点击监听器
binding.button1.setOnClickListener {
// 在当前上下文的基础上,打开指定的action能对应的activity
val intent = Intent("com.hunter.myapplication.ACTION_START")
// 指定category
intent.addCategory("com.hunter.myapplication.MY_CATEGORY")
// 执行该intent
startActivity(intent)
}

隐式Intent的更多用法

使用隐式Intent,不仅可以启动自己程序内的Activity,还可以启动其他程序的Activity,这就使多个应用程序之间的功能共享成为了可能


调用浏览器

比如你的应用程序中需要展示一个网页,这时你没有必要自己去实现一个浏览器(事实上也不太可能),只需要调用系统的浏览器来打开这个网页就行了。

1
2
3
4
5
6
7
// 为按钮注册点击监听器
binding.button1.setOnClickListener {
// 在当前上下文的基础上,打开指定的action能对应的activity,ACTION_VIEW是android内置的动作
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.bing.com")
startActivity(intent)
}

同时,可以在AndroidManifest.xml中的activity标签下的<intent-filter>标签中配置<data>标签:

  • android:scheme:指定数据的协议部分,如https
  • android:host:指定数据的主机名部分,如www.bing.com
  • android:port:指定数据的端口部分
  • android:path:指定主机名和端口之后的部分
  • android:mimeType:指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有<data>标签中指定的内容和Intent中携带的Data完全一致时,当前Activity才能够响应该Intent。


调用拨号界面
1
2
3
4
5
6
// 为按钮注册点击监听器
binding.button1.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
image-20241013210111954

向下一个Activity传递数据

在启动Activity时传递数据的思路很简单,Intent中提供了一系列putExtra()方法的重载,可以把我们想要传递的数据暂存在Intent中,在启动另一个Activity后,只需要把这些数据从Intent中取出就可以了。

例如在FirstActivity中:

1
2
3
4
5
6
7
8
// 为按钮注册点击监听器
binding.button1.setOnClickListener {
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)
// 通过putExtra方法传递一个字符串
intent.putExtra("extra_data", data)
startActivity(intent)
}

在SecondActivity中获取传递的数据:

1
2
val stringExtraData = intent.getStringExtra("extra_data")
Log.d("SecondActivity", "extra data is $stringExtraData")

返回数据给上一个Activity

返回上一个Activity只需要按一下Back键就可以了,没有一个用于启动Activity的Intent来传递数据。可以通过registerForActivityResult()注册对Activity结果的处理。

在FirstActivity做如下改动:

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
class FirstActivity : AppCompatActivity() {
// 定义 ActivityResultLauncher,registerForActivityResult用于处理活动结果的注册
private val resultLauncher = registerForActivityResult(
// 这个 resultLauncher 将用于启动一个新活动并接收其结果
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data
// 处理返回的数据,?用于安全地操作可能的空值
val returnedData = data?.getStringExtra("data_return")
Log.d("FirstActivity", "returned data is $returnedData")
}
}
...
override fun onCreate(savedInstanceState: Bundle?) {
...

// 为按钮注册点击监听器
binding.button1.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
// 启动SecondActivity,并等待结果
resultLauncher.launch(intent)
}
}
}

在SecondActivity中:

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
26
27
28
29
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 调用second_layout.xml布局文件对应的Binding类的inflate函数加载该布局
val binding = SecondLayoutBinding.inflate(layoutInflater)
// 传入根元素的实例,就可以成功显示second_layout.xml这个布局的内容
setContentView(binding.root)

// 为 button2 注册点击监听器
binding.button2.setOnClickListener {
returnDataToFirstActivity()
}

// 注册 OnBackPressedCallback 来处理后退按钮事件
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
returnDataToFirstActivity()
}
})
}

private fun returnDataToFirstActivity() {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
// setResult 专门用于向上一个Activity返回数据
setResult(RESULT_OK, intent)
finish()
}
}

Activity的生命周期

返回栈

Android中的Activity是可以层叠的。我们每启动一个新的Activity,就会覆盖在原Activity之上,然后点击Back键会销毁最上面的Activity,下面的一个Activity就会重新显示出来。

Android使用任务(task)来管理Activity,一个任务就是一组存放在栈里的Activity的集合,这个栈也被称作返回栈(back stack)。


Activity状态

每个Activity在生命周期中,最多可能有4种状态。

  1. 运行状态

  2. 暂停状态

    当一个Activity不再处于栈顶,但仍然可见时(并不是每个Activity都会占满整个屏幕,比如对话框),就进入了暂停状态。只有在内存极低的情况下,系统才会考虑回收这种Activity

  3. 停止状态

    当一个Activity不再处于栈顶,并且完全不可见,就进入了停止状态。其他地方需要内存时,处于停止状态的Activity有可能会被系统回收。

  4. 销毁状态

    Activity从栈中移除后,就变成了销毁状态。系统最倾向于回收这种状态的Activity,以保证手机的内存充足。


Activity的生存期

Activity类中定义了7个回调方法,覆盖了Activity生命周期的每个环节。

  1. onCreate():在Activity被创建的时候调用。应该在该方法中完成初始化操作,比如加载布局、绑定事件等。
  2. onStart():在Activity由不可见变为可见时调用
  3. onResume():在Activity准备好和用户交互时调用。此时Activity一定位于返回栈的栈顶,并处于运行状态。
  4. onPause():在系统准备去启动或恢复另一个Activity时调用。通常在这个方法中,将一些消耗CPU的资源放掉、保存一些关键数据。但执行速度一定要快,避免影响新的栈顶Activity使用
  5. onStop():在Activity完全不可见时调用。和onPause()方法的主要区别在于,如果启动的新Activity是一个对话框式的Activity,那么onPause()方法会得到执行,onStop()方法并不会执行
  6. onDestroy():在Activity被销毁之前调用,之后Activity的状态变为销毁状态。
  7. onRestart():在Activity由停止变为运行状态之前调用

上述7个方法,除了onRestart()之外,都是两两相对,从而可以将Activity分为以下3种生存期:

  • 完整生存期。ActivityonCreate()onDestroy()方法之间所经历的就是完整生存期。
  • 可见生存期。ActivityonStart()onStop()之间所经历的就是可见生存期。在可见生存期内,Activity对于用户总是可见的,即使可能无法和用户进行交互。可以通过这两个方法合理地管理那些对用户可见的资源。比如onStart()方法中对资源进行加载,而在onStop()方法中对资源进行释放,从而保证处于停止状态的Activity不会占用过多内存
  • 前台生存期。ActivityonResume()方法和onPause()方法之间所经历的就是前台生存期。在前台生存期内,Activity总是处于运行状态,此时的Activity是可以和用户进行交互的
image-20241014134452549

新建一个项目ActivityLifeCycleTest为例子,New Project -> Empty Views Activity

image-20241014163751536

会默认生成MainActivityactivity_main.xml。继续创建两个子Activity:NormalActivityDialogActivity。同样地,以Empty Views Activity形式创建,不勾选Generate a Layout File。之后在res/layout目录下,手动创建normal_layout.xmldialog_layout.xml两个布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
<?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">

<!-- 显示一行文字 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a normal activity" />
</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
<?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">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is a dialog activity" />

</LinearLayout>

相应地,NormalActivityDialogActivity中添加对应的布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
class NormalActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.normal_layout)
}
}

class DialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_layout)
}
}

为了将DialogActivity设置成对话框的样式,需要在AndroidManifest.xml中配置<activity>标签:

1
2
3
4
5
<!-- android:theme属性用于指定主题 -->
<activity
android:name=".DialogActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Dialog" />

接着,修改activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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/startNormalActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start NormalActivity" />

<Button
android:id="@+id/startDialogActivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start DialogActivity" />

</LinearLayout>

最后,修改MainActivity中的代码:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class MainActivity : AppCompatActivity() {
private val tag = "MainActivity"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
// 调用activity_main.xml布局文件对应的Binding类的inflate函数加载布局
val binding = ActivityMainBinding.inflate(layoutInflater)
// 传入根元素的实例,就可以成功显示activity_main.xml这个布局的内容
setContentView(binding.root)

binding.startNormalActivity.setOnClickListener {
val intent = Intent(this, NormalActivity::class.java)
startActivity(intent)
}

binding.startDialogActivity.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, "onDestroy")
}

override fun onRestart() {
super.onRestart()
Log.d(tag, "onRestart")
}
}

在Activity的7个回调方法中分别打印了一句话,这样就可以通过观察日志来更直观地理解Activity的生命周期


Activity被回收了怎么办

当一个Activity进入了停止状态,是有可能被系统回收的。那么想象以下场景:应用中有一个Activity A,用户在Activity A的基础上启动了Activity B,Activity A就进入了停止状态,这个时候由于系统内存不足,将Activity A回收掉了,然后用户按下Back键返回Activity A,会出现什么情况呢?其实还是会正常显示Activity A的,只不过这时并不会执行onRestart()方法,而是会执行Activity A的onCreate()方法,因为Activity A在这种情况下会被重新创建一次

但是Activity A中是可能存在临时数据和状态的。打个比方,MainActivity中如果有一个文本输入框,现在你输入了一段文字,然后启动NormalActivity,这时MainActivity由于系统内存不足被回收掉,过了一会你又点击了Back键回到MainActivity,你会发现刚刚输入的文字都没了,因为MainActivity被重新创建了。

Activity中还提供了一个onSaveInstanceState()回调方法,这个方法可以保证在Activity被回收之前一定会被调用,因此我们可以通过这个方法来解决问题。onSaveInstanceState()方法会携带一个Bundle类型的参数,Bundle提供了一系列的方法用于保存数据,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型数据,以此类推。每个保存方法需要传入两个参数,第一个参数是键,用于后面从Bundle中取
值,第二个参数是真正要保存的内容

1
2
3
4
5
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "Something you just typed"
outState.putString("data_key", tempData)
}

我们一直使用的onCreate()方法其实也有一个Bundle类型的参数。这个参数在一般情况下都是null,但是如果在Activity被系统回收之前,通过onSaveInstanceState()方法保存数据,这个参数就会带有保存的全部数据,我们只需要再通过相应的取值方法将数据取出即可,比如将文本内容重新赋值到文本输入框上。

1
2
3
4
5
6
7
override fun onCreate(savedInstanceState: Bundle?) {
...
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
Log.d(tag, "tempData is $tempData")
}
}

Intent还可以结合Bundle一起用于传递数据。首先我们可以把需要传递的数据都保存在Bundle对象中,然后再将Bundle对象存放在Intent里。到了目标Activity之后,先从Intent中取出Bundle,再从Bundle中一一取出数据。

另外,当手机的屏幕发生旋转的时候,Activity也会经历一个重新创建的过程,虽然这个问题同样可以通过onSaveInstanceState()方法来解决,但是一般不太建议这么做,因为对于横竖屏旋转的情况,有更加优雅的解决方
案,后续介绍。


Activity的启动模式

启动模式一共有4种,分别是standard、singleTop、singleTask和singleInstance,可以在AndroidManifest.xml中通过给<activity>标签指定android:launchMode属性来选择启动模式。

standard

standard是Activity默认的启动模式。在standard模式下,每当启动一个新的Activity,就会在返回栈中入栈,并处于栈顶的位置。系统不在乎Activity是否已经在返回栈中,每次启动都会创建一个Activity的新实例。


singleTop

当Activity的启动模式指定为singleTop,在启动Activity时如果发现返回栈的栈顶已经是该Activity,则认为可以直接使用它,不会再创建新的Activity实例。当返回栈的栈顶并不是该Activity时,再次启动该Activity还是会创建新实例。


singleTask

使用singleTop模式,如果Activity没有处于栈顶位置,还是可能创建多个Activity实例。而使用singleTask,每次启用该Activity时,系统首先会在返回栈中检查是否存在该Activity的实例,如果存在则直接使用,并把该Activity之上的所有其他Activity都出栈,如果没有发现就创建新的Activity实例。


singleInstance

不同于以上3种启动模式,指定为singleInstance模式的Activity会启用一个新的返回栈来管理这个Activity(其实如果singleTask模式指定了不同的taskAffinity,也会启动一个新的返回栈)。

假设程序中有一个Activity允许其他程序调用,如果想实现其他程序和当前程序共享这个Activity的实例,就可以使用singleInstance模式解决这个问题,不管哪个应用程序来访问这个Activity,都共用一个返回栈,解决了共享Activity实例的问题。


Activity的实践技巧

知晓当前是在哪一个Activity

创建一个BaseActivity类,由于不需要让BaseActivity在AndroidManifest.xml中注册,所以创建一个普通的Kotlin类即可。然后让BaseActivity继承AppCompatActivity,并重写onCreate()方法:

1
2
3
4
5
6
7
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// javaClass相当于在java中调用getClass()方法,获取实例的class对象,调用simpleName获取当前实例的类名
Log.d("BaseActivity", javaClass.simpleName)
}
}

再将FirstActivity、SecondActivity和ThirdActivity由继承AppCompatActivity改为继承BaseActivity。这样,每当进入一个Activity界面时,其类名就会被打印出来,就能知道当前所在的Activity。


随时退出程序

只需要一个专门的集合对所有的Activity进行管理

新建一个单例类ActivityCollector作为Activity的集合:

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
26
27
28
29
/**
* Object: 单例类
* 使用单例类是因为全局只需要一个Activity集合
*/
object ActivityCollector {
// 通过ArrayList暂存Activity
private val activities = ArrayList<Activity>()

fun addActivity(activity: Activity) {
activities.add(activity)
}

fun removeActivity(activity: Activity) {
activities.remove(activity)
}

/**
* 将ArrayList中存储的Activity全部销毁
*/
fun finishAll() {
for (activity in activities) {
// 是否正在销毁中
if (!activity.isFinishing) {
activity.finish()
}
}
activities.clear()
}
}

在BaseActivity的onCreate()方法中调用了ActivityCollector的addActivity()方法,表明将当前正在创建的Activity添加到集合里。然后在BaseActivity中重写onDestroy()方法,并调用了ActivityCollector的removeActivity()方法,表明从集合里移除一个马上要销毁的Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// javaClass相当于在java中调用getClass()方法,获取实例的class对象,调用simpleName获取当前实例的类名
Log.d("BaseActivity", javaClass.simpleName)
ActivityCollector.addActivity(this)
}

override fun onDestroy() {
super.onDestroy()
ActivityCollector.removeActivity(this)
}
}

不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll()
方法就可以了。例如在ThirdActivity界面想通过点击按钮直接退出程序,只需将代码改成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ThirdActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("ThirdActivity", "Task id is $taskId")
// 调用third_layout.xml布局文件对应的Binding类的inflate函数加载该布局
val binding = ThirdLayoutBinding.inflate(layoutInflater)
// 传入根元素的实例,就可以成功显示third_layout.xml这个布局的内容
setContentView(binding.root)

binding.button3.setOnClickListener {
ActivityCollector.finishAll()
// 杀掉当前进程,以保证程序完全退出,killProcess只能用于杀掉当前程序的进程,不能杀掉其他程序
android.os.Process.killProcess(android.os.Process.myPid())
}
}
}

启动Activity的最佳写法

启动Activity的方法相信你已经非常熟悉了,首先通过Intent构建出当前的“意图”,然后调用startActivity()startActivityForResult()方法将Activity启动起来,如果有数据需要在Activity之间传递,也可以借助Intent来完成。

假设SecondActivity中需要用到两个非常重要的字符串参数,在启动SecondActivity的时候必须传递过来,那么我们很容易会写出如下代码:

1
2
3
4
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("param1", "data1")
intent.putExtra("param2", "data2")
startActivity(intent)

虽然这样写是完全正确的,但是在真正的项目开发中经常会出现对接的问题。比如SecondActivity并不是由你开发的,但现在你负责开发的部分需要启动SecondActivity,而你却不清楚启动SecondActivity需要传递哪些数据。这时无非就有两个办法:一个是你自己去阅读SecondActivity中的代码,另一个是询问负责编写SecondActivity的同事。你会不会觉得很麻烦呢?其实只需要换一种写法,就可以轻松解决上面的窘境。

1
2
3
4
5
6
7
8
9
10
11
12
class SecondActivity : BaseActivity() {
...
companion object {
fun actionStart(context: Context, data1: String, data2: String) {
val intent = Intent(context, SecondActivity::class.java).apply {
putExtra("param1", data1)
putExtra("param2", data2)
}
context.startActivity(intent)
}
}
}

这样写,可以非常清晰地知道启动SecondActivity需要传递哪些数据。同时,还能简化启动Activity的代码:

1
2
3
button1.setOnClickListener {
SecondActivity.actionStart(this, "data1", "data2")
}

UI开发

常用控件的使用

创建一个名为UIWidgetTest的Empty Views Activity项目。

TextView

TextView用于在界面显示文本信息。修改默认创建的activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#00ff00"
android:textSize="24sp"
android:text="This is TextView" />

</LinearLayout>
  • android:gravity:TextView中的文字默认居左上角对齐,通过android:gravity显式指定对齐方式:top、bottom、start、end、center等。可以用“|”来同时指定多个值,这里我们指定的是”center”,效果等同于”center_vertical|center_horizontal”,表示文字在垂直和水平方向都居中对齐。
  • android:textColor="#00ff00":指定文字颜色
  • android:textSize="24sp":指定文字大小,要使用sp作为单位(这样,当用户在系统中修改了文字显式尺寸时,应用程序中的文字大小才会跟着变化)

Button

1
2
3
4
5
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />

Android系统默认会将按钮上的英文字母全部转换成大写。可以在XML中添加android:textAllCaps="false"这个属性,这样系统就会保留原始文字内容

接着可以为Button的点击事件注册一个监听器:

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener {
// 添加逻辑
}
}

这里调用button的setOnClickListener()方法时利用了Java单抽象方法接口的特性,从而可以使用函数式API的写法来监听按钮的点击事件。这样每当点击按钮时,就会执行Lambda表达式中的代码,只需要在Lambda表达式中添加待实现的逻辑

除了使用函数式API的方式来注册监听器,也可以使用接口的方式注册监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
// 添加逻辑
}
}
}
}

让MainActivity实现View.OnClickListener接口,并重写了onClick()方法,然后在调用button的setOnClickListener()方法时将MainActivity的实例传进去。这样每当点击按钮时,就会执行onClick()方法中的代码了。


EditText

EditText是程序用于和用户进行交互的另一个重要控件,它允许用户在控件里输入和编辑内容,并可以在程序中对这些内容进行处理。

1
2
3
4
5
6
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Type something here"
android:maxLines="2" />
  • android:hint属性用于设定提示性的文本。当我们输入任何内容时,这段文本就会自动消失。

随着输入的内容不断增多,EditText会被不断地拉长。这是由于EditText的高度指定的是wrap_content,因此它总能包含住里面的内容,但是当输入的内容过多时,界面就会变得非常难看。可以使用android:maxLines属性来解决这个问题。这里通过android:maxLines指定了EditText的最大行数为两行,这样当输入的内容超过2行时,文本就会向上滚动,EditText则不会再继续拉伸

还可以结合使用EditText与Button来完成一些功能,比如通过点击按钮获取EditText中输入的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
val inputText = binding.editText.text.toString()
Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
}
}
}
}

ImageView

ImageView是用于在界面上展示图片的一个控件,它可以让程序界面更丰富多彩。

图片通常放在以drawable开头的目录下,并且要带上具体的分辨率。现在最主流的手机屏幕分辨率大多是xxhdpi的,所以我们在res目录下再新建一个drawable-xxhdpi目录:右键 -> New -> Android Resource Directory。

image-20241015225933054

再准备好两张图片,放入该目录下,注意图片的命名要符合Android规范:只能包含小写字母、数字、下划线和点号,并且不能以数字开头

如果放入图片后,该图片被Android Studio标红,尝试重启Android Studio: File -> Invalidate Caches / Restart,然后选择 Invalidate and Restart。之后,就能通过<ImageView>标签往应用中插入图片。

1
2
3
4
5
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/cy1a9741" />

还可以在程序中通过代码动态地更改ImageView中的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
...
binding.imageView.setImageResource(R.drawable.cy1a9740)
}
}
}
}

ProgressBar

ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。

1
2
3
4
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

如何才能让进度条在数据加载完成时消失呢?所有的Android控件都具有可见属性,可以通过android:visibility进行指定,可选值有3种:visible、invisible和gone。

  • visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。
  • invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
  • gone则表示控件不仅不可见,而且不再占用任何屏幕空间。

也可以通过代码来设置控件的可见性,setVisibility()方法,允许传入View.VISIBLEView.INVISIBLEView.GONE这三种值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
// 点击让进度条消失,再点击让进度条出现
if (binding.progressBar.visibility == View.VISIBLE) {
binding.progressBar.visibility = View.GONE
} else {
binding.progressBar.visibility = View.VISIBLE
}
}
}
}
}

还可以通过style指定不同的进度条样式、通过android:max设置进度条的最大值:

1
2
3
4
5
6
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100" />

在代码中可以动态地更改进度条的进度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
// 在当前进度上加10
binding.progressBar.progress += 10
}
}
}
}

AlertDialog

AlertDialog可以在当前界面弹出一个对话框,这个对话框置顶于所有界面元素之上,能够屏蔽其他控件的交互能力,因此AlertDialog一般用于提示一些非常重要的内容或者警告信息

比如为了防止用户误删重要内容,在删除前弹出一个确认对话框:

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
26
27
28
29
30
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.button.setOnClickListener(this)
}

override fun onClick(v: View?) {
when (v?.id) {
R.id.button -> {
// 构建对话框
AlertDialog.Builder(this).apply {
setTitle("This is Dialog") // 设置标题
setMessage("Something important.") // 设置内容
setCancelable(false) // 能否使用Back键关闭对话框
// 设置确定按钮的点击事件
setPositiveButton("OK") { _, _ -> // 目前没有具体的处理逻辑
}
// 设置取消按钮的点击事件
setNegativeButton("Cancel") { _, _ ->
}
show() // 显示对话框
}
}
}
}

基本布局

一个丰富的界面是由很多个控件组成的,借助布局能让各个控件都有条不紊地摆放在界面上。布局是一种可用于放置很多控件的容器,它可以按照一定的规律调整内部控件的位置,从而编写出精美的界面。当然,布局的内部除了放置控件外,也可以放置布局,通过多层布局的嵌套,就能完成一些比较复杂的界面实现。

image-20241016165219944

新建一个Empty Views Activity的UILayoutTest项目,并让Android Studio自动帮我们创建好Activity,Activity名和布局名都使用默认值。


LinearLayout

LinearLayout,线性布局,这个布局会将它所包含的控件在线性方向上依次排列。通过android:orientation属性指定排列方向为vertical则垂直排列,指定的是horizontal,控件就在水平方向上排列。

  • android:layout_gravity:指定控件在布局中的对齐方式。当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度不固定,每添加一个控件,水平方向上的长度都会改变,因此无法指定该方向上的对齐方式,vertical时同理。

    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
    26
    27
    28
    <?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="horizontal">

    <Button
    android:id="@+id/button1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="top"
    android:text="Button 1" />

    <Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:text="Button 2" />

    <Button
    android:id="@+id/button3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:text="Button 3" />

    </LinearLayout>
    image-20241016172001737
  • android:layout_weight:允许使用比例的方式指定控件大小,在手机屏幕的适配性方面可以起到非常重要的作用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <EditText
    android:id="@+id/input_message"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:hint="Type something" />

    <Button
    android:id="@+id/send"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:text="Send" />

    </LinearLayout>

    由于使用了android:layout_weight属性,此时控件的宽度
    不再由android:layout_width来决定,指定成0 dp是一种比较规范的写法。在EditText和Button里将android:layout_weight属性的值都指定为1,表示EditText和Button将在水平方向平分宽度(系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值)。

    还可以通过指定部分控件的layout_weight值来实现更好的效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <EditText
    android:id="@+id/input_message"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:hint="Type something" />

    <Button
    android:id="@+id/send"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Send" />

    仅指定了EditText的android:layout_weight属性,并将Button的宽度改回了wrap_content。这表示Button的宽度仍然按照wrap_content来计算,而EditText则会占满屏幕所有的剩余空间。使用这种方式编写的界面,不仅可以适配各种屏幕,而且看起来也更加舒服


RelativeLayout

RelativeLayout,相对布局,它可以通过相对定位的方式让控件出现在布局的任何位置。也正因为如此,RelativeLayout中的属性非常多。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="Button 1" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:text="Button 2" />

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />

<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:text="Button 4" />

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:text="Button 5" />

</RelativeLayout>

效果如下:

image-20241016223814163

上面例子中的每个控件都是相对于父布局进行定位,如果要相对于控件进行定位,则如下所示:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toStartOf="@id/button3"
android:text="Button 1" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toEndOf="@id/button3"
android:text="Button 2" />



<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toStartOf="@id/button3"
android:text="Button 4" />

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toEndOf="@id/button3"
android:text="Button 5" />

</RelativeLayout>

android:layout_above属性可以让一个控件位于另一个控件的上方,需要为这个属性指定相对控件id的引用,其他属性也是相似。当一个控件去引用另一个控件的id时,该控件一定要定义在被引用控件的后面,不然会出现找不到id的情况

效果如下:

image-20241016224713577

RelativeLayout中还有另外一组相对于控件进行定位的属性,android:layout_alignStart表示让一个控件的左边缘和另一个控件的左边缘对齐,android:layout_alignEnd表示让一个控件的右边缘和另一个控件的右边缘对齐。此外,还有android:layout_alignTopandroid:layout_alignBottom,道理都是一样的。


FrameLayout

FrameLayout,帧布局,它相比于前面两种布局简单很多,因此它的应用场景少了很多。这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />

</FrameLayout>

image-20241016225647004

可以看到,文字和按钮都位于布局的左上角。由于Button是在TextView之后添加的,因此按钮压在了文字的上面。除了这种默认效果之外,还可以使用layout_gravity属性来指定控件在布局中的对齐方式,这和LinearLayout中的用法是相似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="This is TextView" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="Button" />

</FrameLayout>

TextView在FrameLayout中居左对齐,Button在FrameLayout中居右对齐:

image-20241016230650265

总体来讲,由于定位方式的欠缺,FrameLayout的应用场景相对偏少一些。


自定义控件

常用控件和布局的继承结构:

image-20241016230815706

可以看到,所有控件都是直接或间接继承自View所有布局都是直接或间接继承自ViewGroupView是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件。因此,我们使用的各种控件其实就是在View的基础上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器

当系统自带的控件并不能满足我们的需求时,可以利用上面的继承结构来创建自定义控件。新创建一个Empty Views Activity的UICustomViews项目。


引入布局

创建一个包含左右两个按钮用于返回、编辑操作的标题栏布局。一般我们的程序中可能有很多个Activity需要这样的标题栏,如果在每个Activity的布局中都编写一遍同样的标题栏代码,会导致代码冗余。可以通过引入布局的方式提取共同代码。

先去src/main/res/values/themes.xml,将默认的主题Theme.Material3.DayNight.NoActionBar改为Theme.AppCompat.Light.NoActionBar,否则后续的布局会有部分内容被主题的配置所覆盖:

1
2
3
4
5
6
7
8
9
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.UICustomViews" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>

<style name="Theme.UICustomViews" parent="Base.Theme.UICustomViews" />
</resources>

在res目录下,新建一个drawable-xxhdpi目录,将title_bg.pngback_bg.pngedit_bg.png(资源下载地址见
前言)(第一行代码——Android(第3版) (ituring.com.cn)界面的随书下载中的随书资源.zip下载后,就能得到相应的资源)放入,分别用于作为标题栏、返回按钮和编辑按钮的背景

在layout目录下,新建一个title.xml布局:

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
26
27
28
29
30
31
32
33
34
35
36
37
<?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="wrap_content"
android:background="@drawable/title_bg">

<Button
android:id="@+id/titleBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="Back"
android:textColor="#fff" />

<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp" />

<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff" />
</LinearLayout>

android:layout_margin这个属性,它可以指定控件在上下左右方向上的间距。当然也可以使用android:layout_marginStartandroid:layout_marginTop等属性来单独指定控件在某个方向上的间距。

然后,通过在activity_main.xml使用<include>标签引入布局,就能使用上该标题栏:

1
2
3
4
5
6
7
8
<?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">

<include layout="@layout/title" />

</LinearLayout>

创建自定义控件

如果布局中有一些控件要求能够响应事件,还是需要在每个Activity中为这些控件单独编写一次事件注册的代码。比如标题栏中的返回按钮,其实不管是在哪一个Activity中,这个按钮的功能都是相同的,即销毁当前Activity。而如果在每一个Activity中都需要重新注册一遍返回按钮的点击事件,也会导致代码冗余,这种情况最好是使用自定义控件的方式来解决。

创建TitileLayout继承LinearLayout,成为自定义的标题栏控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
/**
* 对标题栏布局进行动态加载。from方法能构建一个LayoutInflater对象,
* 然后调用inflate方法可以动态加载一个布局文件,第二个参数是给加载的布局文件添加一个父布局
*/
val binding = TitleBinding.inflate(LayoutInflater.from(context), this, true)
binding.titleBack.setOnClickListener {
// TitleLayout中接收的context参数实际上是一个Activity的实例
// Kotlin中的类型强制转换使用的关键字是as
val activity = context as Activity
activity.finish()
}
binding.titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
}
}
}

在TitleLayout的主构造函数中声明了ContextAttributeSet这两个参数,在布局中引入TitleLayout控件时就会调用这个构造函数。然后在init结构体中需要对标题栏布局进行动态加载,这就要借助LayoutInflater来实现。

现在自定义控件已经创建好了,接下来我们需要在布局文件中添加这个自定义控件,修改activity_main.xml中的代码:

1
2
3
4
5
6
7
8
9
10
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<!-- 添加自定义控件 -->
<com.hunter.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>

重新运行程序,你会发现此时的效果和使用引入布局方式的效果是一样的。


最常用且最难用的控件:ListView

由于手机屏幕空间比较有限,能够一次性在屏幕上显示的内容并不多,当程序中有大量的数据需要展示的时候,就可以借助ListView来实现。ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据会滚动出屏幕


ListView的简单用法

新建一个名为ListViewTest的Empty Views Activity项目,修改activity_main.xml,添加ListView控件:

1
2
3
4
5
6
7
8
9
10
11
<?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>

再修改MainActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainActivity : AppCompatActivity() {
private val data = listOf(
"Apple", "Banana", "Orange", "Watermelon", "Pear",
"Grape", "Pineapple", "Strawberry", "Cherry", "Mango",
"Apple", "Banana", "Orange", "Watermelon", "Pear",
"Grape", "Pineapple", "Strawberry", "Cherry", "Mango"
)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// ArrayAdapter 将集合数据通过适配器传递给ListView
// 将simple_list_item_1作为ListView子项布局的id,这是一个内置的布局文件,里面只有一个TextView,可以简单地显示一段文本
val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, data)
binding.listView.adapter = adapter
}
}

即可通过滚动的方式查看屏幕外的数据。


定制ListView界面


Jetpack Compose

Jetpack Compose 是一种声明式 UI 工具包,与传统的基于 XML 的布局系统不同,它允许开发者使用 Kotlin 代码直接构建 UI


Android UI Toolkit

在代码中,布局由ViewGroup对象表示,它是控制子节点在屏幕上的位置和行为的容器。小部件由View对象表示,它显示单个UI组件,比如按钮和文本框

image-20240822214736405

View 表示 UI 组件的基本构建块。它在屏幕上占据一个矩形区域,在其中绘制特定的 UI 组件,如按钮或文本字段。**View也支持交互和事件专用视图通常会公开一组用于管理交互式事件的特定事件监听器**。


Composable 函数

使用XML构建UI的问题:

  1. UI不具备可扩展性
  2. 难以创建自定义视图
  3. 状态的所有权通常分散在多个所有者之间。

所有这些问题的根本原因在于 Android View 构建其状态和绘制自身及其子类的方式。为了避免这些问题,你需要重新开始,使用不同的基本构建块。在 Jetpack Compose 中,这个构建块被称为composable 函数

具有@Composable注解的函数,几乎就是创建新的小部件所需的全部内容。在Compose的世界里,这些小部件称为Composables

1
2
3
4
5
6
7
8
9
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
// 渲染一个文本视图
Text(
text = "Hello $name!",
// modifier 参数是可选的,用于自定义视图的外观和行为。
modifier = modifier
)
}

注解类通过向代码附加元数据来简化代码。通过使用注解,可以向类添加行为,并生成有用的代码,无需编写大量样板文件。

在Compose中,调用在屏幕上显示某些内容的函数称为发出UI。要发出消息,就需要调用Text函数。Text也是一个Composable函数,它是构成 Jetpack Compose 的默认Composable函数之一。需要注意的一点是,Composable函数只能从其他Composable函数中调用——如果你尝试删除@Composable,你会收到一个错误,阻止你使用 Text()

image-20240822222312278

Composable注解类有三个自己的注解:

1
2
3
4
5
6
7
8
9
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE,
AnnotationTarget.TYPE_PARAMETER,
AnnotationTarget.PROPERTY_GETTER
)
annotation class Composable
  • @MustBeDocumented:表示注解是公共API的一部分,应该包含在生成的文档中。
  • Retention:告诉编译器注解应该存在多长事件。使用AnnotationRetention.BINARY,处理器将在编译过程中,将代码存储在二进制文件中。
  • @Target:描述应用类型的上下文。@Composable可以应用于类型、参数、函数和属性。

显示Composables

可以使用Activity或Fragment作为起点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting("World")
}
}
}

@Composable
fun Greeting(name: String) {
Text(
text = "Hello $name!"
)
}

可以使用**content块**将composableactivity连接。无需使用XML文件定义布局内容,只需要调用composable函数即可。

1
2
3
4
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) { ... }

可以看到,setContent()ComponentActivity扩展函数扩展函数能在不更改源代码的情况下向类添加其他功能。这意味着可以在任何ComponentActivity及其子类上使用setContent()

调用setContent()会将名为content的composable 函数设置为根视图,可以看作一个容器,可以在其中添加任意数量的元素,在此容器内调用其余的composable函数。


基本的Composable函数

Text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package androidx.compose.material

@Composable
fun Text(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
)

例如创建一个如下的Composable函数:

1
2
3
4
5
6
7
8
9
10
@Composable
fun MyText() {
Text(
text = stringResource(id = R.string.jetpack_compose), // jetpack_compose是app/src/main/res/values/strings.xml中定义的字符串
fontStyle = FontStyle.Italic,
color = colorResource(id = R.color.colorPrimary), // colorPrimary是app/src/main/res/values/colors.xml中定义的颜色
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}

预览

在Composable函数上使用@Preview注解即可,不通过运行应用程序就能看到修改效果。

image-20240823161953571

使用预览,Composable函数需要满足如下任一条件:

  1. 没有参数。
  2. 所有参数都有默认参数。
  3. 提供一个@PreviewParameter以及一个特殊工厂,提供想要在UI上绘制的参数。

TextField

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
package androidx.compose.material

@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape =
MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
colors: TextFieldColors = TextFieldDefaults.textFieldColors()
)

TextField是用于输入字段的组件。TextField允许用户输入文本,并且如果TextField重新组合,输入的文本也不能消失或变更。要使TextField正常工作,必须提供一个重构期间不会更改的值,即状态值使用mutableStateOf()函数,可以将一个空String包装到一个状态容器中,将使用它来存储和显示输入字段中的文本

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun MyTextField() {
val textValue = remember {
mutableStateOf("")
}
TextField(
// 显示TextField内的当前文本
value = textValue.value,
// 每次输入新内容时,触发的回调。提供新的 TextFieldValue,以便更新显示的文本。
onValueChange = { textValue.value = it },
// 标签:容器内显示的标签。当用户输入文本时,标签将在书写光标上方动态显示。
label = {}
)
}
  • remember()能在改变状态时,保留设置的内容。如果不使用remember(),在改变状态(每次用户点击键盘上的键,内部状态都会发生变化)时,会丢失设置,并使用默认值(空字符串)。
image-20240825173101612 image-20240825173029465
使用 OutlinedTextField 添加Email字段

OutlinedTextField 是一个样式化的 TextField,它使用一个特殊的内部函数来绘制和动画化字段周围的边框和描述文本。

要添加提示或 Compose 中已知的标签,使用 label 属性并传入另一个composable函数。

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
26
27
28
29
30
@Composable
@Preview(showBackground = true)
fun MyTextField() {
val textValue = remember {
mutableStateOf("")
}

val primaryColor = colorResource(id = R.color.colorPrimary)

OutlinedTextField(
// 显示TextField内的当前文本
value = textValue.value,
// 每次输入新内容时,触发的回调。提供新的 TextFieldValue,以便更新显示的文本。
onValueChange = { textValue.value = it },
// 标签:容器内显示的标签。当用户输入文本时,标签将在书写光标上方动态显示。
label = {
// 通过Text函数向用户提供输入数据的提示
Text(
text = stringResource(id = R.string.email)
)
},
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = primaryColor, // 边框颜色
focusedLabelColor = primaryColor, // 处于聚焦状态的标签颜色
cursorColor = primaryColor // 光标颜色
),
// 显示一个适合输入电子邮件地址的虚拟键盘,通常会包含一个方便输入“@”符号和“.”符号的布局
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Email)
)
}

效果:

image-20240825172421221

Button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package androidx.compose.material

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun MyButton() {
Button(
// 空的lambda表达式,点击按钮时不执行操作,但会保持onClick的启用状态
onClick = {},
// 设置按钮背景色
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.colorPrimary)),
// 设置按钮边框,每个BorderStroke都必须定义一个宽度和颜色
border = BorderStroke(1.dp, color = colorResource(id = R.color.colorPrimaryDark))
) {
Text(
text = stringResource(id = R.string.button_text),
color = Color.White
)
}
}

RadioButton

目前,Jetpack Compose 没有单选组的实现,必须自己创建自定义组。

1
2
3
4
5
6
7
8
9
@Composable
fun RadioButton(
selected: Boolean,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: RadioButtonColors = RadioButtonDefaults.colors()
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Composable
fun MyRadioGroup() {
val radioButtons = listOf(0, 1, 2) // 单选按钮的索引
val selectedButton = remember { // remember 用于在组合过程中记住状态,确保在重组时保持状态不变。
mutableStateOf(radioButtons.first()) // mutableStateOf 用于创建一个可变的状态对象,默认选择第一个按钮
}
Column { // 一个垂直布局容器
radioButtons.forEach { index -> // 遍历 radioButtons 列表中的每个索引,执行如下操作
val isSelected = index == selectedButton.value
// 设置不同状态的颜色
val colors = RadioButtonDefaults.colors(
selectedColor = colorResource(id = R.color.colorPrimary),
unselectedColor = colorResource(id = R.color.colorPrimaryDark),
disabledColor = Color.LightGray
)
RadioButton( // 向Column添加一个按钮
selected = isSelected,
// 当按钮被点击时,更新 selectedButton 的值为当前按钮的索引,表示用户选择了这个按钮。
onClick = { selectedButton.value = index },
colors = colors
)
}
}
}

FloatingActionButton

浮动操作按钮之所以被称为“浮动”,是因为它们具有更高的高度,使其位于所有内容之上。它们被用来将应用程序的主要操作放在用户触手可及的地方。

1
2
3
4
5
6
7
8
9
10
11
12
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit
)
1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun MyFloatingActionButton() {
FloatingActionButton(
onClick = {}, // 空的lambda表达式,点击按钮时不执行操作,但会保持onClick的启用状态
backgroundColor = colorResource(id = R.color.colorPrimary),
contentColor = Color.White,
content = {
// contentDescription 用于无障碍支持(如屏幕阅读器),帮助视觉障碍用户理解按钮的功能。
Icon(Icons.Filled.Favorite, contentDescription = "Test FAB")
}
)
}

大多数情况下,需要对content添加Icon()

1
2
3
4
5
6
7
8
@Composable
@NonRestartableComposable
fun Icon(
imageVector: ImageVector,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)

Icon的主要特点是允许设置ImageVector类型的向量用作图标。


其他Button
  • IconButton:类似FloatingActionButton,但它没有浮动。通常用于导航。
  • OutlinedButton:与OutlinedTextField类似,提供额外的功能,如边框。
  • IconToggleButton:具有两种图标状态,可以打开和关闭它们。
  • TextButton:最常见于卡片和对话框中,可以执行不太明显的操作。

Progress Bars

当执行长时间的操作(例如从服务器或数据库获取数据)时,最好显示进度条。进度条通过显示动画来减少等待时间过长的感觉,并让用户感觉到正在发生某些事情。


CircularProgressIndicator
1
2
3
4
5
6
7
8
@Composable
fun CircularProgressIndicator(
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth
)

如果您不设置进度progress,进度条将运行无限旋转动画。


LinearProgressIndicator
1
2
3
4
5
6
7
8
@Composable
fun LinearProgressIndicator(
/*@FloatRange(from = 0.0, to = 1.0)*/
progress: Float,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.primary,
backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity)
)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun ProgressIndicatorScreen() {

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator( // 旋转动画进度条
color = colorResource(id = R.color.colorPrimary),
strokeWidth = 5.dp
)
LinearProgressIndicator(progress = 0.5f) // 线性进度条,进度设置为0.5
}

BackButtonHandler {
JetFundamentalsRouter.navigateTo(Screen.Navigation)
}
}

AlertDialog

对话框用于提醒用户有关操作的信息,或请求确认。例如,您可以使用对话框来确认用户是否要删除某个项目、请求他们对应用程序进行评分等。使用对话框最重要的部分是处理确定何时显示或关闭该对话框的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun AlertDialog(
onDismissRequest: () -> Unit,
confirmButton: @Composable () -> Unit,
modifier: Modifier = Modifier,
dismissButton: @Composable (() -> Unit)? = null,
title: @Composable (() -> Unit)? = null,
text: @Composable (() -> Unit)? = null,
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
properties: DialogProperties = DialogProperties()
)
  • onDismissRequest:回调函数,当对话框被请求关闭时执行(点击对话框外部、按下返回键
  • confirmButton:确认操作
  • dismissButton:关闭操作

Layout 布局

Linear Layouts

LinearLayout 的特征是将其子项定位在线性流中。此流称为方向,可以是水平或垂直的。在 Jetpack Compose 中,有两个不同的可组合函数可以替换 LinearLayout,每个方向一个。

Row 行
1
2
3
4
5
6
7
@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
  • horizontal Arrangement包括:
    • SpaceBetween:为每个子对象放置相等的间距,不计算第一个子对象之前或最后一个子对象之后的间距
    • SpaceEvenly:为每个子对象放置相等的间距,包括起始和结束间距
    • SpaceAround:和SpaceEvenly一样放置子项,但连续子项之间的间距减少一半
    • CenterStartEnd:将子项置于中心、开头、末尾。子项之间没有空格
  • vertical Alignment包括:
    • Top:将子项与父项的顶部对齐。
    • CenterVertically:将子项垂直对齐父项的中心。
    • Bottom:将子项与父项的底部对齐。

在 Row 中定位子项的最后一种方法是使用权重。要添加权重,需要使用特殊方式从 Compose 访问 weight() 修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
fun MyRow() {
Row(
verticalAlignment = Alignment.CenterVertically, // 垂直居中
horizontalArrangement = Arrangement.SpaceEvenly, // 水平等距离排列
modifier = Modifier.fillMaxSize() // 布局填满屏幕。如果不填充屏幕大小,排列和对齐就无关紧要,所有项目都会一个接一个地放在屏幕的左上角。
) {
THREE_ELEMENT_LIST.forEach { textResId ->
Text(
text = stringResource(id = textResId),
fontSize = 18.sp
)
}
}
}

Column 列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
fun MyColumn() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxSize()
) {
THREE_ELEMENT_LIST.forEach { textResId ->
Text(
text = stringResource(id = textResId),
fontSize = 22.sp
)
}
}
}

Box

Box用于相对于父组件的边缘显示子组件,并允许叠放子组件。当需要在特定位置显示元素或希望显示重叠元素(如对话框)时,非常有用。

1
2
3
4
5
6
7
@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
)
  • contentAlignment:用于修改子组件的外观和行为。如果想在每个子项之间有不同的Alignment,需要在子项上使用Modifier.align()设置Alignment。
    • TopStart
    • TopCenter
    • TopEnd
    • CenterStart
    • Center
    • CenterEnd
    • BottomStart
    • BottomCenter
    • BottomEnd
  • propagateMinConstraints:是否传递最小约束并将其用于内容。
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
@Composable
fun MyBox(
modifier: Modifier = Modifier, // 用于修改组件的外观和行为
contentModifier: Modifier = Modifier // 用于修改子组件的外观和行为
) {
Box(modifier = modifier.fillMaxSize()) {
Text(
text = stringResource(id = R.string.first),
fontSize = 22.sp,
modifier = contentModifier.align(Alignment.TopStart)
)

Text(
text = stringResource(id = R.string.second),
fontSize = 22.sp,
modifier = contentModifier.align(Alignment.Center)
)

Text(
text = stringResource(id = R.string.third),
fontSize = 22.sp,
modifier = contentModifier.align(Alignment.BottomEnd)
)
}
}

当 Box 中有多个子项时,它们的渲染顺序与你将它们放置在 Box 中的顺序相同


Surface

Surface 的独特之处在于它一次只能容纳一个子对象,但它为子对象的内容提供了许多样式处理,例如高度、边框等。

1
2
3
4
5
6
7
8
9
10
@Composable
fun Surface(
modifier: Modifier = Modifier,
shape: Shape = RectangleShape, // 定义Surface的形状及其阴影。
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
border: BorderStroke? = null,
elevation: Dp = 0.dp, // 设置标高并绘制适当的阴影。
content: @Composable () -> Unit
)

使用 Surface 的最常见方式是作为组件的根布局。由于它只能容纳一个 child,因此该 child 通常是定位其余元素的另一个布局。**Surface() 不处理定位,它的子项处理**。

1
2
3
4
5
6
7
8
9
10
11
12
@Composable
fun MySurface(modifier: Modifier) {
Surface(
modifier = modifier.size(100.dp), // 设置高宽
color = Color.LightGray,
contentColor = colorResource(id = R.color.colorPrimary),
elevation = 1.dp, // 设置标高,将 Surface 升高到其他元素之上
border = BorderStroke(1.dp, Color.Black)
) {
MyColumn() // 自定义的 Column
}
}

有一种名为 Card 的常用自定义 Surface 实现。一个Card有完全相同的五个用途,只能容纳一个子组件。Card 和 Surface 之间的唯一区别是其默认参数。Card 具有预定义的标高,并使用带圆角的 Material 主题形状。


Scaffold

可以使用Scaffold来实现一个可视化布局,该布局遵循标准 Material Design 结构。它结合了几种不同的组件来构建整个屏幕。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@Composable
fun MyScaffold() {
// 这种状态管理允许控制 Scaffold 的行为,例如打开或关闭抽屉(Drawer)或显示 SnackBar。
val scaffoldState: ScaffoldState = rememberScaffoldState()
// 该作用域可以用于启动协程,通常用于执行异步任务,例如在抽屉打开时执行某些操作
val scope: CoroutineScope = rememberCoroutineScope()

Scaffold(
scaffoldState = scaffoldState,
contentColor = colorResource(id = R.color.colorPrimary),
content = { paddingValues -> // paddingValues 参数的使用确保内容不会被应用程序栏遮挡
MyRow(modifier = Modifier.padding(paddingValues))
},
// 定义顶部应用栏
topBar = { MyTopAppBar(scaffoldState = scaffoldState, scope = scope) },
// 定义底部应用栏
bottomBar = { MyBottomAppBar() },
// 定义抽屉内容
drawerContent = { MyColumn() }
)
}

@Composable
fun MyTopAppBar(
scaffoldState: ScaffoldState, // 管理 Scaffold 的状态,包括抽屉的状态。
scope: CoroutineScope // 用于启动协程的作用域,允许在按钮点击时执行异步操作。
) {
val drawerState = scaffoldState.drawerState // 以便后续操作可以控制抽屉的打开与关闭状态。

TopAppBar( // 顶部应用栏,通常用于显示应用程序的标题和导航图标。
navigationIcon = { // 定义顶部应用栏的导航图标部分
IconButton( // 创建一个图标按钮,用户可以点击它来打开或关闭抽屉。
onClick = {
scope.launch { // 启动一个协程
if (drawerState.isClosed)
drawerState.open()
else
drawerState.close()
}
},
content = {
Icon( // 创建一个图标表示菜单
imageVector = Icons.Default.Menu,
contentDescription = stringResource(id = R.string.menu),
tint = Color.White
)
}
)
},
title = { // 顶部应用栏的标题
Text(
text = stringResource(id = R.string.app_name),
color = Color.White
)
},
backgroundColor = colorResource(id = R.color.colorPrimary)
)
}

@Composable
fun MyBottomAppBar() {
BottomAppBar(
content = {},
backgroundColor = colorResource(id = R.color.colorPrimary)
)
}
image-20240827195217341

构建Lists

Scrolling Modifier

在Jetpack Compose中,可以将Column支持滚动的modifier一起使用。

实现一个简单的滚动Column:

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
26
27
28
29
30
@Composable
fun MyScrollingScreen(modifier: Modifier = Modifier) {
Column(
// 根据滚动方向创建滚动状态,并保留滚动位置
modifier = modifier.verticalScroll(rememberScrollState())
) {
BookImage(
imageResId = R.drawable.advanced_architecture_android,
contentDescriptionResId = R.string.advanced_architecture_android
)
BookImage(
imageResId = R.drawable.kotlin_aprentice,
contentDescriptionResId = R.string.kotlin_apprentice
)
BookImage(
imageResId = R.drawable.kotlin_coroutines,
contentDescriptionResId = R.string.kotlin_coroutines
)
}
}

@Composable
fun BookImage(@DrawableRes imageResId: Int, @StringRes contentDescriptionResId: Int) {
Image(
bitmap = ImageBitmap.imageResource(imageResId),
contentDescription = stringResource(contentDescriptionResId),
contentScale = ContentScale.FillBounds,
modifier = Modifier.size(476.dp, 616.dp)
)
}
  • 垂直滚动:

    1
    2
    3
    4
    Column(
    // 根据滚动方向创建滚动状态,并保留滚动位置
    modifier = modifier.verticalScroll(rememberScrollState())
    )
  • 水平滚动:

    1
    2
    3
    4
    Row(
    // 根据滚动方向创建滚动状态,并保留滚动位置
    modifier = modifier.horizontalScroll(rememberScrollState())
    )

当拥有静态内容时,可滚动的列和行非常有用。但是,对于动态的数据收集,它们不是一个好主意。这是因为可滚动可组合项会急切地组合和渲染其中的所有元素,当您要显示大量元素时,这可能是一个繁重的操作


LazyColumn 和 LazyRow

当使用 LazyColumn 或 LazyRow 时,框架仅会组合能够在屏幕上显示的元素。当你滚动时,会组合新的元素,并且旧的元素会被销毁。当你向回滚动时,旧的元素会重新组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Composable
fun LazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
)

其中最重要的参数content,它表示列表中的内容。此内容属于 LazyListScope 类型,而不是通常的 Composable 类型。

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
26
27
@LazyScopeMarker
@JvmDefaultWithCompatibility
interface LazyListScope {
fun item(
key: Any? = null,
contentType: Any? = null,
content: @Composable LazyItemScope.() -> Unit
) {
error("The method is not implemented")
}

fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
contentType: (index: Int) -> Any? = { null },
itemContent: @Composable LazyItemScope.(index: Int) -> Unit
) {
error("The method is not implemented")
}

@ExperimentalFoundationApi
fun stickyHeader(
key: Any? = null,
contentType: Any? = null,
content: @Composable LazyItemScope.() -> Unit
)
}
  • item()允许向列表中添加新的组合项。每次都可以使用不同的组合项类型
  • items()允许设置一个 包含希望在每个列表项中使用的数据 的列表。一旦设置了数据,还需要提供一个 itemContent,它是一个用于显示列表中每个项的可组合项。
  • stickyHeader()允许设置一个头部可组合项,它会一直显示在列表的顶部,即使你向下滚动查看新项目时也是如此。

示例:

image-20240827201534145

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private val bookInfo = listOf(
BookCategory(
R.string.android,
listOf(
R.drawable.android_aprentice,
R.drawable.saving_data_android,
R.drawable.advanced_architecture_android
)
),
BookCategory(
R.string.kotlin,
listOf(
R.drawable.kotlin_coroutines,
R.drawable.kotlin_aprentice
)
)
)

@Composable
fun ListScreen() {
MyList()

BackButtonHandler {
JetFundamentalsRouter.navigateTo(Screen.Navigation)
}
}

@Composable
fun MyList() {
LazyColumn { // 垂直滚动的列表
items(bookInfo) {
// 将items中的每个对象转换为可组合的函数。
item ->
ListItem(bookCategory = item)
}
}
}

@Composable
fun ListItem(bookCategory: BookCategory, modifier: Modifier = Modifier) {
Column( // 垂直排列
modifier = Modifier.padding(8.dp) // 在边框附近添加8.dp空间
) {
Text(
text = stringResource(id = bookCategory.categoryResourceId), // 类别标题
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.colorPrimary)
)
Spacer(modifier = modifier.height(8.dp)) // 在类名和其余内容之间添加一些空格,在水平书籍列表的顶部显示类别名称。

LazyRow { // 水平滚动的列表,书籍图像
items(bookCategory.bookImageResources) { item ->
BookImage(item)
}
}
}
}

data class BookCategory(@StringRes val categoryResourceId: Int, val bookImageResources: List<Int>)

@Composable
fun BookImage(imageResource: Int) {
Image( // 显示图像
painter = painterResource(id = imageResource), // 通过id检索目标资源
contentDescription = stringResource(id = R.string.book_image),
modifier = Modifier.size(170.dp, 200.dp), // 显示的宽高
contentScale = ContentScale.Fit // 使图像适应指定的宽高
)
}

Compose中的Grid

如图,该网格包含10个元素,分为3列。最后一行的两个元素被标记为不可见,从而达到视觉上10个元素的效果。

image-20240828134043557
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private val filledItems = listOf(
Icons.Filled.Check,
Icons.Filled.Close,
Icons.Filled.ThumbUp,
Icons.Filled.Build,
Icons.Filled.Delete,
Icons.Filled.Home,
Icons.Filled.Close,
Icons.Filled.ThumbUp,
Icons.Filled.Build,
Icons.Filled.ThumbUp,
)

@Composable
fun GridScreen() {
LazyVerticalGrid( // 创建垂直网格的容器
columns = GridCells.Fixed(3), // 3列
modifier = Modifier.fillMaxSize(), // 网格占据屏幕中的所有可用空间
content = {
items(filledItems.size) { index ->
GridIcon(iconResource = filledItems[index]) // 将图标作为参数传入
}
}
)

BackButtonHandler {
JetFundamentalsRouter.navigateTo(Screen.Navigation)
}
}

@Composable
fun GridIcon(iconResource: ImageVector) {
Icon(
imageVector = iconResource,
contentDescription = stringResource(id = R.string.grid_icon),
modifier = Modifier
.size(80.dp)
.padding(20.dp),
tint = colorResource(id = R.color.colorPrimary) // 为图标指定颜色
)
}

搭建应用Jet Notes

自下而上的构建方法

使用 Jetpack Compose 构建应用时,明智的做法是从较小的可组合项开始,然后逐步构建到设计中。从最小的组件构建应用程序可以从一开始就解耦和复用代码。

image-20240828145326151


Note

先构建Note组件,方便后续复用。

image-20240828145440911

image-20240828163426316

image-20240828163358139

C:\Users\Hunter\AndroidStudioProjects\jet-materials\08-applying-material-design-to-compose\projects\starter

是这个笔记App最终的代码。


简介

Anaconda是一个免费开源的Python和R语言的发行版本,用于计算科学(数据科学、机器学习、大数据处理和预测分析),Anaconda致力于简化包管理和部署。Anaconda的包使用软件包管理系统Conda进行管理。

Anaconda3默认包含Python 3.7,但是用户可以创建虚拟环境来使用任意版本的Python包


常用命令

  1. 查看conda版本:conda --version
  2. 查看conda的环境配置:conda config --show
  3. 更新conda:conda update conda
  4. 更新Anaconda整体:conda update Anaconda

管理环境

Conda允许创建相互隔离的虚拟环境(Virtual Environment),这些环境各自包含属于自己的文件、包以及他们的依存关系,并且不会相互干扰。

  1. 创建虚拟环境

    conda create -n env_name python=3.8

    不指定python版本时,自动创建基于最新python版本的虚拟环境。

  2. 创建虚拟环境时,安装必要的包

    conda create -n env_name numpy matplotlib python=3.8

  3. 列举虚拟环境

    conda env list

    所显示的列表中,前面带星号*的表示当前活动环境。

  4. 激活虚拟环境

    conda activate env_name

  5. 退出虚拟环境

    conda deactivate,回到base

  6. 删除虚拟环境、虚拟环境中的包

    • conda remove --name env_name --all
    • conda remove --name env_name package_name
  7. 导出环境

    conda env export --name myenv > myenv.yml

  8. 还原环境

    conda env create -f myenv.yml


管理Package

  1. 查询包的安装情况。

    • conda list
    • conda list pkgname*
  2. 查询当前Anaconda repository中是否有想要安装的包

    conda search package_name

  3. 安装、更新、卸载包

    • conda install package_name
    • conda update package_name
    • conda uninstall package_name
  4. 清理缓存

    • conda clean -p 删除没有用的包 –packages
    • conda clean -t 删除tar打包 –tarballs
    • conda clean -y -all 删除所有的安装包及cache(索引缓存、锁定文件、未使用过的包和tar包)

conda install vs pip install

  1. conda可以管理非python包,pip只能管理python包。
  2. conda自己可以用来创建环境,pip不能,需要依赖virtualenv之类的。
    conda安装的包是编译好的二进制文件,安装包文件过程中会自动安装依赖包;pip安装的包是wheel或源码,装过程中不会去支持python语言之外的依赖项。
  3. conda安装的包会统一下载到一个目录文件中,当环境B需要下载的包,之前其他环境安装过,就只需要把之间下载的文件复制到环境B中,下载一次多次安装。pip是直接下载到对应环境中。
  4. conda只能在conda管理的环境中使用,例如比如conda所创建的虚环境中使用。pip可以在任何环境中使用,在conda创建的环境 中使用pip命令,需要先安装pip(conda install pip ),然后可以 环境A 中使用pip 。
  5. conda 安装的包,pip可以卸载,但不能卸载依赖包,pip安装的包,只能用pip卸载。

由于conda的库不如pip的库丰富,有时候可能迫不得已要使用pip安装。只有在conda install搞不定时才使用pip intall


Windows 通过WSL2 Ubuntu和Docker搭建深度学习环境

参考:


WSL2 Ubuntu安装Docker Engine

  1. 设置 Docker的 apt 仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # Add Docker's official GPG key:
    sudo apt-get update
    sudo apt-get install ca-certificates curl
    sudo install -m 0755 -d /etc/apt/keyrings
    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    sudo chmod a+r /etc/apt/keyrings/docker.asc

    # Add the repository to Apt sources:
    echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
  2. 下载安装Docker的安装包

    1
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  3. 查看docker版本,确认是否成功安装。

    1
    2
    ❯ docker -v
    Docker version 27.1.1, build 6312585

WSL2 下安装Anaconda的Docker镜像

参考Anaconda官网文档Docker — Anaconda documentation

1
2
3
4
5
6
7
8
9
# 拉取镜像
docker pull continuumio/miniconda3:24.5.0-0

# 启动容器
docker run -it --name=miniconda continuumio/miniconda3:24.5.0-0 bash

# 进入容器后,查看默认安装的Python版本
(base) root@8fead34bd870:/# python --version
Python 3.12.4

在miniconda3容器中安装cudatoolkit和cudnn

wsl2和windows11共用显卡驱动,因此我们只需安装cudatoolkit和cudnn。以后windows显卡驱动正常更新即可。

windows下nvidia-smi命令查看显卡驱动,以及支持的 CUDA 的最高版本,CUDA Version指的是可驱动的最高版本

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
26
27
28
29
30
31
32
33
34
35
36
 nvidia-smi
Fri Aug 9 23:04:30 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 531.79 Driver Version: 531.79 CUDA Version: 12.1 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 3060 Ti WDDM | 00000000:01:00.0 On | N/A |
| 0% 49C P8 10W / 225W| 885MiB / 8192MiB | 3% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 3008 C+G C:\Program Files\Typora\Typora.exe N/A |
| 0 N/A N/A 3372 C+G ...ekyb3d8bbwe\PhoneExperienceHost.exe N/A |
| 0 N/A N/A 7852 C+G ...nt.CBS_cw5n1h2txyewy\SearchHost.exe N/A |
| 0 N/A N/A 9944 C+G ...CBS_cw5n1h2txyewy\TextInputHost.exe N/A |
| 0 N/A N/A 11300 C+G ...les\Microsoft OneDrive\OneDrive.exe N/A |
| 0 N/A N/A 11628 C+G ...t.LockApp_cw5n1h2txyewy\LockApp.exe N/A |
| 0 N/A N/A 11632 C+G ...5n1h2txyewy\ShellExperienceHost.exe N/A |
| 0 N/A N/A 14872 C+G C:\Windows\explorer.exe N/A |
| 0 N/A N/A 15800 C+G ...2txyewy\StartMenuExperienceHost.exe N/A |
| 0 N/A N/A 16008 C+G ...siveControlPanel\SystemSettings.exe N/A |
| 0 N/A N/A 16976 C+G ...64__v826wp6bftszj\TranslucentTB.exe N/A |
| 0 N/A N/A 20136 C+G ...42.0_x64__8wekyb3d8bbwe\GameBar.exe N/A |
| 0 N/A N/A 21440 C+G ...ram Files (x86)\Anycast\Anycast.exe N/A |
| 0 N/A N/A 21848 C+G ...crosoft\Edge\Application\msedge.exe N/A |
| 0 N/A N/A 22204 C+G ...tionsPlus\logioptionsplus_agent.exe N/A |
| 0 N/A N/A 27108 C+G ...voice\logioptionsplus_logivoice.exe N/A |
+---------------------------------------------------------------------------------------+

根据可驱动的最高版本直接去官网下载对应版本https://developer.nvidia.com/cuda-toolkit-archive

image-20240809232152015

执行官网提供的安装命令:

1
2
3
4
wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.0-1_all.deb
sudo dpkg -i cuda-keyring_1.0-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda

创建环境,安装pytorch

1
2
3
conda create -n pytorch python=3.10

conda active pytorch

Start Locally | PyTorch

根据官网确定环境配置,得到相应的安装命令:

1
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia

plan2

基于 WSL2 和 Docker 的深度学习环境指北 - duanyll

安装Docker Engine

安装 NVIDIA Container Toolkit

安装带 CUDA 的 PyTorch 镜像

pytorch/pytorch:2.4.0-cuda12.1-cudnn9-devel

若想要使用pycharm连接docker容器,容器端口号必须指定为22,因为SFTP默认使用22端口。

docker run -it –gpus all -p 8077:22 pytorch/pytorch:2.4.0-cuda12.1-cudnn9-devel

进入容器,用which python查看,为/opt/conda/bin/python,可知默认使用conda来管理环境和package。

容器中安装SSH

sudo apt purge openssh-server # wsl2 自带的好像 sshd 不完整,先删除掉


apt update

apt -y upgrade

apt install -y vim openssh-server

service ssh start

service ssh status

设置root密码和配置文件

passwd root

vim /etc/ssh/sshd_config

添加如下内容

1
2
3
4
5
6
#启用公钥私钥配对认证方式
PubkeyAuthentication yes
#公钥文件路径(和上面生成的文件同)
AuthorizedKeysFile .ssh/authorized_keys
#root能使用ssh登录
PermitRootLogin yes

重启ssh

service ssh restart

【详细教程】pycharm使用docker容器开发_pycharm docker-CSDN博客


不知道为什么连不上,只能正常连接子系统和docker。

修复 WSL2 镜像网络模式下无法连接 Docker 的问题 - sulinehk blog - 专注于计算机科学与软件工程的技术博客

可能是添加了这个配置才好的。


直接运行项目中的jupyter notebook内容,会报错:

Running as root is not recommended. Use --allow-root to bypass.

在容器中执行:

1
2
3
4
5
6
7
jupyter server --generate-config

# 自动生成默认配置
Writing default config to: /root/.jupyter/jupyter_server_config.py

# 在如下文件中添加 c.ServerApp.allow_root = True
vim /root/.jupyter/jupyter_server_config.py

安装matplotlib

Matplotlib 是 Python 的绘图库。

conda install -y matplotlib


Matplotlib Pyplot

Pyplot 是 Matplotlib 的子库,提供了和 MATLAB 类似的绘图 API。

Pyplot 是常用的绘图模块,能很方便让用户绘制 2D 图表。

Pyplot 包含一系列绘图函数的相关函数,每个函数会对当前的图像进行一些修改,例如:给图像加上标记,生新的图像,在图像中产生新的绘图区域等等。

使用的时候,我们可以使用 import 导入 pyplot 库,并设置一个别名plt:

1
import matplotlib.pyplot as plt

一些常用的 pyplot 函数:

  • plot():用于绘制线图和散点图
  • scatter():用于绘制散点图
  • bar():用于绘制垂直条形图和水平条形图
  • hist():用于绘制直方图
  • pie():用于绘制饼图
  • imshow():用于绘制图像
  • subplots():用于创建子图

概述

SpringSecurity是一个基于Spring开发的非常强大的权限验证框架,其核心功能包括:

  • 认证 (用户登录)
  • 授权 (此用户能够做哪些事情)
  • 攻击防护 (防止伪造身份攻击)
阅读全文 »

MVC理论基础

MVC架构

MVC架构

三层架构

img
  • Model包括数据访问层业务层
  • View属于表示层
  • Controllert通常被视为表示层的一部分,但也可能包含一些轻量级的业务逻辑

三层架构中,最关键的是表示层

  • 它直接与用户交互,所有的请求都经过表示层解析,再告知业务层处理。
  • 所有页面的返回和数据的填充也靠表示层来完成。

SpringMVC就是一个优秀的表示层框架,将业务逻辑和表示逻辑解耦,更加精细地划分对应的职责,最后将View和Model进行渲染,得到最终的页面并返回给前端。

阅读全文 »

ACID原则

事务机制遵循ACID原则:

  • Atomicity 原子性:事务是一个原子操作,由一系列操作组成。事务的原子性确保所有操作要么完成,要么完全不起作用(完整性)
  • Consistency 一致性:事务执行前后,系统必须确保它所建模的业务处于一致的状态。例如转账,无论事务是否成功,转账者和收款人的总额应该不变。
  • Isolation 隔离性:并发操作相同的数据时,各事务之间相互独立。(但难免会存在冲突
  • Durability 持久性:一旦事务完成,它对数据的改变是持久的,即使数据库发生故障也不影响持久性。

只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障(A、I、D是手段,C是目的)

阅读全文 »

现象

发起HTTP请求调用,控制台报错:

org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation

​ at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:322) ~[spring-webmvc-6.1.10.jar:6.1.10]

阅读全文 »
0%