课本
书籍资源进入官网下载,PC端进入
第十二章-最佳的UI体验,MaterialDesign3实战
M3首页
其实长久以来,大多数人可能会认为Android系统的UI并不算美观,至少没有iOS系统的美观。以至于很多IT公司在进行应用界面设计的时候,为了保证双平台的统一性,强制要求Android端的界面风格必须和iOS端一致。这种情况在现实工作当中实在是太常见了,虽然我认为这是非常不合理的。因为对于一般用户来说,他们不太可能会在两个操作系统上分别使用同一个应用,但是必定会在同一个操作系统上使用不同的应用。因此,同一个操作系统中各个应用之间
的界面统一
性要远比
一个应用在双平台
的界面统一
性重要
得多。
但是Android标准的界面设计风格并不是特别被大众所接受,很多公司觉得自己可以设计出更加好看的界面,从而导致Android平台的界面风格长期难以得到统一。为了解决这个问题,Google也是使出了杀手锏,在2014年Google I/O大会上重磅推出了一套全新的界面设计语言——Material Design。
本章仅演示怎么使用button按钮和部分使用方法,例如怎么正确在程序中使用等还是需要查看官方文档
什么是Material Design
Material Design是由Google的设计工程师们基于传统优秀的设计原则,结合丰富的创意和科学技术所开发的一套全新的界面设计语言,包含了视觉、运动、互动效果等特性。那么Google凭什么认为Material Design就能解决Android平台界面风格不统一的问题呢?一言以蔽之,好看!
不过,在重磅推出之后,Material Design的普及程度却不是特别理想。因为这只是一个推荐的设计规范
,主要是面向UI设计人员
的,而不是面向开发者
的。很多开发者可能根本就搞不清楚什么样的界面和效果才叫Material Design,就算搞清楚了,实现起来也会很费劲,因为不少Material Design的效果是很难实现的,而Android中几乎没有提供相应的API支持,基本需要靠开发者自己从零写起。
Google当然意识到了这个问题,于是在2015
年的Google I/O大会上推出了一个DesignSupport
库,这个库将Material Design中最具代表性的一些控件和效果进行了封装
,使得开发者即使在不了解Material Design的情况下,也能非常轻松地将自己的应用Material化。后来Design Support库又改名成了Material
库,用于给Google全平台类的产品提供MaterialDesign的支持。本章我们就将对Material库进行深入的学习,并且配合AndroidX库中的一些控件来完成一个优秀的Material Design应用。
什么是Material Design3
14 年 Google 发布了 Material Design(简称MD),成为了 Google 系产品统一的 UI 设计语言。时至今日 MD 已经有了两次大升级,2018年发布的 Material Theming (Material Design2,简称 M2),以及 2021年新发布的 Material You (Material Design3,简称 M3
)
创建一个Material Design3项目
在最新版Android studio中支持直接创建Material Design3项目,但是创建的项目带有大量示例代码.
这里为了方便简化学习,我们来手动创建一个空的项目.
- 新建项目还是选择
Empty Activity
,和以前一样的流程这里不做讲解,文件名随意. - 创建完成后打开项目中的
settings.gradle
文件,添加google的maven仓库:allprojects { repositories { google() mavenCentral() } }
- 添加material依赖
访问Google的Maven存储库或MVN存储库以查找最新版本的库。比我当前我的最新稳定版本是1.7.0
在build.gradle
中添加dependencies { // ... implementation 'com.google.android.material:material:<最新版本号>' // ... }
- 更改主题
编辑项目res/values/themes.xml
文件:<resources xmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> <!-- 更改父主题--> <style name="Theme.Md3Test" parent="Theme.Material3.Light"> <item name="colorPrimary">@color/purple_500</item> <item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorOnPrimary">@color/white</item> <item name="colorSecondary">@color/teal_200</item> <item name="colorSecondaryVariant">@color/teal_700</item> <item name="colorOnSecondary">@color/black</item> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> </style> </resources>
- 为了方便学习,也可以下载我配置好的基础项目
图标
在使用组件前我们应该学会使用图标
自带的图标库
- 右键
res/drawable
->New
->Vector Asset
- 选择
Clip Art
,搜索自己想要的图标(假设需要加号),接着修改自己需要的参数就行.
学会查看官方文档
Md3官方文档,加入网页后会看到如下页面:
- 开始: 介绍MD3– 从UX指南和工具到可重用组件和开源代码
- 开发: MDC-Android、Flutter、Jetpack Compose 和 Web 的代码和开发人员文档资源。
- 设计基础: 提供了许多优秀用户界面的基础,从可访问性标准到布局和交互的基本模式。
- 样式: 样式是UI的视觉方面,它赋予UI独特的外观和感觉。它们可以通过更改Material主题进行定制。
组件
: 组件是用于创建用户界面的交互构建块。它们可以根据用途分为五类: Action, containment, navigation, selection, and text input.- 博客: 官方最新的特性新闻
组件
加入组件页面后可以看见根据用途分的5类Action, containment, navigation, selection, and text input.可以根据自己的需要进行选择
若是需要查看全部组件,那么将鼠标悬浮到左边即可
这里以按钮为示例进行讲解
这里一般是一个组件分类的总体介绍,一般是设计规范
进入具体的组件页面后看到如下界面:
- 概述: 一般是只用看开发文档即可
在这里可以找到各个软件的文档,一般只要看MDC- Android
的原生控件开发文档即可.
- 控件规格: 控件的设计大小或者属性
在这个分类里面可以看见控件的设计思想,和默认属性等,还有可选属性.但这不是必要的,因为在MDC- Android
里面也有.
- 控件设计指导: 正确的在应用程序上排列控件示例,了解即可
- 无障碍: 字面意思,业务需要可以看一下
组件
Button-按钮
常见的按钮有9种类型:1. 阴影按钮2. 填充按钮3.填充色调按钮4. 轮廓按钮5. 文本按钮6. 图标按钮7. 切换(分段)按钮8. 浮动动作按钮 (FAB)9. 拓展浮动动作按钮
阴影按钮
阴影按钮是基本上带有阴影的轮廓按钮。为防止阴影蠕变,请仅在绝对必要时使用它们,例如当按钮需要视觉时与图案背景分离
简单使用
在布局中:
<Button
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/elevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Elevated button"
/>
效果:
在代码中仍然调用setOnClickListener方法:
elevatedButton.setOnClickListener {
// Respond to button press
添加图标
更改style样式添加icon图标即可
<Button
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:id="@+id/elevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Elevated button"
app:icon="@drawable/ic_baseline_add_24"
/>
分析
阴影按钮具有文本标签、描边容器和可选图标。
属性
- 文本标签属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
文本 | android:text | setText/getText | null |
颜色 | android:textColor | setTextColor/getTextColor | ?attr/colorOnSurface(查看声明) |
字体外观 | android:textAppearance | setTextAppearance | ?attr/textAppearanceLabelLarge |
- 容器属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
颜色 | app:backgroundTint | setBackgroundColor setBackgroundTintList getBackgroundTintList |
?attr/colorSurface(查看声明) |
描边颜色 | app:strokeColor | setStrokeColor setStrokeColorResource getStrokeColor |
null |
描边宽度 | app:strokeWidth | setStrokeWidth setStrokeWidthResource getStrokeWidth |
0dp |
形状 | app:shapeAppearance | setShapeAppearanceModel getShapeAppearanceModel |
?attr/shapeAppearanceSmallComponent |
阴影 | app:elevation | setElevation getElevation |
1dp |
点击按钮产生的波纹色 | app:rippleColor | setRippleColor setRippleColorResource getRippleColor |
?attr/colorOnSurface不透明度为 16%(查看声明) |
- 图标属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
图标 | app:icon | etIcon setIconResource getIcon |
null |
颜色 | app:iconTint | setIconTint setIconTintResource getIconTint |
?attr/colorOnSurface(查看所有州) |
大小 | app:iconSize | setIconSize getIconSize |
wrap_content |
图标位置(相对于文本标签的位置) | app:iconGravity | setIconGravity getIconGravity |
start |
图标外边距(图标和文本标签之间的空格) | app:iconPadding | setIconPadding getIconPadding |
8dp |
- style
元素 | style |
---|---|
默认样式 | Widget.Material3.Button.ElevatedButton |
图标样式 | Widget.Material3.Button.ElevatedButton.Icon |
填充按钮
填充按钮的对比表面颜色使其成为仅次于FAB的最突出的按钮。它用于流中的最终操作或取消
阻止操作。
注意如果未设置样式,则填充按钮是默认样式。
简单使用
在布局中:
<Button
android:id="@+id/filledButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Filled button"
/>
注意:由于这是默认类型,因此无需指定style标记 只要您使用的是material组件主题。如果没有,请将样式设置为 @style/Widget.Material3.Button
在代码中这样调用:
filledButton.setOnClickListener {
// Respond to button press
}
添加图标和控件分析与阴影按钮类似
属性与阴影按钮一致,这里补充一部分
- style
元素 | style |
---|---|
默认样式 | Widget.Material3.Button |
图标样式 | Widget.Material3.Button.Icon |
非高架风格 | Widget.Material3.Button.UnelevatedButton |
未提升的图标样式 | Widget.Material3.Button.UnelevatedButton.Icon |
填充色调按钮
填充色调按钮具有较浅的背景色和较深的标签颜色,使它们视觉上比常规填充按钮突出。它们仍然用于完成或取消阻止流中的操作,但这些不需要强调最好。
简单使用
在布局中:
<Button
style="@style/Widget.Material3.Button.TonalButton"
android:id="@+id/filledTonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Filled tonal button"
/>
在代码中:
filledTonalButton.setOnClickListener {
// Respond to button press
}
添加图标和控件分析与阴影按钮类似
属性与阴影按钮一致,这里补充一部分
- style
元素 | style |
---|---|
默认样式 | Widget.Material3.Button.TonalButton |
图标样式 | Widget.Material3.Button.TonalButton.Icon |
轮廓按钮
带轮廓的按钮适用于需要注意但不是主要操作的操作,例如“查看全部” 或“添加到购物车”。这也是用来给别人机会的按钮改变主意或退出。
简单使用
在布局中:
<Button
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/outlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Outlined button"
/>
在代码中:
outlinedButton.setOnClickListener {
// Respond to button press
}
添加图标和控件分析与阴影按钮类似
属性与阴影按钮一致,这里补充一部分
- style
元素 | style |
---|---|
默认样式 | Widget.Material3.Button.OutlinedButton |
图标样式 | Widget.Material3.Button.OutlinedButton.Icon |
文本按钮
文本按钮使用较少,视觉突出,因此应该用于低强调的动作,例如:当提供多个选项。
简单使用
在布局中:
<Button
style="@style/Widget.Material3.Button.TextButton"
android:id="@+id/textButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text button"
/>
在代码中:
textButton.setOnClickListener {
// Respond to button press
}
添加图标与阴影按钮类似
控件分析
属性与阴影按钮一致,这里补充一部分
- style
元素 | style |
---|---|
默认样式 | Widget.Material3.Button.TextButton |
图标样式 | Widget.Material3.Button.TextButton.Icon |
全宽按钮 | Widget.Material3.Button.TextButton.Dialog.FullWidth |
图标按钮
图标按钮帮助用户只需轻点一下即可执行补充操作。有两种类型的图标按钮:标准按钮和包含按钮。
- 标准图标按钮:默认情况下,图标按钮没有容器。
- 包含的图标按钮:有一个容器在图标周围。
使用要求
- 当需要紧凑按钮时,应该使用图标按钮.例如工具栏
- 应该确保图标的意义明确
- 应该由工具提示描述图标操作,而不是图标
简单使用
- 在布局中:
<Button style="?attr/materialIconButtonStyle" android:id="@+id/iconButton" android:layout_width="wrap_content" android:layout_height="wrap_content" app:icon="@drawable/ic_baseline_add_24" />
- 在代码中:
iconButton.addOnButtonCheckedListener { iconButton, checkedId, isChecked -> // Respond to button selection }
- 效果图
style
元素 | style | 主题属性 |
---|---|---|
默认样式 | Widget.Material3.Button.IconButton | ?attr/materialIconButtonStyle |
填充图标按钮 | Widget.Material3.Button.IconButton.Filled | ?attr/materialIconButtonFilledStyle |
填充色调图标按钮 | Widget.Material3.Button.IconButton.Filled.Tonal | ?attr/materialIconButtonFilledTonalStyle |
“带轮廓的图标”按钮 | Widget.Material3.Button.IconButton.Outlined | ?attr/materialIconButtonOutlinedStyle |
分段(切换)按钮
切换按钮可以是用于从一组按钮选项中进行选择。有两种类型的分段按钮:单选和多选。
- 分段按钮以前称为切换按钮
- 单选分段按钮用于选择单个选项、在视图之间切换或对元素进行排序
- 多选分段按钮用于从一组选项或过滤器元素中选择多个选项
简单使用
在布局中:
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 1"
/>
<Button
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 2"
/>
<Button
style="?attr/materialButtonOutlinedStyle"
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button 3"
/>
</com.google.android.material.button.MaterialButtonToggleGroup>
在代码中:
toggleButton.addOnButtonCheckedListener { toggleButton, checkedId, isChecked ->
// Respond to button selection
}
图片按钮组
- 修改
res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="Widget.App.Button.OutlinedButton.IconOnly" parent="Widget.Material3.Button.OutlinedButton"> <item name="iconPadding">0dp</item> <item name="android:insetTop">0dp</item> <item name="android:insetBottom">0dp</item> <item name="android:paddingLeft">12dp</item> <item name="android:paddingRight">12dp</item> <item name="android:minWidth">48dp</item> <item name="android:minHeight">48dp</item> </style> </resources>
- 在布局中:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.material.button.MaterialButtonToggleGroup android:id="@+id/toggleButton" android:layout_width="wrap_content" android:layout_height="wrap_content"> <Button style="@style/Widget.App.Button.OutlinedButton.IconOnly" android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" app:icon="@drawable/ic_baseline_add_24" /> <Button style="@style/Widget.App.Button.OutlinedButton.IconOnly" android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" app:icon="@drawable/ic_baseline_add_24" /> <Button style="@style/Widget.App.Button.OutlinedButton.IconOnly" android:id="@+id/button3" android:layout_width="wrap_content" android:layout_height="wrap_content" app:icon="@drawable/ic_baseline_add_24" /> </com.google.android.material.button.MaterialButtonToggleGroup> </LinearLayout>
- 效果图
控件分析
属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
单选 | app:singleSelection | setSingleSelection isSingleSelection |
false |
需要选择 | app:selectionRequired | setSelectionRequired isSelectionRequired |
false |
**启用组和所有子项 | android:enabled | setEnabled isEnabled |
true |
- style
元素 | 风格 |
---|---|
默认样式 | Widget.Material3.MaterialButtonToggleGroup |
浮动动作按钮(FAB)
FAB 在屏幕上执行主要或最常见的操作。它出现在所有屏幕内容的前面,通常为带有图标的方形。
在使用FAB之前需要添加依赖项。有关更多信息,请转到入门页面。
无障碍: 您应该通过属性或方法在FA上设置内容描述,以便屏幕阅读器能够正确识别android:contentDescription``setContentDescription
有四种类型的FAB:1.FAB,2.小型FAB, 3.大型FAB,4下一小节的浮动FAB
简单使用
- 在布局中:
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/floating_action_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:contentDescription="确认" app:srcCompat="@drawable/ic_baseline_done_24"/>
- 在代码中:
fab.setOnClickListener { // Respond to FAB click }
- 效果图
分析
常规、小型和大型 FAB 关键属性
- 容器属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
颜色 | app:backgroundTint | setBackgroundTintList getBackgroundTintList |
?attr/colorPrimaryContainer (see all states) |
边框宽度 | app:borderWidth | N/A | 0dp |
大小 | app:fabSize app:fabCustomSize |
setSize setCustomSize clearCustomSize getSize getCustomSize |
auto |
形状外观 | shapeAppearance shapeAppearanceOverlay |
setShapeAppearanceModel getShapeAppearanceModel |
ShapeAppearanceOverlay.Material3.FloatingActionButton |
阴影 | app:elevation | setElevation getCompatElevation |
6dp |
悬浮和聚焦阴影 | app:hoveredFocusedTranslationZ | setCompatHoveredFocusedTranslationZ getCompatHoveredFocusedTranslationZ |
2dp |
点击阴影 | app:pressedTranslationZ | setCompatPressedTranslationZ getCompatPressedTranslationZ |
6dp |
波纹颜色 | app:rippleColor | setRippleColor getRippleColor getRippleColorStateList |
variations of ?attr/colorOnPrimaryContainer, see all states |
动画 | app:showMotionSpec app:hideMotionSpec |
setMotionSpec setMotionSpecResource getMotionSpec |
@null |
- 图标属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
图标 | app:srcCompat | setImageDrawable setImageResource getDrawable |
null |
颜色 | app:tint | setImageTintList getImageTintList |
?attr/colorOnPrimaryContainer (see all states) |
- style属性
元素 | 风格 |
---|---|
默认样式 | Widget.Material3.FloatingActionButton.Primary(?attr/floatingActionButtonStyle) |
一些方法
- 显示/隐藏
使用theand
方法对可见性进行动画处理。显示控件并将其淡入,而隐藏控件并淡出它.// To show: fab.show() // To hide: fab.hide()
- 扩展和收缩
使用theand
方法对显示和隐藏的文本进行动画处理 扩展动画扩展 FAB 以显示 文本和图标。收缩动画将 FAB 缩小以仅显示 图标。// To extend: extendedFab.extend() // To shrink: extendedFab.shrink()
设置FAB的尺寸
可以通过使用或调整大小 对于大/小 FAB(仅限 M3),通过应用所需的样式。discrete大小模式在 Material3 样式中已弃用。
如果要使用小型 FAB,请应用以下样式属性之一:
- ?attr/floatingActionButtonSmallStyle
- ?attr/floatingActionButtonSmallPrimaryStyle
- ?attr/floatingActionButtonSmallSecondaryStyle
- ?attr/floatingActionButtonSmallTertiaryStyle
- ?attr/floatingActionButtonSmallSurfaceStyle`
如果要使用大型 FAB,请应用以下样式属性之一:
- ttr/floatingActionButtonLargeStyle
- ttr/floatingActionButtonLargePrimaryStyle
- ttr/floatingActionButtonLargeSecondaryStyle
- ttr/floatingActionButtonLargeTertiaryStyle
- ttr/floatingActionButtonLargeSurfaceStyle
拓展浮动动作按钮
扩展的 FAB 更宽,并且包含文本标签。
简单使用
- 在布局中
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton android:id="@+id/extended_fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="16dp" android:layout_gravity="bottom|end" android:contentDescription="拓展浮动按钮" android:text="拓展浮动按钮" app:icon="@drawable/ic_baseline_done_24"/>
- 在代码中:
extendedFab.setOnClickListener { // Respond to Extended FAB click }
- 效果图
分析
关系属性和浮动按钮一致,这里仅补充
- 容器属性
元素 | 属性 | 相关方法 | 默认值 |
---|---|---|---|
扩展策略 | app:extendStrategy | 不适用 | wrap_content |
压高 | app:pressedTranslationZ | 不适用 | 6dp |
动画 | app:showMotionSpec app:hideMotionSpec extendMotionSpec shrinkMotionSpec |
setMotionSpec setMotionSpecResource get*MotionSpec |
查看动画 |
顺带一说
- 根据操作的重要性选择按钮的类型。动作越重要,其按钮应越强调。
- 所有按钮都有完全圆角
- 按钮有四种常见的颜色映射,每种映射都有浅色和深色主题
新增功能
- 颜色:新的颜色映射与动态颜色的兼容性。图标和标签现在共享相同的颜色。
- 图标:头部和尾部图标的标准大小现在为 18dp
- 形状:全圆角半径、更高的高度和新的最小宽度
- 排版:按钮文本为句子大小写,没有全部大写
- 类型:三种新的按钮类型 - 阴影按钮,填充按钮和填充色调按钮 - 取代以前称为包含按钮的内容。
- M2:按钮的高度为 36dp,角半径略微圆润。按钮标签使用全部大写。
- M3:按钮较高,为 40dp,并具有完全圆角。按钮标签使用句子大小写。
按钮选择的标准
重点级别 | 元件 | 理由 | 示例操作 |
---|---|---|---|
高强调 – 用于屏幕上的主要、最重要或最常见的操作 | 拓展FAB | 扩展后的 FAB 更宽的格式和文本标签使其比 FAB 更具视觉效果。它通常用于较大的屏幕上,而FAB似乎太小了。 | 创建文件 |
FAB | FAB 仍然是屏幕主要操作的默认组件。它有三种尺寸:小型 FAB、FAB 和大型 FAB。 | 创建\撰写 | |
填充按钮 | 填充按钮的对比表面颜色使其成为仅次于 FAB 的最突出的按钮。它用于流中的最终操作或取消阻止操作。 | 保存\确认\完成 | |
中等强调 – 用于不会分散其他屏幕元素注意力的重要操作 | 填充色按钮 | 填充色调按钮具有较浅的背景色和较深的标签颜色,使其视觉效果不如常规填充按钮突出。它们仍用于流中的最终操作或取消阻止操作,但强调较少。 | 保存\确认\完成 |
阴影按钮 | 阴影的按钮本质上是具有较浅背景颜色和阴影的填充按钮。为了防止阴影蠕变,请仅在绝对必要时使用它们,例如当按钮需要与图案背景进行视觉分离时。 | 回复\查看全部\加入购物车\删除 | |
分级显示按钮 | 对需要注意但不是主要操作的操作使用分级显示的按钮,例如查看全部或添加到购物车。这也是用于让某人有机会改变主意或退出流程的按钮。 | 回复\查看全部\加入购物车\删除 | |
低强调 – 用于突出程度最低的可选或补充操作 | 文本按钮 | 文本按钮的视觉突出度较低,因此应用于低强调操作,例如替代选项。 | 了解更多 \查看全部 \更改帐号 \开启 |
分段按钮 | 分段按钮比单个图标按钮具有更多的视觉突出度。 | 左对齐/中间对齐/右对齐 | |
图标按钮 | 图标按钮是最紧凑和最微妙的按钮类型,用于可选的补充操作,例如“书签”或“星号”。 | 添加到收藏夹 \打印 |
Kotlin课堂:编写好用的工具方法
到目前为止,我们已经将Kotlin大部分系统性的知识点学习完了。掌握了如此多的Kotlin特性,你知道该如何对它们进行灵活运用吗?
事实上,Kotlin提供的丰富语法特性给我们提供了无限扩展的可能,各种复杂的API经过特殊的封装处理之后都能变得简单易用。比如我们之前在第7章体验过的KTX库,就是Google为了简化许多API的用法而专门设计的。不过KTX库所能覆盖到的功能毕竟有限,因此最重要的还是我们要能养成对Kotlin的各种特性进行灵活运用的意识。那么在本节的Kotlin课堂中,我将带你对几个常用API的用法进行简化,从而编写出一些好用的工具方法。
求N个数的最大最小值
两个数比大小这个功能,相信每一位开发者都遇到过。如果我想要获取两个数中较大的那个数,除了使用最基本的if语句之外,还可以借助Kotlin内置的max()函数,如下所示:
val a = 10
val b = 15
val larger = max(a, b)
这种代码看上去简单直观,也很容易理解,因此好像并没有什么优化的必要。
可是现在如果我们想要在3个数中获取最大的那个数,应该怎么写呢?由于max()函数只能接收两个参数,因此需要先比较前两个数的大小,然后再拿较大的那个数和剩余的数进行比较,写法如下:
val a = 10
val b = 15
val c = 5
val largest = max(max(a, b), c)
有没有觉得代码开始变得复杂了呢?3个数中获取最大值就需要使用这种嵌套max()函数的写法了,那如果是4个数、5个数呢?没错,这个时候你就应该意识到,我们是可以对max()函数的用法进行简化的。
回顾一下,我们之前在第7章的Kotlin课堂中学过vararg关键字,它允许方法接收任意多个同等类型的参数,正好满足我们这里的需求。那么我们就可以新建一个Max.kt文件,并在其中自定义一个max()函数,如下所示:
fun max(vararg nums: Int): Int {
var maxNum = Int.MIN_VALUE
for (num in nums) {
maxNum = kotlin.math.max(maxNum, num)
}
return maxNum
}
可以看到,这里max()函数的参数声明中使用了vararg关键字,也就是说现在它可以接收任意多个整型参数。接着我们使用了一个maxNum变量来记录所有数的最大值,并在一开始将它赋值成了整型范围的最小值。然后使用for-in循环遍历nums参数列表,如果发现当前遍历的数字比maxNum更大,就将maxNum的值更新成这个数,最终将maxNum返回即可。
仅仅经过这样的一层封装之后,我们在使用max()函数时就会有翻天覆地的变化,比如刚才同样的功能,现在就可以使用如下的写法来实现:
val a = 10
val b = 15
val c = 5
val largest = max(a, b, c
这样我们就彻底摆脱了嵌套函数调用的写法,现在不管是求3个数的最大值还是求N个数的最大值,只需要不断地给max()函数传入参数就可以了。
不过,目前我们自定义的max()函数还有一个缺点,就是它只能求N个整型数字的最大值,如果我还想求N个浮点型或长整型数字的最大值,该怎么办呢?当然你可以定义很多个max()函数的重载,来接收不同类型的参数,因为Kotlin中内置的max()函数也是这么做的。但是这种方案实现起来过于烦琐,而且还会产生大量的重复代码,因此这里我准备使用一种更加巧妙的做法。
Java中规定,所有类型的数字都是可比较的,因此必须实现Comparable接口,这个规则在Kotlin中也同样成立。那么我们就可以借助泛型,将max()函数修改成接收任意多个实现Comparable接口的参数,代码如下所示:
fun <T : Comparable<T>> max(vararg nums: T): T {
if (nums.isEmpty()) throw RuntimeException("Params can not be empty.")
var maxNum = nums[0]
for (num in nums) {
if (num > maxNum) {
maxNum = num
}
}
return maxNum
}
可以看到,这里将泛型T的上界指定成了Comparable<T>
,那么参数T就必然是Comparable<T>
的子类型了。接下来,我们判断nums参数列表是否为空,如果为空的话就主动抛出一个异常,提醒调用者max()函数必须传入参数。紧接着将maxNum的值赋值成nums参数列表中第一个参数的值,然后同样是遍历参数列表,如果发现了更大的值就对maxNum进行更新。
经过这样的修改之后,我们就可以更加灵活地使用max()函数了,比如说求3个浮点型数字的最大值,同样也变得轻而易举:
val a = 3.5
val b = 3.8
val c = 4.1
val largest = max(a, b, c)
而且现在不管是双精度浮点型、单精度浮点型,还是短整型、整型、长整型,只要是实现Comparable接口的子类型,max()函数全部支持获取它们的最大值,是一种一劳永逸的做法。
而如果你想获取N个数的最小值,实现的方式也是类似的,只需要定义一个min()函数就可以了,这个功能就当作课后习题留给你来完成吧。
简化Toast的用法
新建一个Toast.kt文件,并在其中编写如下代码:
fun String.showToast(context: Context) {
Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
}
fun Int.showToast(context: Context) {
Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
}
这里分别给String类和Int类新增了一个showToast()函数,并让它们都接收一个Context参数。然后在函数的内部,我们仍然使用了Toast原生API用法,只是将弹出的内容改成了this,另外将Toast的显示时长固定设置成Toast.LENGTH_SHORT。
那么经过这样的扩展之后,我们以后在使用Toast时可以变得多么简单呢?体验一下就知道了,比如同样弹出一段文字提醒就可以这么写:
"This is Toast".showToast(context)
怎么样,比起原生Toast的用法,有没有觉得这种写法畅快多了呢?另外,这只是直接弹出一段字符串文本的写法,如果你想弹出一个定义在strings.xml中的字符串资源,也非常简单,写法如下:
R.string.app_name.showToast(context)
当然,这种写法其实还存在一个问题,就是Toast的显示时长被固定了,如果我现在想要使用Toast.LENGTH_LONG类型的显示时长该怎么办呢?要解决这个问题,其实最简单的做法就是在showToast()函数中再声明一个显示时长参数,但是这样每次调用showToast()函数时都要额外多传入一个参数,无疑增加了使用复杂度。
不知道你现在有没有受到什么启发呢?回顾一下,我们在第2章学习Kotlin基础语法的时候,曾经学过给函数设定参数默认值的功能。只要借助这个功能,我们就可以在不增加showToast()函数使用复杂度的情况下,又让它可以支持动态指定显示时长了。修改Toast.kt中的代码,如下所示:
fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, this, duration).show()
}
可以看到,我们给showToast()函数增加了一个显示时长参数,但同时也给它指定了一个参数
默认值。这样我们之前所使用的showToast()函数的写法将完全不受影响,默认会使用
Toast.LENGTH_SHORT类型的显示时长。而如果你想要使用Toast.LENGTH_LONG的显示时
长,只需要这样写就可以了:
"This is Toast".showToast(context, Toast.LENGTH_LONG)
简化Snackbar的用法
Snackbar的用法和Toast类似但是又比Toast复杂一些,在学习本小节之前应该先了解它的用法
- 先来回顾一下Snackbar的常规用法吧,如下所示:
那么对于这种结构的API,我们该如何进行简化呢?其实简化的方式并不固定,接下来我即将演示的写法也只是我个人认为比较不错的一种。Snackbar.make(view, "This is Snackbar", SnackbarLENGTH_SHORT) .setAction("Action") { // 处理具体的逻辑 } .show()
- 由于make()方法接收一个View参数,Snackbar会使用这个View自动查找最外层的布局,用于展示Snackbar。因此,我们就可以给View类添加一个扩展函数,并在里面封装显示Snackbar的具体逻辑。新建一个Snackbar.kt文件,并编写如下代码:
这段代码应该还是很好理解的,和刚才的showToast()函数比较相似。只是我们将扩展函数添加到了View类当中,并在参数列表上声明了Snackbar要显示的内容以及显示的时长。另外,Snackbar和Toast类似,显示的内容也是支持传入字符串和字符串资源id两种类型的,因此这里我们给showSnackbar()函数进行了两种参数类型的函数重载。fun View.showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT) { Snackbar.make(this, text, duration).show() } fun View.showSnackbar(resId: Int, duration: Int = Snackbar.LENGTH_SHORT) { Snackbar.make(this, resId, duration).show() }
- 现在想要使用Snackbar显示一段文本提示,只需要这样写就可以了:
假如Snackbar没有setAction()方法,那么我们的简化工作到这里就可以结束了。但是setAction()方法作为Snackbar最大的特色之一,如果不能支持的话,我们编写的showSnackbar()函数也就变得毫无意义了。view.showSnackbar("This is Snackbar")
- 这个时候,神通广大的高阶函数又能派上用场了,我们可以让showSnackbar()函数再额外接收一个函数类型参数,以此来实现Snackbar的完整功能支持。修改Snackbar.kt中的代码,如下所示:
可以看到,这里我们给两个showSnackbar()函数都增加了一个函数类型参数,并且还增加了一个用于传递给setAction()方法的字符串或字符串资源id。这里我们需要将新增的两个参数都设置成可为空的类型,并将默认值都设置成空,然后只有当两个参数都不为空的时候,我们才去调用Snackbar的setAction()方法来设置额外的点击事件。如果触发了点击事件,只需要调用函数类型参数将事件传递给外部的Lambda表达式即可。fun View.showSnackbar(text: String, actionText: String? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) { val snackbar = Snackbar.make(this, text, duration) if (actionText != null && block != null) { snackbar.setAction(actionText) { block() } } snackbar.show() } fun View.showSnackbar(resId: Int, actionResId: Int? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) { val snackbar = Snackbar.make(this, resId, duration) if (actionResId != null && block != null) { snackbar.setAction(actionResId) { block() } } snackbar.show() }
- 这样showSnackbar()函数就拥有比较完整的Snackbar功能了,比如本小节最开始的那段示例代码,现在就可以使用如下写法进行实现:
怎么样,和Snackbar原生API的用法相比,我们编写的showSnackbar()函数是不是要明显简单好用得多?view.showSnackbar("This is Snackbar", "Action") { // 处理具体的逻辑 }