在之前的 Codelab 中,您已了解到 Material 是 Google 打造的一个设计系统,由各种准则、组件和工具构成,这些元素可为采用界面设计的最佳做法提供支持。在此 Codelab 中,您将更新在之前的 Codelab 中构建的小费计算器应用,以打造更精巧的用户体验,如下面的最终屏幕截图所示。您还将在其他一些场景中测试该应用,以确保用户体验尽可能流畅。
前提条件
- 熟悉常见界面微件,例如
TextView
、ImageView
、Button
、EditText
、RadioButton
、RadioGroup
和Switch
- 熟悉
ConstraintLayout
和如何通过设置约束定位子视图 - 擅长修改 XML 布局
- 了解位图图像和矢量可绘制对象的区别
- 会在主题中设置主题属性
- 能够在设备上启用深色主题
- 之前已为项目依赖项修改应用的
build.gradle
文件
学习内容
- 如何在应用中使用 Material Design 组件
- 如何通过从 Image Asset Studio 导入 Material 图标来在您的应用中使用这些图标
- 如何创建和应用新样式
- 如何设置颜色以外的其他主题属性
构建内容
- 一款遵循建议的界面最佳做法的精巧小费计算器应用
所需条件
- 一台安装了 Android Studio 的计算机。
- 完成之前 Codelab 后生成的 Tip Time 应用的代码
通过之前的 Codelab,您已完成 Tip Time 应用的构建。该应用是一款小费计算器应用,提供各种自定义小费的选项。您的应用界面当前如以下屏幕截图所示。应用功能可正常运行,但整个应用看起来更像是一个原型。从视觉上看,各个字段排列并不整齐。在提高样式和间距一致性,以及使用 Material Design 组件方面,肯定还有改进的空间。
Material 组件是常见的界面微件,可让您更轻松地在应用中实施 Material 样式。本文档提供了有关如何使用和自定义 Material Design 组件的信息。每个组件都有通用的 Material Design 准则,对于 Android 上可用的组件有 Android 平台特定的指南。如果所选平台中不存在某个组件,加标签的图表可为您提供足够的信息来重新创建相应组件。
通过使用 Material 组件,您的应用将与用户设备上的其他应用一起以更一致的方式运行。这样一来,在一个应用中学到的界面模式就可以沿用到下一个应用中。因此,用户将能更快地学会如何使用您的应用。建议尽可能使用 Material 组件(而不是非 Material 微件)。您将在下一个任务中了解到,Material 组件更灵活,可自定义程度更高。
Material Design 组件 (MDC) 库需要作为依赖项添加到项目中。如果您使用的是 Android Studio 4.1 或更高版本,那么在默认情况下,您的项目中应该已经包含此代码行。在应用的 build.gradle
文件中,确保此依赖项包含在该库的最新版本中。如需了解更多详情,请参阅 Material 网站上的使用入门页面。
app/build.gradle
dependencies {
...
implementation 'com.google.android.material:material:<version>'
}
文本字段
在小费计算器应用布局的顶部,您当前设置了 EditText
字段,用于表示服务费用。此 EditText
字段可用,但它不符合 Material Design 最新准则中关于文本字段外观和行为的规定。
如需使用任何新组件,请先在 Material 网站上了解相应组件。根据文本字段指南可知,文本字段分为以下两种类型:
实心文本字段
框状文本字段
如需创建上文所示的文本字段,请使用 TextInputLayout
及 MDC 库中附带的 TextInputEditText
。您可以将 Material 文本字段轻松自定义为:
- 显示输入文本或始终可见的标签
- 在文本字段中显示图标
- 显示帮助程序或错误消息
在此 Codelab 的第一个任务中,您将使用 Material 文本字段(由 TextInputLayout
和 TextInputEditText
组成)替换服务费用 EditText
。
- 在 Android Studio 中打开 Tip Time 应用后,转到
activity_main.xml
布局文件。该文件应包含使用小费计算器布局的ConstraintLayout
。 - 如需查看 Material 文本字段的 XML 样式示例,请返回文本字段的 Android 指南。您应该会看到如下所示的代码段:
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
- 看到此示例后,请插入一个 Material 文本字段,作为
ConstraintLayout
的第一个子级(在EditText
字段之前)。您将在后续步骤中移除EditText
字段。
您可以将此代码段输入 Android Studio,并使用自动补全功能简化输入。或者,您也可以从文档页面复制示例 XML,并将其粘贴到您的布局中(如下所示)。请注意 TextInputLayout
如何呈现子视图,即 TextInputEditText
。请记住,省略号 (...) 表示省略了一部分代码段,以便您可以重点关注实际更改的 XML 代码行。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/cost_of_service" ... />
...
您会在 TextInputLayout
元素上看到错误。您尚未在父级 ConstraintLayout
中为此视图妥善设置约束。此外,系统也没有识别字符串资源。您将在后续步骤中修复这些错误。
- 在文本字段中添加垂直和水平约束条件,以便在父级
ConstraintLayout
内合理确定文本字段的位置。由于您尚未删除EditText
,请从EditText
中剪切以下属性并将其粘贴置于TextInputLayout
中:约束条件、资源 IDcost_of_service
、布局宽度 (160dp
)、wrap_content
的布局高度以及提示文本@string/cost_of_service
。
...
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:hint="@string/cost_of_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
...
您可能会看到一个错误,提示 cost_of_service
ID 与 EditText
的资源 ID 相同,但您目前可以忽略此错误。(完成几个步骤之后您将移除 EditText
)。
- 接下来,确保
TextInputEditText
元素具有所有合适的属性。将输入类型从EditText
剪切并粘贴到TextInputEditText.
中。将TextInputEditText
元素资源 ID 更改为cost_of_service_edit_text.
。
<com.google.android.material.textfield.TextInputLayout ... >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/cost_of_service_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
match_parent
的宽度和 wrap_content
高度均按原样即可。设置 match_parent
的宽度时,TextInputEditText
的宽度将与父级 TextInputLayout
的宽度相同,均为 160dp
。
- 现在您已从
EditText
中复制了所有相关信息,请继续操作并从布局中删除EditText
。 - 在布局的 Design 视图中,您应该会看到此预览。“Cost of Service”字段现在看起来与 Material 文本字段类似。
- 您还不能运行该应用,因为
calculateTip()
方法的MainActivity.kt
文件中存在错误。回想之前某一 Codelab 的内容可知,如果项目已启用视图绑定,Android 会根据资源 ID 名称在绑定对象中创建属性。我们从中检索服务费用的字段在 XML 布局中已更改,因此需要相应地更新 Kotlin 代码。
您将从资源 ID 为 cost_of_service_edit_text
的 TextInputEditText
元素中检索用户输入。在 MainActivity
中,使用 binding.costOfServiceEditText
访问存储在其中的文本字符串。calculateTip()
方法的其余部分可以保持不变。
private fun calculateTip() {
// Get the decimal value from the cost of service text field
val stringInTextField = binding.costOfServiceEditText.text.toString()
val cost = stringInTextField.toDoubleOrNull()
...
}
- 太棒了!现在,请运行该应用并测试它是否仍然可以正常工作。请注意,当前在您输入时,“Cost of Service”标签将以何种方式显示在您输入的内容上方。小费应仍会按预期计算。
开关
在 Material Design 准则中,还有关于开关的指南。开关是一个微件,您可以在其中开启或关闭设置。
- 查看 Material 开关的 Android 指南。您将了解
SwitchMaterial
微件(来自 MDC 库),它将为开关提供 Material 样式。如果您继续滚动浏览本指南,您将看到一些示例 XML。 - 如需使用
SwitchMaterial
,必须在布局中显式指定SwitchMaterial
,并使用完全限定路径名称。
在 activity_main.xml
布局中,将 XML 标记从 Switch
更改为 com.google.android.material.switchmaterial.SwitchMaterial.
。
...
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
android:layout_width="0dp"
android:layout_height="wrap_content" ... />
...
- 运行该应用以验证它是否仍然编译。应用不会发生任何明显的变化。但是,使用 MDC 库中的
SwitchMaterial
(而非 Android 平台中的Switch
)有一点好处,即当库的SwitchMaterial
实现更新时(例如,Material Design 准则发生更改),您将免费获得更新后的微件,且无需您进行任何更改。这有助于保障您的应用在未来继续可用。
至此,您已经看到了两个示例,了解了使用现成的 Material Design 组件可以给您的界面带来哪些好处,以及如何让您的应用更符合 Material 准则。请记住,您可以随时访问此网站,探索 Android 上提供的其他 Material Design 组件。
图标是一种符号,可通过视觉形式表示预期功能,帮助用户了解界面。图标设计通常从用户应该已体验过的现实世界中的物体汲取灵感。图标设计通常会将细节降至最低级别,只需确保用户熟悉即可。例如,在现实世界中,我们使用铅笔书写,因此铅笔图标通常表示创建、添加或修改某一项。
照片由 Angelina Litvin 拍摄,选自 Unsplash 网站 |
有时,图标会与现实世界中一些过时的物体相关联,例如软盘图标就是如此。此图标广泛用于表示保存文件或数据库记录;但是,虽然软盘在 20 世纪 70 年代十分流行,但在 2000 年以后并不常见。不过,如今这一图标仍在继续使用,这说明强大的视觉效果可以超越其物质形式的生命周期。
照片由 Vincent Botta 拍摄,选自 Unsplash 网站 |
在应用中表示图标
对于应用中的图标,建议使用矢量可绘制对象,而不是为不同的屏幕密度提供不同版本的位图图像。矢量可绘制对象以 XML 文件表示,其中存储如何创建图像的说明,而不是保存构成图像的实际像素。矢量可绘制对象可以放大或缩小,而不会损失视觉质量或增加文件大小。
提供的图标
Material Design 提供了大量图标,分成若干常见类别,可满足您的大多数需求。查看图标列表。
这些图标还可以使用五种主题(Filled、Outlined、Rounded、Two-Tone 和 Sharp)中的一种来绘制,并可以对图标进行色调调节。
Filled | Outlined | Rounded | Two-Tone | Sharp |
添加图标
在此任务中,您将向应用中添加三个矢量可绘制对象图标:
- “Cost of Service”文本字段费用旁边的图标
- 服务问题旁边的图标
- “round up tip”提示旁边的图标
以下是该应用的最终版本的屏幕截图。添加图标后,您可以调整布局以适应这些图标的放置位置。注意,添加图标后,字段和“CALCULATE”按钮会移到右侧。
添加矢量可绘制对象资源
您可以直接在 Android Studio 中的 Asset Studio 中将这些图标创建为矢量可绘制对象。
- 打开应用窗口左侧的 Resource Manager 标签页。
- 点击 + 图标,然后选择 Vector Asset。
- 对于 Asset Type,请确保选中加标签 Clip Art 的单选按钮。
- 点击 Clip Art: 旁边的按钮,选择其他剪贴画图片。在显示的提示中,在出现的窗口中输入“call made”。您将使用此箭头图标表示“round up tip”选项。选中该图标,然后点击 OK。
- 将图标重命名为
ic_round_up
。(建议您在命名图标文件时使用前缀 ic_)。您可以将 **Size** 保留为 24 dp x 24 dp,将 **Color** 设置为黑色 000000。 - 点击 Next。
- 接受默认目录位置,然后点击 Finish。
- 对其他两个图标重复步骤 2 - 7:
- 服务问题图标:搜索“room service”图标,并将其另存为
ic_service
。 - 服务费用图标:搜索“store”图标,并将其另存为
ic_store
。
- 完成上述操作后,Resource Manager 看起来将类似于以下屏幕截图。您还将在
res/drawable
文件夹中列出这三个矢量可绘制对象(ic_round_up
、ic_service
和ic_store
)。
支持较低版本的 Android
您刚刚在应用中添加了矢量可绘制对象,但请务必注意,在 Android 平台上,直到 Android 5.0(API 级别 21)才添加了对矢量可绘制对象的支持。
根据您的项目设置,Tip Time 应用的最低 SDK 版本是 API 19。这意味着,应用可以在搭载 Android 平台版本 19 或更高版本的 Android 设备上运行。
如需让您的应用在这些较低版本的 Android 上正常工作(称为向后兼容性),请将 vectorDrawables
元素添加到应用的 build.gradle
文件。这样,您就能够在低于 API 21 的平台版本上使用矢量可绘制对象,而无需在构建项目时将其转换为 PNG。如需了解更多详情,请参阅此处。
app/build.gradle
android {
defaultConfig {
...
vectorDrawables.useSupportLibrary = true
}
...
}
正确配置项目后,您现在可以开始将图标添加到布局中。
插入图标和位置元素
您将使用 ImageViews
在应用中显示图标。这是最终界面的显示方式。
- 打开
activity_main.xml
布局。 - 首先将商店图标置于“Cost of Service”文本字段旁边。在
TextInputLayout
之前,插入一个新的ImageView
作为ConstraintLayout
的第一个子级。
<androidx.constraintlayout.widget.ConstraintLayout
...>
<ImageView
android:layout_width=""
android:layout_height=""
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
- 在
ImageView
上设置适当的属性以存储ic_store
图标。将 ID 设置为icon_cost_of_service
。将app:srcCompat
属性设置为可绘制资源@drawable/ic_store
,然后您就会在 XML 的该行旁看到该图标的预览。此外,由于该图片仅用于装饰目的,因此还要设置android:importantForAccessibility="no"
。
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store" />
由于尚未约束视图,预计 ImageView
上会出现错误。您接下来要解决这个问题。
- 分两步确定
icon_cost_of_service
的位置。首先在ImageView
中添加约束条件(此步骤),然后更新它旁边的TextInputLayout
上的约束条件(第 5 步)。下图展示了应该如何设置约束条件。
在 ImageView
上,您希望将其起始边缘约束为父视图的起始边缘 (app:layout_constraintStart_toStartOf="parent"
)。
与图标旁边的文本字段相比,图标垂直居中显示,因此请将此 ImageView
(layout_constraintTop_toTopOf
) 的顶部约束为文本字段的顶部。将此 ImageView
(layout_constraintBottom_toBottomOf
) 的底部约束为文本字段的底部。如要引用文本字段,请使用资源 ID @id/cost_of_service
。默认行为是,当将两个约束条件应用于同一维度中的微件时(例如顶部约束条件和底部约束条件),系统将同等应用这两个约束条件。因此,相对于“Cost of Service”字段,图标呈垂直居中显示。
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/cost_of_service"
app:layout_constraintBottom_toBottomOf="@id/cost_of_service" />
在 Design 视图中,图标和文本字段仍重叠。下一步将修复此问题。
- 添加该图标之前,文本字段位于父级开头。现在,需将文本字段移到右侧。更新
cost_of_service
文本字段相对于icon_cost_of_service
的约束条件。
TextInputLayout
的起始边缘应约束为 ImageView
的结束边缘 (@id/icon_cost_of_service
)。如需在两个视图之间添加间距,请在 TextInputLayout
上添加 16dp
的起始外边距。
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_cost_of_service">
<com.google.android.material.textfield.TextInputEditText ... />
</com.google.android.material.textfield.TextInputLayout>
完成这些更改后,图标将正确置于文本字段旁。
- 接下来,在“How was the service?”
TextView
旁插入服务铃铛图标。虽然您可以在ConstraintLayout
中的任意位置声明ImageView
,但是如果在 XML 布局中的TextInputLayout
之后但在service_question
TextView
之前插入新的ImageView
,XML 布局将更易于阅读。
为新 ImageView
分配资源 ID @+id/icon_service_question
。为 ImageView
和服务问题 TextView
设置适当的约束条件。
同时为 service_question TextView
添加 16dp
的上外边距,使得服务问题与其上方“Cost of Service”文本字段之间留有更多的垂直空间。
...
<ImageView
android:id="@+id/icon_service_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/service_question"
app:layout_constraintBottom_toBottomOf="@id/service_question" />
<TextView
android:id="@+id/service_question"
...
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@id/cost_of_service"
app:layout_constraintTop_toBottomOf="@id/cost_of_service"/>
...
- 此时,Design 视图应如下所示。“Cost of Service”字段和服务问题(及各自的图标)看起来都很棒,但单选按钮现在看起来位置不合适。它们与上方内容没有垂直对齐。
- 将单选按钮移至右侧,放在服务问题下方,改进单选按钮的位置。这意味着更新
RadioGroup
约束条件。将RadioGroup
的起始边缘约束为service_question
TextView
的起始边缘。RadioGroup
上的所有其他属性可以保持不变。
...
<RadioGroup
android:id="@+id/tip_options"
...
app:layout_constraintStart_toStartOf="@id/service_question">
...
- 然后,继续将
ic_round_up
图标添加到“round up tip?”开关旁的布局中。请尝试自行执行此操作,如果遇到问题,请查阅下面的 XML。您可以为新的ImageView
分配资源 IDicon_round_up
。 - 在布局 XML 中,在
RadioGroup
之后但在SwitchMaterial
微件之前插入一个新的ImageView
。 - 为
ImageView
分配资源 IDicon_round_up
,并将srcCompat
设置为图标@drawable/ic_round_up
的可绘制对象。将ImageView
的起始位置约束为父级起始位置,并使图标相对于SwitchMaterial
垂直居中。 - 更新
SwitchMaterial
图标,使其位于图标旁边,且起始外边距为16dp
。为icon_round_up
和round_up_switch
生成的 XML 应如下所示。
...
<ImageView
android:id="@+id/icon_round_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_round_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/round_up_switch"
app:layout_constraintBottom_toBottomOf="@id/round_up_switch" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_round_up" />
...
- Design 视图应如下所示。三个图标均已正确放置。
- 如果将此视图与最终的应用屏幕截图进行比较,您会发现“CALCULATE”按钮也被移到了与“Cost of Service”字段、服务问题、单选按钮选项和“round up tip”问题垂直对齐的位置。通过将“CALCULATE”按钮的起始位置约束为
round_up_switch
的起始位置,即可实现此样式。此外,还应在“CALCULATE”按钮及其上面的开关之间添加8dp
的垂直外边距。
...
<Button
android:id="@+id/calculate_button"
...
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@id/round_up_switch" />
...
- 最后,很重要的一点是,通过为
TextView
添加8dp
的上外边距来确定tip_result
的位置。
...
<TextView
android:id="@+id/tip_result"
...
android:layout_marginTop="8dp" />
...
- 步骤虽然繁多,但逐步完成每一项工作即可取得很棒的效果。为了使元素在布局中正确对齐,需要非常注意细节,这样最终结果看起来才会更好!运行应用,其界面应如以下屏幕截图所示。垂直对齐元素并增加元素之间的间距,元素才不会挤在一起。
还没有大功告成!您可能已经注意到,服务问题和小费金额的字体大小及颜色与单选按钮和开关中的文本有所不同。让我们在下一个任务中使用样式和主题使它们保持一致。
样式是单一类型微件的视图属性值的集合。例如,TextView
样式可以指定字体颜色、字体大小和背景色等。通过将这些属性提取到样式中,您可以轻松地将样式应用于布局中的多个视图,并在一个位置对样式进行维护。
在此任务中,您首先需要为文本视图、单选按钮和开关微件创建样式。
创建样式
- 在 res > values 目录中创建一个名为
styles.xml
的新文件(如果尚不存在此文件)。如需进行创建,请右键点击 values 目录,然后依次选择 New > Values Resource File。将其命名为styles.xml
。新文件将包含以下内容。
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
- 创建新的
TextView
样式,使文本在整个应用中保持一致。在styles.xml
中定义一次样式,然后就可以将其应用于布局中的所有TextViews
。虽然可以从头开始定义样式,但您也可以从 MDC 库中的现有TextView
样式进行扩展。
设置组件样式时,您通常应从正在使用的微件类型的父样式进行扩展。这一点很重要,原因有两个:首先,它可确保在您的组件上设置所有重要的默认值;其次,您的样式将继续继承今后父样式的所有更改。
您可以随意命名您的样式,但建议采用以下惯例。如果继承自父 Material 样式,请以并行方式命名样式,将 MaterialComponents
替换为应用的名称 (TipTime
)。这会将您的更改移动到其自己的命名空间中,从而消除 Material 组件引入新样式时可能出现的冲突。示例:
您的样式名称:Widget.TipTime.TextView
继承自父样式:Widget.MaterialComponents.TextView
将其添加到 styles.xml
文件中 resources
起始标记和结束标记之间。
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
</style>
- 设置
TextView
样式,以便它替换以下属性:android:minHeight,android:gravity,
和android:textAppearance.
android:minHeight
将 TextView
的最小高度设置为 48dp。根据 Material Design 准则,任何行的最小高度都应为 48dp。
您可以通过设置 android:gravity
属性使文本在 TextView
中垂直居中。(如以下屏幕截图所示。)重力值用于控制视图内内容的位置。由于实际文本内容的高度不会占据完整的 48dp,因此值 center_vertical
会在 TextView
中将文本垂直居中(但不会更改其水平位置)。其他可能的重力值包括 center
、center_horizontal
、top
和 bottom
。您可以随意尝试其他重力值来查看对文本的影响。
将文本外观属性值设置为 ?attr/textAppearanceBody1
。TextAppearance 是围绕文本大小、字体和其他属性预制的一组样式。如需了解 Material 提供的其他可能的文本外观,请参阅此类型缩放列表。
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- 通过为
activity_main.xml
中的每个TextView
添加样式属性,将Widget.TipTime.TextView
样式应用于service_question
TextView
。
<TextView
android:id="@+id/service_question"
style="@style/Widget.TipTime.TextView"
... />
添加样式前,TextView
看起来如下所示,采用较小字号的灰色字体:
添加样式后,TextView
看起来如下所示。现在,此 TextView
看起来与布局的其余部分更加一致。
- 将相同的
Widget.TipTime.TextView
样式应用于tip_result
TextView
。
<TextView
android:id="@+id/tip_result"
style="@style/Widget.TipTime.TextView"
... />
- 此外,还应将相同的文本样式应用于开关中的文本标签。但是,您无法为
SwitchMaterial
微件设置TextView
样式。TextView
样式仅可应用于TextViews
。因此,请为开关创建一个新的样式。这些属性在minHeight
、gravity
和textAppearance
方面相同。此处唯一的区别在于样式名称和父样式,因为您现在继承的是 MDC 库中的Switch
样式。样式的名称还应反映父样式的名称。
您的样式名称:Widget.TipTime.CompoundButton.Switch
。继承自父样式:Widget.MaterialComponents.CompoundButton.Switch
<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
您还可以在此样式中指定特定于开关的其他属性,但在您的应用中,没有必要这样做。
- 单选按钮文本是您要确保文本在视觉保持上一致的最后一个位置。您不能对
RadioButton
微件应用TextView
样式或Switch
样式。相反,您必须为单选按钮创建新的样式。您可以从 MDC 库的RadioButton
样式进行扩展。
创建此样式时,请同时在单选按钮文本和圆圈视觉元素之间添加一些内边距。paddingStart
是您尚未使用的新属性。内边距是视图内容与视图边界之间的间距。paddingStart
属性仅在组件起始位置设置内边距。查看在单选按钮上设置 0dp 和 8dp paddingStart
的区别。
<style name="Widget.TipTime.CompoundButton.RadioButton"
parent="Widget.MaterialComponents.CompoundButton.RadioButton">
<item name="android:paddingStart">8dp</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- (可选)创建
dimens.xml
文件以提高常用值的可管理性。您可以采用与创建上述styles.xml
文件相同的方式创建文件。选择值目录,右键点击并依次选择 New > Values Resource File。
在这个小型应用中,您重复进行了两次最小高度设置。目前这完全是可管理的,但如果有 4 个、6 个、10 个或更多的组件共用一个值,这就会变得难以掌控。记住分别更改所有组件非常乏味且容易出错。您可以在 res > values 中再创建一个名为 dimens.xml
的实用资源文件,用于存储您可以命名的常见维度。通过将常用值标准化为命名维度,可以更轻松地管理应用。TipTime 很小,所以我们不会在这个可选步骤之外使用它。不过,对于您可能会与设计团队合作处理的生产环境中的更复杂应用,您可以借助 dimens.xml
更频繁地轻松更改这些值。
dimens.xml
<resources>
<dimen name="min_text_height">48dp</dimen>
</resources>
您将更新 styles.xml
文件以使用 @dimen/min_text_height
,而不是直接使用 48dp
。
...
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">@dimen/min_text_height</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
...
向您的主题添加这些样式
您可能已经注意到,您还未将新的 RadioButton
和 Switch
样式应用到相应的微件。原因在于,您将使用主题属性在应用主题中设置 radioButtonStyle
和 switchStyle
。我们再来回顾一下什么是主题。
主题是命名资源(称为主题属性)的集合,以后可在样式、布局等中加以引用。您可以为整个应用、activity 或视图层次结构指定主题,而不仅仅是针对单个 View.
指定主题。之前,您已通过设置主题属性(例如 colorPrimary
和 colorSecondary
)在 themes.xml
中修改应用的主题,更改后的主题将在整个应用及其组件中使用。
radioButtonStyle
和 switchStyle
是您可以设置的其他主题属性。您为这些主题属性提供的样式资源将应用到每个单选按钮以及相应主题所适用的视图层次结构中的每个开关。
此外,还有适用于 textInputStyle
的主题属性,其中指定的样式资源将应用于应用中的所有文本输入字段。如需使 TextInputLayout
看起来像一个框状文本字段(如 Material Design 准则中所示),可使用 MDC 库中定义为 Widget.MaterialComponents.TextInputLayout.OutlinedBox
的 OutlinedBox
样式。以下是您将使用的样式。
- 修改
themes.xml
文件,以使主题引用所需的样式。设置主题属性的方式与在之前的一个 Codelab 中声明colorPrimary
和colorSecondary
主题属性的方式相同。但此次,相关主题属性是textInputStyle
、radioButtonStyle
和switchStyle
。您将使用您之前为RadioButton
和Switch
创建的样式以及 MaterialOutlinedBox
文本字段的样式。
将以下内容复制到 res/values/themes.xml
内应用主题的样式标记中。
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
- 您的
res/values/themes.xml
文件看起来应如下所示。您可以根据需要在 XML 中添加注释(使用<!-
和-->
表示)。
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- Radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- Switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- 务必对 themes.xml (night) 中的深色主题进行同样的更改。您的
res/values-night/themes.xml
文件看起来应如下所示:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Application theme for dark theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- For radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- For switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- 运行应用并查看更改。文本字段的
OutlinedBox
样式看起来更加美观,并且所有文本现在看起来都是一致的!
即将完成应用修改时,您不仅应使用预期的工作流测试您的应用,还应在其他用户场景中进行测试。您可能会发现,对代码进行一些细微更改,就能极大地改善用户体验。
旋转设备
- 将您的设备旋转为横屏模式您可能需要先启用自动旋转设置。(该设置位于设备的快速设置或设置 > 显示 > 高级 > 自动旋转屏幕选项下)。
然后,在模拟器中,您可以使用模拟器选项(位于设备右上角)来将屏幕向右或向左旋转。
- 您会注意到,某些界面组件(包括 CALCULATE 按钮)会被截断。这样显然会阻止您使用该应用!
- 如需解决此错误,请在
ConstraintLayout
周围添加一个ScrollView
。您的 XML 看起来大致如下。
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:context=".MainActivity">
...
</ConstraintLayout>
</ScrollView>
- 再次运行并测试应用。将设备旋转为横屏模式时,您应该可以滚动界面来访问“CALCULATE”按钮并查看小费结果。此修复不仅适用于横屏模式,也适用于其他可能具有不同维度的 Android 设备。现在,无论设备屏幕尺寸如何,用户都可以滚动布局。
按下 Enter 键时隐藏键盘
您可能已经注意到,输入服务费用后,键盘始终保持显示状态。为了更好地访问“CALCULATE”按钮,每次都需要手动隐藏键盘,这样有点麻烦。相反,应使键盘在按 Enter 键时自动隐藏。
对于文本字段,您可以定义键监听器,以便在点按某些键时响应事件。键盘上每个可能的输入选项都包含一个与之关联的按键代码,包括 Enter
键。请注意,屏幕键盘也称为软键盘,与实体键盘不同。
在此任务中,在文本字段上设置键监听器,以在按下 Enter
键时监听。检测到该事件后,隐藏键盘。
- 复制此辅助方法并将其粘贴到
MainActivity
类中。您可以将其插入到MainActivity
类的右大括号前面。handleKeyEvent()
是一个私有辅助函数,用于在keyCode
输入参数等于KeyEvent.
KEYCODE_ENTER
时隐藏屏幕键盘。InputMethodManager 用于控制是显示还是隐藏软键盘,并且允许用户选择显示哪一个软键盘。如果键事件处理成功,此方法会返回 true,否则返回 false。
MainActivity.kt
private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Hide the keyboard
val inputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
return true
}
return false
}
- 现在,在
TextInputEditText
微件上附加一个键监听器。记住,您可以通过绑定对象binding.costOfServiceEditText.
访问TextInputEditText
微件。
在 costOfServiceEditText
上调用 setOnKeyListener()
方法并传入 OnKeyListener
。这类似于使用 binding.calculateButton.setOnClickListener { calculateTip() }.
在应用中为“CALCULATE”按钮设置点击监听器的方式。
在视图上设置键监听器的代码稍微复杂一些,但一般思路是,OnKeyListener
有一个 onKey()
方法,该方法在按下按键时触发。onKey()
方法需要 3 个输入参数:视图、按下的按键的代码以及按键事件(您不会使用该按键事件,因此可以将之命名为“_
”)。调用 onKey()
方法时,应调用 handleKeyEvent()
方法并传递视图参数和按键代码参数。编写此代码的语法为:view, keyCode, _ -> handleKeyEvent(view, keyCode).
。实际上,该语法称为 lambda 表达式,在后续单元中,您将详细了解 lambda。
添加相应代码,以在 activity 的 onCreate()
方法内的文本字段中设置键监听器。这是因为创建布局后,您需要在用户开始与 activity 交互之前附加键监听器。
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(binding.root)
binding.calculateButton.setOnClickListener { calculateTip() }
binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
}
}
- 测试新更改是否有效。运行应用并输入服务费用。按键盘上的 Enter 键,这样软键盘应该会隐藏。
在启用 TalkBack 的情况下测试您的应用
基于您在本课程中学到的内容,您希望构建可供尽可能多的用户访问的应用。有些用户可能会使用 TalkBack 访问您的应用并在其中导航。TalkBack 是 Android 设备随附的 Google 屏幕阅读器。TalkBack 会为您提供语音反馈,这样即使您不看屏幕也能轻松使用设备。
启用 TalkBack 后,请确保用户可以完成应用中计算小费的用例。
- 按照这些说明在您的设备上启用 TalkBack。
- 返回 Tip Time 应用。
- 按照这些说明,通过 TalkBack 探索您的应用。向右滑动可按顺序浏览屏幕元素,向左滑动即可按相反顺序浏览。点按任意位置两次即可选择。确认您可以通过滑动手势访问应用的所有元素。
- 确保 TalkBack 用户能够导航到屏幕上的每一项,输入服务费用,更改小费选项,计算小费,以及听到播报的小费。请注意,不会为图标提供语音反馈,因为您已将这些图标标记为
importantForAccessibility="no"
。
如需详细了解如何进一步减少您的应用使用障碍,请参阅这些原则。
(可选)调整矢量可绘制对象的色调
在这一可选任务中,您将根据主题主色对图标进行色调调节,以使图标在浅色主题下与深色主题下有所不同(如下所示)。此更改是对界面的完美补充,使得图标与应用主题更统一。
正如我们在前文提到的那样,VectorDrawables
与位图图像相比有一个优势:即可以对图标进行缩放和色调调节。下面是表示铃铛图标的 XML。需要注意两个不同的颜色属性:android:tint
和 android:fillColor
。
ic_service.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>
如果存在着色,它将覆盖可绘制对象的所有 fillColor
指令。在此例中,白色会被 colorControlNormal
主题属性覆盖。colorControlNormal
是微件正常状态(未选中/未激活状态)的颜色。目前是灰色。
我们可以对应用进行的一种视觉增强操作是根据应用主题主色对可绘制对象进行色调调节。对于浅色主题,该图标将会显示为 @color/green
;而在深色主题下,该图标将显示为 @color/green_light
,即 ?attr/colorPrimary
。根据应用主题主色对可绘制对象进行色调调节可以使布局中的元素显得更加统一和连贯。这也使我们不必复制浅色主题和深色主题的图标集。只有 1 组矢量可绘制对象,且色调将根据 colorPrimary
主题属性而变化。
- 更改
ic_service.xml
中android:tint
属性的值
android:tint="?attr/colorPrimary"
在 Android Studio 中,该图标现在将显示正确的色调。
colorPrimary
主题属性所指向的值因浅色与深色主题而异。
- 重复此操作,以更改其他矢量可绘制对象的色调。
ic_store.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
ic_round_up.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
- 运行应用。验证图标在浅色和深色主题中是否显示不同。
- 在最后的清理步骤中,请记得重新格式化应用中的所有 XML 和 Kotlin 代码文件。
恭喜!您已完成小费计算器应用的构建!您一定对您构建的内容十分自豪吧。希望这会为您奠定基础,助力您构建更美观、功能更强大的应用!
此 Codelab 的解决方案代码可在下面列出的 GitHub 代码库中找到。
如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。
获取代码
- 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
- 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个对话框。
- 在对话框中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
- 在计算机上找到该文件(可能在 Downloads 文件夹中)。
- 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
在 Android Studio 中打开项目
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Open an existing Android Studio project。
注意:如果 Android Studio 已经打开,请依次选择 File > New > Import Project 菜单选项。
- 在 Import Project 对话框中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
- 双击该项目文件夹。
- 等待 Android Studio 打开项目。
- 点击 Run 按钮 以构建并运行应用。请确保该应用可以正常使用。
- 在 Project 工具窗口中浏览项目文件,了解应用的实现方式。
- 尽可能使用 Material Design 组件,以便遵循 Material Design 准则并实现更多自定义功能。
- 添加图标以便用户以直观的方式了解应用的各个部分如何工作。
- 使用
ConstraintLayout
可以确定元素在布局中的位置。 - 测试应用在各种极端情况下的情形(例如在横屏模式下旋转应用),并根据需要进行改进。
- 对您的代码添加注释,帮助阅读代码的其他人了解您的方法。
- 重新格式化您的代码并清理代码,以使其尽可能简洁。
- 作为之前一些 Codelab 的延续,请使用您在此处学到的最佳做法(如使用 Material Design 组件)更新您的单位转换器烹饪应用,以更严格地遵循 Material 准则。