了解Android studio 初学者零基础推荐(4)

更改应用图标

起始代码网址

https://github.com/google-developer-training/basic-android-kotlin-compose-training-affirmations

包含起始代码的分支名称intermediate

设置应用图标的目标是,无论设备型号或屏幕密度如何,您的应用图标都会呈现简单清晰的效果。

屏幕密度是指屏幕上每英寸的像素数或每英寸的点数(dpi)。对于中密度设备(mdpi),屏幕上每英寸有160个点,而超超超高密度设备(xxxhdpi)的屏幕上有640个点。为了适应不同屏幕密度,我们需要提供不同版本的应用图标。

android上的密度限定表:

自适应图标

前景层和背景层

Android(自API 26起)引入了自适应图标的支持。自适应图标由前景层和背景层组成,这种图标可以更加灵活和有趣的呈现视觉效果。使的android应用可以在任何设备的显示屏量身定制启动图标,从而适应各种设备。

在上面的示例中,白色的 Android 图标位于前景图层中,而蓝白网格位于背景图层中。前景图层会叠加在背景图层上。系统会在其上再叠加一个蒙版(在此例中为圆形蒙版),这样就产生了一个圆形应用图标。

浏览自适应图标文件

注意:(1)这些前景图和背景图有特定要求,例如它们的尺寸都必须为 108dp*108dp,您可以在 AdaptiveIconDrawable 文档中查看更多详细信息,还可以在 Material Design 网站上查看 Android 图标的设计指南。

(2)由于图标的边缘可能会根据设备制造商提供的蒙版的形状进行剪裁,请务必将图标的关键信息放在"安全区域"内。安全区域是一个位于前景图层中心直径为66dpi的圆圈。该安全区域之外不应是重要信息。

扩展

App中使用图片需不需要标注出处?!

  • 取决于图片的版权和许可协议。

如果你使用的图片是:

  • 自己拍摄或原创,不需要标注出处,但建议保留版权信息。

  • 公共领域(Public Domain)或CC0授权的图片,一般不强制要求标注出处,但标注是尊重作者的好习惯。

  • 带版权保护的图片(比如商业图库、摄影师作品),必须按照授权协议标注出处,或者支付版权费用。

  • 有明确署名要求的免费授权(如CC BY协议),必须注明作者和出处。

你想下载 CC0 1.0 Universal 授权的图片,通常就是找那些明确标注为公共领域CC0许可的免费图片资源网站,下载后即可免费商用,无需署名(但标注作者是尊重好习惯)。

网站名 说明 网址
Unsplash 高质量摄影作品,大部分免费可商用 [Beautiful Free Images & Pictures | Unsplash](https://unsplash.com "Beautiful Free Images & Pictures
Pixabay 丰富多样图片、插画,全部免费可商用 https://pixabay.com
Pexels 免费可商用,包含视频和照片 https://pexels.com
Public Domain Pictures 纯公共领域图片库 https://publicdomainpictures.net
Flickr (CC0搜索) 使用高级搜索筛选CC0授权的图片 https://flickr.com

使用Jetpack Compose 实现Material主题设置

Material Design 是Google设计人员和开发者打造的一套设计体系,旨在帮助开发者针对Android以及其他移动平台和网络平台打造优质的数字体验。它提供了一套准则,指导如何以具有可读性、吸引性和一致性的方式构建应用界面。这一部分学习自定义颜色排版 、和形状方面 的指导,以及添加顶部应用栏,以显示应用的名称和图标,以便您在应用中使用Material Design。(见培训课程 第三单元 构建精美应用)

应用概览

这一部分我们打造一个显示狗狗列表的宠物app,使用Material Design打造出出色的应用体验。

主题颜色

我们要配置的主题颜色是下面这样的,分为浅色主题和深色主题,具体颜色和应用界面显示如下:

字型

这个应用中我们使用的字型样式如下:

主题文件

Theme.kt 存储所有有关主题的信息,根据要求修改如下:

Kotlin 复制代码
@Composable
fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColors
        else -> LightColors
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            setUpEdgeToEdge(view, darkTheme)
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        shapes = Shapes,
        typography = Typography,
        content = content
    )
}

/**
 * Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
 * light or dark depending on whether the [darkTheme] is enabled or not.
 */
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
    val window = (view.context as Activity).window
    WindowCompat.setDecorFitsSystemWindows(window, false)
    window.statusBarColor = Color.Transparent.toArgb()
    val navigationBarColor = when {
        Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
        Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
        // Min sdk version for this app is 24, this block is for SDK versions 24 and 25
        else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
    }
    window.navigationBarColor = navigationBarColor
    val controller = WindowCompat.getInsetsController(window, view)
    controller.isAppearanceLightStatusBars = !darkTheme
    controller.isAppearanceLightNavigationBars = !darkTheme
}

MainActivity.kt 中,添加了 WoofTheme() 以便为整个应用提供 Material 主题设置。

Kotlin 复制代码
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           WoofTheme {
               Surface(
                   modifier = Modifier.fillMaxSize()
               ) {
                   WoofApp()
               }
           }
       }
   }
}

查看 WoofPreview()。添加了 WoofTheme(),以提供您在 WoofPreview() 中看到的 Material 主题设置。

Kotlin 复制代码
@Preview
@Composable
fun WoofPreview() {
    WoofTheme(darkTheme = false) {
        WoofApp()
    }
}

获取起始代码

首先,克隆该代码的 GitHub 代码库:

复制代码
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout starter

注意: 起始代码位于所下载代码库的 starter 分支中。

您可以在 Woof app GitHub 仓库中浏览该代码。

查看起始代码

  1. 在 Android Studio 中打开起始代码。
  2. 依次打开 com.example.woof > data > Dog.kt 。其中包含用于代表狗狗的照片、名字、年龄和爱好的 Dog data class。它还包含狗狗列表以及您将在应用中用作数据的信息。
  3. 依次打开 res > drawable。此文件包含此项目所需的所有图片资源,包括应用图标、狗狗图片和顶部应用栏中使用的图标。
  4. 依次打开 res > values > strings.xml。此文件包含您在此应用中使用的字符串,包括应用名称、狗狗名字、说明等。
  5. 打开 MainActivity.kt。此文件包含创建简单列表的代码,用于显示狗狗的照片、名字及年龄。
  6. WoofApp() 包含用于显示 DogItemLazyColumn
  7. DogItem() 包含用于显示狗狗照片及相关信息的 Row
  8. DogIcon() 显示小狗的照片。
  9. DogInformation() 显示狗狗的名字和年龄。
  10. WoofPreview() 可让您在 Design 窗格中预览应用。

注意 :您可能会注意到,每个 @Composable 方法都有一个修饰符作为参数。在 Compose 中,最佳实践是将修饰符参数传递给可组合函数。这是因为它允许父级可组合项将上下文信息传递给子级可组合项。

例如,假设有一个按钮,一个父按钮可能想让其子按钮使用完整尺寸,而另一个父按钮可能想让该子按钮封装内容。这样会提高代码的可重用性。此参数通常名为"修饰符",应作为函数的参数列表的第一个可选参数显示。此修饰符将应用于方法的第一个子级。如需了解详情,请参阅 Jetpack Compose 的 API 准则

确保您的模拟器/设备采用浅色主题

在此 Codelab 中,您将使用浅色主题和深色主题,不过,在我们学习过程中的大部分主题都采用浅色主题。开始之前,请确保您的设备/模拟器使用的是浅色主题。

如需在浅色主题下查看您的应用,请在模拟器或实体设备上执行以下操作:

  1. 打开设备的设置应用。
  2. 搜索深色主题,然后点击进入该主题。
  3. 如果深色主题处于开启状态,请将其关闭。

运行起始代码以查看应用的起始状态;这是一个列表,其中会显示狗狗的照片、名字和年龄。它能正常运行,但外观不好看,所以我们会解决该问题。

修改颜色

让我们先修改应用的主题颜色,android系统颜色用十六进制颜色值表示。十六进制颜色代码以井号 (#) 字符开头,后跟六个字母和/或数字(代表该颜色的红色、绿色和蓝色 [RGB] 分量)。前两个字母/数字表示红色,后面的两个表示绿色,最后两个表示蓝色。

颜色还可以包含 Alpha 值(字母和/或数字),用于表示颜色的透明度(#00 表示不透明度为 0% [完全透明],#FF 表示不透明度为 100% [完全不透明])。若添加 alpha 值,则该值为井号 (#) 字符后的十六进制颜色代码的前两个字符。如果未添加 alpha 值,系统会假定它是 #FF,即 100% 不透明(完全不透明)。

以下是一些颜色及其十六进制值的示例。

使用 Material Theme Builder 创建配色方案

为了创建应用的自定义配色方案,我们将使用 Material Theme Builder。

点击此处链接即可打开 Material Theme Builder。

在左侧窗格中,您会看到"Core Colors"(核心颜色),然后点击"Primary"(主色):

系统会打开 HCT 颜色选择器。

若要创建应用屏幕截图中显示的配色方案,您需要更改此颜色选择器中的主色。在文本框中,将当前文本替换为 #006C4C。这样,应用的主色就会变为绿色。

请注意这是如何将屏幕上的应用更新为采用绿色配色方案的。

向下滚动页面,您会看到系统根据您输入的颜色生成的浅色和深色主题的完整配色方案。

您可能想了解所有这些角色的含义及其使用方法,以下是其中几个主要角色:

  • primary(主色)用于整个界面的关键组件。
  • secondary(副色)用于界面中不太显眼的组件。
  • tertiary(第三色)用于对比强调,可以平衡主色和副色,或者引起用户对某个元素(例如输入字段)的高度关注。
  • on 颜色元素显示在调色板中其他颜色的上层 ,主要应用于文本、图标和描边。在调色板中,我们有一个 onSurface 颜色(显示在 surface 颜色的上层)和一个 onPrimary 颜色(显示在主要颜色的上层)。

有了这些槽可以实现统一的设计体系,相关组件的颜色也相似。

关于颜色的理论讲解到此为止 - 现在可以向应用中添加这个漂亮的调色板了!

向主题添加调色板

在 Material Theme Builder 页面上,您可以选择点击 Export (导出)按钮,以便下载 Color.kt 文件以及包含您在 Theme Builder 中所建自定义主题的 Theme.kt 文件。

这样一来,我们创建的自定义主题便会添加到您的应用中。不过,由于生成的 Theme.kt 文件不包含动态颜色的代码(稍后我们会在此 Codelab 中介绍),因此请将这些文件复制进来。

注意:如果您决定将通过 Material Theme Builder 生成的文件用于其他项目,则需要将软件包名称更新为对应项目的软件包名称。

打开 Color.kt 文件,以复制新的配色方案。

复制代码
package com.example.woof.ui.theme

import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF006C4C)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFF89F8C7)
val md_theme_light_onPrimaryContainer = Color(0xFF002114)
val md_theme_light_secondary = Color(0xFF4D6357)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFCFE9D9)
val md_theme_light_onSecondaryContainer = Color(0xFF092016)
val md_theme_light_tertiary = Color(0xFF3D6373)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFC1E8FB)
val md_theme_light_onTertiaryContainer = Color(0xFF001F29)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFBFDF9)
val md_theme_light_onBackground = Color(0xFF191C1A)
val md_theme_light_surface = Color(0xFFFBFDF9)
val md_theme_light_onSurface = Color(0xFF191C1A)
val md_theme_light_surfaceVariant = Color(0xFFDBE5DD)
val md_theme_light_onSurfaceVariant = Color(0xFF404943)
val md_theme_light_outline = Color(0xFF707973)
val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED)
val md_theme_light_inverseSurface = Color(0xFF2E312F)
val md_theme_light_inversePrimary = Color(0xFF6CDBAC)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF006C4C)
val md_theme_light_outlineVariant = Color(0xFFBFC9C2)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFF6CDBAC)
val md_theme_dark_onPrimary = Color(0xFF003826)
val md_theme_dark_primaryContainer = Color(0xFF005138)
val md_theme_dark_onPrimaryContainer = Color(0xFF89F8C7)
val md_theme_dark_secondary = Color(0xFFB3CCBE)
val md_theme_dark_onSecondary = Color(0xFF1F352A)
val md_theme_dark_secondaryContainer = Color(0xFF354B40)
val md_theme_dark_onSecondaryContainer = Color(0xFFCFE9D9)
val md_theme_dark_tertiary = Color(0xFFA5CCDF)
val md_theme_dark_onTertiary = Color(0xFF073543)
val md_theme_dark_tertiaryContainer = Color(0xFF244C5B)
val md_theme_dark_onTertiaryContainer = Color(0xFFC1E8FB)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF191C1A)
val md_theme_dark_onBackground = Color(0xFFE1E3DF)
val md_theme_dark_surface = Color(0xFF191C1A)
val md_theme_dark_onSurface = Color(0xFFE1E3DF)
val md_theme_dark_surfaceVariant = Color(0xFF404943)
val md_theme_dark_onSurfaceVariant = Color(0xFFBFC9C2)
val md_theme_dark_outline = Color(0xFF8A938C)
val md_theme_dark_inverseOnSurface = Color(0xFF191C1A)
val md_theme_dark_inverseSurface = Color(0xFFE1E3DF)
val md_theme_dark_inversePrimary = Color(0xFF006C4C)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFF6CDBAC)
val md_theme_dark_outlineVariant = Color(0xFF404943)
val md_theme_dark_scrim = Color(0xFF000000)

打开 Theme.kt 文件,并将其内容替换为以下代码,以为主题添加新的颜色。

复制代码
package com.example.woof.ui.theme

import android.app.Activity
import android.os.Build
import android.view.View
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

private val LightColors = lightColorScheme(
    primary = md_theme_light_primary,
    onPrimary = md_theme_light_onPrimary,
    primaryContainer = md_theme_light_primaryContainer,
    onPrimaryContainer = md_theme_light_onPrimaryContainer,
    secondary = md_theme_light_secondary,
    onSecondary = md_theme_light_onSecondary,
    secondaryContainer = md_theme_light_secondaryContainer,
    onSecondaryContainer = md_theme_light_onSecondaryContainer,
    tertiary = md_theme_light_tertiary,
    onTertiary = md_theme_light_onTertiary,
    tertiaryContainer = md_theme_light_tertiaryContainer,
    onTertiaryContainer = md_theme_light_onTertiaryContainer,
    error = md_theme_light_error,
    errorContainer = md_theme_light_errorContainer,
    onError = md_theme_light_onError,
    onErrorContainer = md_theme_light_onErrorContainer,
    background = md_theme_light_background,
    onBackground = md_theme_light_onBackground,
    surface = md_theme_light_surface,
    onSurface = md_theme_light_onSurface,
    surfaceVariant = md_theme_light_surfaceVariant,
    onSurfaceVariant = md_theme_light_onSurfaceVariant,
    outline = md_theme_light_outline,
    inverseOnSurface = md_theme_light_inverseOnSurface,
    inverseSurface = md_theme_light_inverseSurface,
    inversePrimary = md_theme_light_inversePrimary,
    surfaceTint = md_theme_light_surfaceTint,
    outlineVariant = md_theme_light_outlineVariant,
    scrim = md_theme_light_scrim,
)


private val DarkColors = darkColorScheme(
    primary = md_theme_dark_primary,
    onPrimary = md_theme_dark_onPrimary,
    primaryContainer = md_theme_dark_primaryContainer,
    onPrimaryContainer = md_theme_dark_onPrimaryContainer,
    secondary = md_theme_dark_secondary,
    onSecondary = md_theme_dark_onSecondary,
    secondaryContainer = md_theme_dark_secondaryContainer,
    onSecondaryContainer = md_theme_dark_onSecondaryContainer,
    tertiary = md_theme_dark_tertiary,
    onTertiary = md_theme_dark_onTertiary,
    tertiaryContainer = md_theme_dark_tertiaryContainer,
    onTertiaryContainer = md_theme_dark_onTertiaryContainer,
    error = md_theme_dark_error,
    errorContainer = md_theme_dark_errorContainer,
    onError = md_theme_dark_onError,
    onErrorContainer = md_theme_dark_onErrorContainer,
    background = md_theme_dark_background,
    onBackground = md_theme_dark_onBackground,
    surface = md_theme_dark_surface,
    onSurface = md_theme_dark_onSurface,
    surfaceVariant = md_theme_dark_surfaceVariant,
    onSurfaceVariant = md_theme_dark_onSurfaceVariant,
    outline = md_theme_dark_outline,
    inverseOnSurface = md_theme_dark_inverseOnSurface,
    inverseSurface = md_theme_dark_inverseSurface,
    inversePrimary = md_theme_dark_inversePrimary,
    surfaceTint = md_theme_dark_surfaceTint,
    outlineVariant = md_theme_dark_outlineVariant,
    scrim = md_theme_dark_scrim,
)

@Composable
fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColors
        else -> LightColors
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            setUpEdgeToEdge(view, darkTheme)
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        shapes = Shapes,
        typography = Typography,
        content = content
    )
}

/**
 * Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
 * light or dark depending on whether the [darkTheme] is enabled or not.
 */
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
    val window = (view.context as Activity).window
    WindowCompat.setDecorFitsSystemWindows(window, false)
    window.statusBarColor = Color.Transparent.toArgb()
    val navigationBarColor = when {
        Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
        Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
        // Min sdk version for this app is 24, this block is for SDK versions 24 and 25
        else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
    }
    window.navigationBarColor = navigationBarColor
    val controller = WindowCompat.getInsetsController(window, view)
    controller.isAppearanceLightStatusBars = !darkTheme
    controller.isAppearanceLightNavigationBars = !darkTheme
}

WoofTheme() 中,colorScheme val 使用了 when 语句

  • 如果 dynamicColor 为 true,并且 build 版本为 S 或更高,则检查设备是否采用 darkTheme
  • 如果采用深色主题,colorScheme 会设为 dynamicDarkColorScheme
  • 如果没有采用深色主题,则会设为 dynamicLightColorScheme
  • 如果应用未使用 dynamicColorScheme,则检查该应用是否采用 darkTheme。如果采用,那么 colorScheme 会设为 DarkColors
  • 如果这两种情况都不是,则 colorScheme 会设为 LightColors

复制进来的 Theme.kt 文件将 dynamicColor 设为了 false,而我们在使用的设备采用了浅色模式,因此 colorScheme 将设为 LightColors

复制代码
val colorScheme = when {
       dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
           val context = LocalContext.current
           if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
       }

       darkTheme -> DarkColors
       else -> LightColors
   }

重新运行应用,请注意,应用栏已自动更改颜色。

颜色映射

Material 组件会自动映射到颜色槽。整个界面中的其他关键组件(例如悬浮操作按钮)也默认为主色。也就是说,您无需为组件明确分配颜色;当您在应用中设置颜色主题时,它会自动映射到颜色槽。您可以通过在代码中明确设置颜色来覆盖此设置。您可以点击此处,详细了解颜色角色。

在本部分中,我们将使用 Card 来封装包含 DogIcon()DogInformation()Row,以区分列表项的颜色与背景。

DogItem() 可组合函数中,使用 Card() 封装 Row()

复制代码
Card() {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}

由于 Card 现在是 DogItem() 中的第一个子级可组合项,因此请将 DogItem() 中的修饰符传递给 Card,并将 Row 的修饰符更新为 Modifier 的一个新实例。

复制代码
Card(modifier = modifier) {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}

您可以查看 WoofPreview()。由于使用了 Card 可组合项,列表项现在已经自动更改了颜色。颜色看上去非常棒,但列表项之间没有间距。

Dimens 文件

就像在应用中使用 strings.xml 存储字符串一样,使用名为 dimens.xml 的文件存储尺寸值也不失为一个好做法。这样做可以避免对值进行硬编码;就算需要,您也可以在一个地方集中做出更改。

依次点击 app > res > values > dimens.xml ,然后查看该文件。这个文件存储了 padding_smallpadding_mediumimage_size 的尺寸值,而这些尺寸在整个应用中都会用到。

复制代码
<resources>
   <dimen name="padding_small">8dp</dimen>
   <dimen name="padding_medium">16dp</dimen>
   <dimen name="image_size">64dp</dimen>
</resources>

如需从 dimens.xml 文件中添加值,请使用以下正确格式:

例如,要添加 padding_small,您可以传入 dimensionResource(id = R.dimen.padding_small )

WoofApp() 中,在对 DogItem() 的调用中添加带有 padding_smallmodifier

复制代码
@Composable
fun WoofApp() {
    Scaffold { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}

WoofPreview() 中,列表项被更清晰地分隔开来。

深色主题

在 Android 系统中,可以选择将设备切换为深色主题。深色主题使用更暗、更柔和的颜色,并且:

  • 可以大幅减少耗电量(具体取决于设备的屏幕技术)。
  • 为弱视以及对强光敏感的用户提高可视性。
  • 让所有人都可以在光线较暗的环境中更轻松地使用设备。

您的应用可以选择启用 Force Dark,这意味着系统会为您实现深色主题。不过,如果您实现深色主题,可为用户提供更好的体验,以便您继续完全控制应用主题。

在选择自己的深色主题时,请务必注意,深色主题的颜色必须符合无障碍功能对比度标准。深色主题使用较深的界面颜色,且色彩强度有限。

在预览中查看深色主题

您在上一步中已经添加了深色主题的颜色。如需查看深色主题的实际效果,您需要向 MainActivity.kt 添加另一个 Preview 可组合项。这样,当您更改代码中的界面布局时,就能同时看到浅色主题和深色主题的预览效果。

WoofPreview() 下,新建一个名为 WoofDarkThemePreview() 的函数,并为其添加 @Preview@Composable 注解。

复制代码
@Preview
@Composable
fun WoofDarkThemePreview() {

}

DarkThemePreview() 内,添加 WoofTheme()。如果不添加 WoofTheme(),您将看不到我们在应用中添加的任何样式。将 darkTheme 参数设置为 true

复制代码
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {

   }
}

WoofTheme() 内调用 WoofApp()

复制代码
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {
       WoofApp()
   }
}

现在,在 Design 窗格中向下滚动,您会看到采用深色主题的应用,包括深色的应用/列表项背景和浅色的文本。您可以比较深色主题与浅色主题之间的差异。

|---|
| |
| |

在设备或模拟器上查看深色主题

如需在模拟器或实体设备上以深色主题背景查看应用,请执行以下操作:

  1. 打开设备的设置应用。
  2. 搜索深色主题,然后点击进入该主题。
  3. 开启深色主题
  4. 重新打开 Woof 应用后,该应用将采用深色主题

此 Codelab 将重点介绍浅色主题,因此,请在继续设置此应用之前关闭深色主题。

  1. 打开设备的设置应用。
  2. 选择显示
  3. 关闭深色主题

比较应用在本部分开始时的外观与现在的外观。列表项和文本的定义更明确,配色方案更具视觉吸引力。

动态配色

Material 3 非常注重用户体验,其中推出的动态配色这项新功能就能根据用户的壁纸为应用创建主题。这样一来,如果用户喜欢绿色且拥有蓝色的手机背景,Woof 应用也会据此呈现蓝色。动态主题仅适用于搭载 Android 12 及更高版本的特定设备。

自定义主题可用于具有鲜明品牌颜色的应用,并且还需要在不支持动态主题的设备上实现,以确保应用仍然能够呈现相应主题。

  1. 若要启用动态配色,请打开 Theme.kt 并前往 WoofTheme() 可组合项,然后将 dynamicColor 参数设为 true

    @Composable
    fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
    )

  2. 若要更改设备或模拟器的背景,请打开设置 ,然后搜索壁纸

  3. 将壁纸更改为某一种或一组颜色。

  4. 重新运行应用即可查看动态主题(注意,设备或模拟器必须搭载 Android 12 或更高版本,您才能看到动态配色)。请随意尝试不同的壁纸来体验吧!

  1. 此 Codelab 重点介绍的是自定义主题,因此请在继续操作之前停用 dynamicColor

    @Composable
    fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
    )

添加形状

应用形状会给可组合项的外观和风格带来很大的变化。形状能够引导用户注意力、区别组件、传达状态以及呈现品牌风格。

许多形状都是使用 RoundedCornerShape 定义的,它所描述的是圆角矩形。传入的数字会定义角的圆度。如果使用 RoundedCornerShape(0.dp),则矩形没有圆角;如果使用 RoundedCornerShape(50.dp),角将变为完全圆形。

您还可以为每个角添加不同的圆角百分比,进一步自定义形状。尝试各种形状真是太有意思啦!

Shape.kt 文件用于定义 Compose 中组件的形状。组件分为三种类型:小、中和大。在本部分中,您将修改定义为 medium 大小的 Card 组件。系统会根据组件的大小将组件分组为形状类别

在此部分中,您要将狗狗的图片设置为圆形,并修改列表项的形状。

将狗狗图片的形状设为圆形

打开 Shape.kt 文件,您会发现 small 参数已设为 RoundedCornerShape(50.dp)。这个设置将用来让图片的形状变为圆形。

复制代码
val Shapes = Shapes(
   small = RoundedCornerShape(50.dp),
)

打开 MainActivity.kt 。在 DogIcon() 中,将 clip 属性添加到 Imagemodifier;这会将图片裁剪为某种形状。传入 MaterialTheme.shapes.small

复制代码
import androidx.compose.ui.draw.clip

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),

查看 WoofPreview() 时,您会注意到狗狗图标已变为圆形!不过,有些照片的侧边会被截断,而不是显示为完整的圆形。

若要将所有照片设为圆形,请添加 ContentScaleCrop 属性,这会根据显示大小裁剪图片。请注意,contentScaleImage 的一个属性,不是 modifier 的一部分。

复制代码
import androidx.compose.ui.layout.ContentScale

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),
       contentScale = ContentScale.Crop,

以下是完整的 DogIcon() 可组合项。

复制代码
@Composable
fun DogIcon(
    @DrawableRes dogIcon: Int,
    modifier: Modifier = Modifier
) {
    Image(
        modifier = modifier
            .size(dimensionResource(R.dimen.image_size))
            .padding(dimensionResource(R.dimen.padding_small))
            .clip(MaterialTheme.shapes.small),
        contentScale = ContentScale.Crop,
        painter = painterResource(dogIcon),

        // Content Description is not needed here - image is decorative, and setting a null content
        // description allows accessibility services to skip this element during navigation.

        contentDescription = null
    )
}

现在,WoofPreview() 中的图标是圆形。

向列表项添加形状

在此部分中,您将向列表项添加形状。列表项已通过 Card 显示。Card 是可以包含一个可组合项并包含装饰选项的 Surface,可通过边框、形状等添加装饰。在本部分中,您将使用 Card 向列表项添加形状。

打开 Shape.kt 文件。Card 是中等大小的组件,因此您要添加 Shapes 对象的 medium 参数。对于此应用,列表项的右上角和左下角要设为圆角,但又不是设为完全圆形。为此,请将 16.dp 传递给 medium 属性。

复制代码
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)

由于 Card 已默认使用中等形状,因此您无需明确地将其设为中型。查看预览 ,看看采用新形状的 Card

如果您返回到 WoofTheme() 中的 Theme.kt 文件并查看 MaterialTheme(),会看到 shapes 属性设置为您刚刚更新的 Shapes val

复制代码
MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

下图并排展示了列表项在调整形状前后的效果。请注意,如果添加形状,应用的外观会更具吸引力。

添加排版

Material Design 字型比例

字型比例是一系列字体样式的选择,可在应用中使用,确保样式既灵活又一致。Material Design 字型比例包含字型系统支持的 15 种字体样式。命名和分组已简化为:显示、大标题、标题、正文和标签,每个都有大号、中号和小号。只有在您想自定义应用时,才需要使用这些选项。如果您不知道为每个字型比例类别设置什么,请注意,您可以使用默认的排版缩放设置。

字型比例包含可重复使用的文本类别,每个类别都有预期的应用和含义。

显示

作为屏幕上最大的文本,显示字体专用于简短的重要文字或数字,最适合在大屏幕设备上使用。

大标题

大标题最适合用来在小屏幕设备上显示高强调度的简短文本。这类样式有助于标记文本的主要段落或重要内容区域。

标题

标题比大标题样式要小,适用于内容相对较短的中强调度文本。

Body

正文样式用于显示应用中较长的文本段落。

标签

标签样式是较小的实用样式,用于显示组件内部的文本或内容正文中非常小的文本(例如字幕)。

字体

虽然 Android 平台提供了各种字体,但您可能需要使用非默认提供的字体来自定义应用。自定义字体可以增添个性,并可用于品牌塑造。

在本部分中,您将添加名为 Abril FatfaceMontserrat BoldMontserrat Regular 的自定义字体。您将使用 displayLarge 和 displayMedium 大标题以及 Material 字型系统中的 bodyLarge 文本,然后将其添加到应用中的文本。

创建字体 Android 资源目录。

在向应用添加字体之前,您需要添加一个字体目录。

  1. 在 Android Studio 的项目视图中,右键点击 res 文件夹。
  2. 依次选择 New > Android Resource Directory

1.将目录命名为 font ,将资源类型设为 font ,然后点击 OK

2.打开位于 res > font 的新字体资源目录。

下载自定义字体

由于您使用的不是 Android 平台提供的字体,因此您需要下载自定义字体。

  1. 访问 https://fonts.google.com/
  2. 搜索 Montserrat,然后点击 Download family
  3. 解压缩该 ZIP 文件。
  4. 打开下载的 Montserrat 文件夹。在 static 文件夹中,找到 Montserrat-Bold.ttfMontserrat-Regular.ttfttf 代表 TrueType 字体,即字体文件的格式)。选择两种字体,将它们拖动到 Android Studio 中项目的字体资源目录中。

5.在字体文件夹中将 Montserrat-Bold.ttf 重命名为 montserrat_bold.ttf ,并将Montserrat-Regular.ttf 重命名为 montserrat_regular.ttf

6.搜索 Abril Fatface,然后点击 Download family

7.打开下载的 Abril_Fatface 文件夹。选择 AbrilFatface-Regular.ttf 并将其拖动到字体资源目录中。

8.在字体文件夹中,将 Abril_Fatface_Regular.ttf 重命名为 abril_fatface_regular.ttf

项目中的字体资源目录和三个自定义字体文件应如下所示:

初始化字体

在项目窗口中,依次打开 ui.theme > Type.kt 。在 import 语句下方和 Typography val 上方初始化下载的字体。首先,初始化 Abril Fatface ,方法是将其设为 FontFamily 并使用字体文件 abril_fatface_regular 传入 Font

复制代码
​​import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import com.example.woof.R

val AbrilFatface = FontFamily(
   Font(R.font.abril_fatface_regular)
)

Abril Fatface 下方初始化 Montserrat ,方法是将其设为 FontFamily 并使用字体文件 montserrat_regular 传入 Font。对于 montserrat_bold,还应添加 FontWeight.Bold。即使您传入了字体文件的粗体版本,Compose 也不知道该文件是粗体文件,因此您需要明确地将此文件关联到 FontWeight.Bold

复制代码
import androidx.compose.ui.text.font.FontWeight

val AbrilFatface = FontFamily(
   Font(R.font.abril_fatface_regular)
)

val Montserrat = FontFamily(
   Font(R.font.montserrat_regular),
   Font(R.font.montserrat_bold, FontWeight.Bold)
)

接下来,将不同类型的标题设为您刚刚添加的字体。Typography 对象具有上面讨论的 13 种不同字体的参数。您可以根据需要定义任意数量。在此应用中,我们将设置 displayLargedisplayMediumbodyLarge。在此应用的下一部分中,您将使用 labelSmall,因此需在此处添加。

下表显示了您添加的每个大标题的字体、粗细和大小。

对于 displayLarge 属性,应将其设为 TextStyle,并使用上表中的信息填写 fontFamilyfontWeightfontSize。这意味着所有设为 displayLarge 的文本都将使用 Abril Fatface 作为字体,字体粗细正常,fontSize36.sp

displayMediumlabelSmallbodyLarge 重复此过程。

复制代码
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp


val Typography = Typography(
   displayLarge = TextStyle(
       fontFamily = AbrilFatface,
       fontWeight = FontWeight.Normal,
       fontSize = 36.sp
   ),
   displayMedium = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Bold,
       fontSize = 20.sp
   ),
   labelSmall = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Bold,
       fontSize = 14.sp
   ),
   bodyLarge = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Normal,
       fontSize = 14.sp
   )
)

如果您打开 WoofTheme() 中的 Theme.kt 文件并查看 MaterialTheme(),会发现 typography 参数就等于您刚刚更新的 Typography val

复制代码
MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

向应用文本添加排版

现在,您要为应用中的每一个文本实例添加大标题字型。

添加 displayMedium 作为 dogName 的样式,因为它是一种简短的重要信息。将 bodyLarge 添加为 dogAge 的样式,因为它适合较小的文本。

复制代码
@Composable
fun DogInformation(
   @StringRes dogName: Int,
   dogAge: Int,
   modifier: Modifier = Modifier
) {
   Column(modifier = modifier) {
       Text(
           text = stringResource(dogName),
           style = MaterialTheme.typography.displayMedium,
           modifier = Modifier.padding(top = dimensionResource(id = R.dimen.padding_small))
       )
       Text(
           text = stringResource(R.string.years_old, dogAge),
           style = MaterialTheme.typography.bodyLarge
       )
   }
}

现在,在 WoofPreview() 中,狗狗的名字会使用 20.sp 的粗体 Montserrat 字体显示,狗狗的年龄会使用 14.sp 的正常 Montserrat 字体显示。

下面并排展示了添加排版前后的列表项。请注意狗狗的名字和年龄之间的字体差异。

|----------------------------------------------------------------------------|
| |
| |

添加顶部栏

Scaffold 是一种布局,可为各种组件和屏幕元素(如 ImageRowColumn)提供槽。Scaffold 还为 TopAppBar 提供了槽,您将在本部分中使用。

TopAppBar 可用于许多用途,但在本例中,您会将其用于品牌宣传以及彰显应用个性。TopAppBar 有四种不同的类型:居中、小、中和大。在此 Codelab 中,您将实现一个居中的顶部应用栏。您将创建一个类似于以下屏幕截图的可组合项,并将其放入 ScaffoldtopBar 部分。

对于此应用,顶部栏由一个包含徽标图片和应用名称文字的 Row 组成。徽标包含可爱的渐变色的爪子和应用名称!

向顶部栏添加图片和文字

MainActivity.kt 中,创建一个名为 WoofTopAppBar() 且带有可选 modifier 的可组合项。

复制代码
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
  
}

Scaffold支持 contentWindowInsets 参数,该参数有助于为 Scaffold 内容指定边衬区。WindowInsets 是屏幕上应用可与系统界面相交的部分,应通过 PaddingValues 参数将其传递给内容槽。了解详情

contentWindowInsets 值会作为 contentPadding 传递给 LazyColumn

复制代码
@Composable
fun WoofApp() {
    Scaffold { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}
  1. Scaffold 中,添加 topBar 属性并将其设置为 WoofTopAppBar()

    Scaffold(
    topBar = {
    WoofTopAppBar()
    }
    )

WoofApp() 可组合项将如下所示:

复制代码
@Composable
fun WoofApp() {
    Scaffold(
        topBar = {
            WoofTopAppBar()
        }
    ) { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}

WoofPreview() 没有发生任何变化,因为 WoofTopAppBar() 中没有内容。让我们来修改一下!

WoofTopAppBar() Composable 中,添加 CenterAlignedTopAppBar() 并将修饰符参数设置为传入 WoofTopAppBar() 的修饰符。

复制代码
import androidx.compose.material3.CenterAlignedTopAppBar

@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       modifier = modifier
   )
}

针对 title 参数,传入一个 Row 来存放 CenterAlignedTopAppBarImageText

复制代码
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier){
   CenterAlignedTopAppBar(
       title = {
           Row() {
              
           }
       },
       modifier = modifier
   )
}

将徽标 Image 添加到 Row 中。

  • modifier 中的图片大小设为 dimens.xml 文件中的 image_size,并将内边距设为 dimens.xml 文件中的 padding_small

  • 使用 painterImage 设为可绘制对象文件夹中的 ic_woof_logo

  • contentDescription 设为 null。在这种情况下,应用徽标不会为有视觉障碍的用户添加任何语义信息,因此我们无需添加内容说明。

    Row() {
    Image(
    modifier = Modifier
    .size(dimensionResource(id = R.dimen.image_size))
    .padding(dimensionResource(id = R.dimen.padding_small)),
    painter = painterResource(R.drawable.ic_woof_logo),
    contentDescription = null
    )
    }

接下来,在 Row 中的 Image. 后添加一个 Text 可组合项。

  • 使用 stringResource() 将其设为 app_name 的值。这会将文本设为存储在 strings.xml 中的应用的名称。

  • 将文本样式设为 displayLarge,因为应用名称是简短的重要文本。

    Text(
    text = stringResource(R.string.app_name),
    style = MaterialTheme.typography.displayLarge
    )

这就是 WoofPreview() 中显示的内容,看起来有点不对劲,因为图标和文本没有垂直对齐。

若要解决此问题,请向 Row 添加 verticalAlignment 值参数,并将其设为 Alignment.CenterVertically

复制代码
import androidx.compose.ui.Alignment

Row(
   verticalAlignment = Alignment.CenterVertically
)

这样看起来好多了!

以下是完整的 WoofTopAppBar() 可组合项:

复制代码
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       title = {
           Row(
               verticalAlignment = Alignment.CenterVertically
           ) {
               Image(
                   modifier = Modifier
                       .size(dimensionResource(id = R.dimen.image_size))
                       .padding(dimensionResource(id = R.dimen.padding_small)),
                   painter = painterResource(R.drawable.ic_woof_logo),

                   contentDescription = null
               )
               Text(
                   text = stringResource(R.string.app_name),
                   style = MaterialTheme.typography.displayLarge
               )
           }
       },
       modifier = modifier
   )
}

运行应用,欣赏 TopAppBar 将该应用关联在一起是多么美观。

现在,来看看采用深色主题的最终应用吧!

利用JetPack Compose创建简单动画

添加展开图标

图标

图标是一种符号,可以形象地表示预期功能,帮助用户了解界面。图标设计通常会从用户在现实世界中可能接触到的对象中汲取灵感。图标设计通常对细节设计没有什么要求,只需确保用户熟悉即可。例如,在现实世界中,我们使用铅笔书写,因此铅笔图标通常表示创建修改

Material Design 提供了大量图标,分成若干常见类别,可满足您的大多数需求。

添加 Gradle 依赖项

为您的项目添加 material-icons-extended 库依赖项。您将使用此库中的Icons.Filled.ExpandLess Icons.Filled.ExpandMore 图标。

有关 Gradle 依赖项的注意事项: 如需向您的项目添加依赖项,请在模块的 build.gradle.kts 文件的 dependencies 代码块中指定依赖项配置,例如"implementation"。在构建您的应用时,构建系统会编译该库模块,并将生成的编译内容打包到应用中。

在后面的单元中,您将详细了解如何为应用添加库。

  1. Project 窗格中,依次打开 app > build.gradle.kts (Module :app)

  2. 滚动到 build.gradle.kts (Module :app) 文件的末尾。在 dependencies{} 代码块中,添加以下行:

    implementation("androidx.compose.material:material-icons-extended")

提示 :每当您修改 Gradle 文件时,Android Studio 可能需要导入或更新库,并运行一些后台任务。Android Studio 会显示一个弹出式窗口,让您同步项目。点击 Sync Now

添加图标可组合项

添加一个函数,以显示 Material 图标库中的展开图标,并将其用作按钮。

  1. MainActivity.kt 中的 DogItem() 函数后面,创建一个名为 DogItemButton() 的全新可组合函数。

  2. 传入针对展开状态的 Boolean、针对 onClick 处理程序的 lambda 表达式和可选的 Modifier,如下所示:

    复制代码
    @Composable
    private fun DogItemButton(
       expanded: Boolean,
       onClick: () -> Unit,
       modifier: Modifier = Modifier
    ) {
     
    
    }
  3. DogItemButton() 函数内,添加一个 IconButton() 可组合项,该可组合项接受 onClick 具名形参(一个使用尾随 lambda 语法的 lambda,会在按下此图标时调用),以及一个可选的 modifier。将 IconButton's onClickmodifier value parameters 设置为等于传入 DogItemButton 的函数。

    复制代码
    @Composable
    private fun DogItemButton(
       expanded: Boolean,
       onClick: () -> Unit,
       modifier: Modifier = Modifier
    ){
       IconButton(
           onClick = onClick,
           modifier = modifier
       ) {
    
       }
    }
  4. IconButton() lambda 代码块内,添加一个 Icon 可组合项,并将 imageVector value-parameter 设置为 Icons.Filled.ExpandMore。这是将在列表项 末尾显示的按钮。Android Studio 会向您显示针对 Icon() 可组合项形参的警告,您将在下一步中修复相应问题。

    复制代码
    import androidx.compose.material.icons.filled.ExpandMore
    import androidx.compose.material.icons.Icons
    import androidx.compose.material3.Icon
    import androidx.compose.material3.IconButton
    
    IconButton(
       onClick = onClick,
       modifier = modifier
    ) {
       Icon(
           imageVector = Icons.Filled.ExpandMore
       )
    }
  5. 添加值形参 tint,并将图标的颜色设为 MaterialTheme.colorScheme.secondary。添加具名形参 contentDescription,并将其设置为字符串资源 R.string.expand_button_content_description

复制代码
IconButton(
   onClick = onClick,
   modifier = modifier
){
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       contentDescription = stringResource(R.string.expand_button_content_description),
       tint = MaterialTheme.colorScheme.secondary
   )
}

显示图标

通过将 DogItemButton() 可组合项添加到布局中来显示它。

  1. DogItem() 的开头,添加 var 以保存列表项的展开状态。将初始值设置为 false

    复制代码
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    
    var expanded by remember { mutableStateOf(false) }
  2. 在列表项中显示图标按钮。在 DogItem() 可组合项的 Row 代码块的末尾,调用 DogInformation() 之后添加 DogItemButton()。针对回调传入 expanded 状态和空 lambda。您将在后续步骤中定义 onClick 操作。

    复制代码
    Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
    ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
    }
  3. Design 窗格中查看 WoofPreview()

回顾 remember() 和 mutableStateOf()

使用 mutableStateOf() 函数,以便 Compose 观察状态值发生的更改,并触发重组来更新界面。使用 remember() 函数封装 mutableStateOf() 函数调用,以在初始组合期间将值存储在组合中,并在重组期间返回存储的值。

请注意,"展开"按钮未与列表项的末尾对齐。您将在下一步中修复该问题。

对齐"展开"按钮

如要将"展开"按钮与列表项的末尾对齐,您需要使用 Modifier.weight() 属性在布局中添加分隔符。

注意: Modifier.weight() 会根据界面元素相对于其加权同级元素(相应行或列中的其他子元素)的权重,按比例设置界面元素的宽度和高度。

示例:假设一行中的三个子元素的权重分别为 1f、1f 和 2f。在本例中,系统为所有子元素都分配了权重。根据指定的权重值按比例划分可用的行空间,为权重值较高的子元素分配更多空间。子元素的权重分配情况如下所示:

在上面的行中,系统为第一个和第二个子级可组合项分配了行宽的 ¼,而为第三个子级可组合项分配了行宽的 ½。

如果系统没有为子级可组合项分配权重(权重为可选形参),那么子级可组合项的高度/宽度将默认为封装内容(封装界面元素中的内容)。

有关浮点值的注意事项 :Kotlin 中的浮点值为十进制数,以在数字末尾附上 f 或 F 表示。

Woof 应用中,每个列表项行都包含狗狗图片、狗狗信息和一个"展开"按钮。您需要在"展开"按钮前添加一个 Spacer 可组合项(权重为 1f),以便与按钮图标适当对齐。由于分隔符是行中唯一加权的子元素,因此该元素会在测量其他未加权子元素的宽度之后,填充行中的剩余空间。

在列表项行中添加分隔符

1.在 DogItem() 中的 DogInformation()DogItemButton() 之间,添加 Spacer。传入带 weight(1f)ModifierModifier.weight() 会使分隔符填充该行中剩余的空间。

Kotlin 复制代码
import androidx.compose.foundation.layout.Spacer

Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(modifier = Modifier.weight(1f))
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}

2.在 Design 窗格中查看 WoofPreview()。请注意,"展开"按钮现在已与列表项的末尾对齐

添加可组合项以显示狗狗爱好

在此任务中,您将添加 Text 可组合项以显示狗狗的爱好信息。

1.创建一个新的名为 DogHobby() 的可组合函数,用于接受狗狗的爱好字符串资源 ID 和可选的 Modifier

Kotlin 复制代码
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}

2.在 DogHobby() 函数内,创建一个 Column 并传入在 DogHobby() 中传入的修饰符。

Kotlin 复制代码
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 

   }
}

3.在 Column 代码块内,添加两个 Text 可组合项:一个用于在爱好信息上方显示 About 文本,另一个用于显示爱好信息。

将第一个可组合项的 text 设置为 strings.xml 文件中的 about,并将 style 设置为 labelSmall。将第二可组合项的 text 设置为传入的 dogHobby,并将 style 设置为 bodyLarge

Kotlin 复制代码
Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}

4.在 DogItem() 中,DogHobby() 可组合项将位于包含 DogIcon()DogInformation()Spacer()DogItemButton()Row 下方。为此,请使用 Column 封装 Row,以便将爱好添加到 Row 下方。

Kotlin 复制代码
Column() {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       Spacer(modifier = Modifier.weight(1f))
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
   }
}

5.将 DogHobby() 添加在 Row 之后作为 Column 的第二个子级。传入 dog.hobbies,其中包含传入的狗狗的独特爱好,以及包含 DogHobby() 可组合项内边距的 modifier

Kotlin 复制代码
Column() {
   Row() {
      ...
   }
   DogHobby(
       dog.hobbies,
       modifier = Modifier.padding(
           start = dimensionResource(R.dimen.padding_medium),
           top = dimensionResource(R.dimen.padding_small),
           end = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_medium)
       )
   )
}

有关狗狗爱好的注意事项: 所有狗狗的爱好信息均已在起始代码中提供给您。如需在代码中查看此类信息,请打开 data/Dog.kt 文件,查看预先填充了狗狗信息的 dogs 列表。

完整的 DogItem() 函数应如下所示:

Kotlin 复制代码
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       modifier = modifier
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(dimensionResource(R.dimen.padding_small))
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { /*TODO*/ },
               )
           }
           DogHobby(
               dog.hobbies, 
               modifier = Modifier.padding(
                   start = dimensionResource(R.dimen.padding_medium),
                   top = dimensionResource(R.dimen.padding_small),
                   end = dimensionResource(R.dimen.padding_medium),
                   bottom = dimensionResource(R.dimen.padding_medium)
               )
           )
       }
   }
}

6.在 Design 窗格中查看 WoofPreview()。请注意,狗狗的爱好信息会显示。

点击按钮时显示和隐藏狗狗爱好

您的应用为每个列表项都提供了展开按钮,但该按钮还没有什么作用!在本部分中,您将添加用于在用户点击"展开"按钮时隐藏或显示爱好信息的选项。

1.在 DogItem() 可组合函数的 DogItemButton() 函数调用中,定义 onClick() lambda 表达式,在用户点击按钮时将 expanded 布尔状态值更改为 true,在用户再次点击按钮时将其改回 false

Kotlin 复制代码
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)

注意: 逻辑 NOT 运算符 ( ! ) 会返回 Boolean 表达式的否定值。

例如,如果 expandedtrue,则 !expanded 的求值结果为 false

2.在 DogItem() 函数中,使用对 expanded 布尔值进行的 if 检查来封装 DogHobby() 函数调用。

Kotlin 复制代码
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       ...
   ) {
       Column(
           ...
       ) {
           Row(
               ...
           ) {
               ...
           }
           if (expanded) {
               DogHobby(
                   dog.hobbies, modifier = Modifier.padding(
                       start = dimensionResource(R.dimen.padding_medium),
                       top = dimensionResource(R.dimen.padding_small),
                       end = dimensionResource(R.dimen.padding_medium),
                       bottom = dimensionResource(R.dimen.padding_medium)
                   )
               )
           }
       }
   }
}

现在,只有当 expanded 的值为 true 时,系统才会显示狗狗的爱好信息。

3.预览会显示界面的外观,您还可以与之互动。如要与界面预览对象进行互动,请将鼠标悬停在 Design 窗格中的 WoofPreview 文本上,然后点击 Design 窗格右上角的 Interactive Mode 按钮 。这会在交互模式下启动预览。

4.点击"展开"按钮与预览对象进行互动。请注意,点击"展开"按钮后,系统会隐藏或显示狗狗的爱好信息。

请注意,在列表项展开时,"展开"按钮图标没有发生变化。为了提供更加出色的用户体验,您将更改图标,使 ExpandMore 显示向下箭头 ,ExpandLess 显示向上箭头 。

5.在 DogItemButton() 函数中,添加一个根据 expanded 状态更新 imageVector 值的 if 语句,如下所示:

Kotlin 复制代码
import androidx.compose.material.icons.filled.ExpandLess


@Composable
private fun DogItemButton(
   ...
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           ...
       )
   }
}

请注意您在上一个代码段中是如何编写 if-else 的。

if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore

这与以下代码中使用大括号 { } 一样:

if (expanded) {

复制代码
`Icons.Filled.ExpandLess`

} else {

复制代码
`Icons.Filled.ExpandMore`

}

如果 if-else 语句只有一行代码,那么大括号是可选的。

6.在设备或模拟器上运行应用,或再次使用预览中的交互模式。请注意,图标会在 ExpandMoreExpandLess 之间切换。

有关交互模式的注意事项:在交互模式下,您可以采用与在设备上互动的相同方式与预览对象互动。然而,使用预览模式无法替代在设备上运行应用来进行测试。

添加动画

您可以通过动画添加视觉提示,通知用户应用中的动态。当界面状态发生改变时(例如有新内容加载或有新操作可用时),动画尤其有用。动画还可以为您的应用添加精美外观。

在本部分中,您将添加一个弹簧动画,以动画形式呈现列表项高度的变化。

弹簧动画

弹簧动画是一种基于物理特性的动画,依靠弹簧弹力来驱动。使用弹簧动画时,移动的值和速度是根据应用的弹簧弹力计算得出的。

例如,如果您在屏幕上拖动某个应用图标,然后松开手指释放它,那么该图标便会被一股看不见的力量拉回其原始位置。

下图演示了弹簧动画效果。手指从图标上松开后,图标会弹回,就像弹簧弹跳一样。

弹簧效果

弹簧弹力基于以下两个属性:

  • 阻尼比:弹簧的弹力。
  • 刚度:弹簧的刚度,即弹簧移动到终点的速度。

以下是一些具有不同阻尼比和刚度的动画示例。
高弹力 无弹力 高刚度 很低的刚度

查看 DogItem() 可组合函数中的 DogHobby() 函数调用。狗狗的爱好信息基于 expanded 布尔值包含在组合中。根据狗狗的爱好信息处于可见状态还是隐藏状态,列表项的高度会发生变化。目前,该过渡过程非常流畅。在此部分中,您将使用 animateContentSize 修饰符在展开状态和非展开状态之间更流畅地进行过渡。

Kotlin 复制代码
// No need to copy over
@Composable
fun DogItem(...) {
  ...
    if (expanded) {
       DogHobby(
          dog.hobbies, 
          modifier = Modifier.padding(
              start = dimensionResource(R.dimen.padding_medium),
              top = dimensionResource(R.dimen.padding_small),
              end = dimensionResource(R.dimen.padding_medium),
              bottom = dimensionResource(R.dimen.padding_medium)
          )
      )
   }
}

1.在 MainActivity.ktDogItem() 内,向 Column 布局添加 modifier 形参。

Kotlin 复制代码
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}

2.使用 animateContentSize 修饰符链接该修饰符,以便为大小(列表项高度)变化添加动画效果。

Kotlin 复制代码
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

在当前实现中,您要为应用中的列表项高度变化添加动画效果。但是,动画效果非常细微,在运行应用时很难觉察出来。为解决此问题,请使用可选的 animationSpec 形参来自定义动画。

3.对于 Woof 而言,动画会缓入和缓出,不会有弹跳。为此,请将 animationSpec 形参添加到 animateContentSize() 函数调用中。使用 DampingRatioNoBouncy 将其设置为弹簧动画,使其无弹跳,并使用 StiffnessMedium 形参让弹簧略硬。

Kotlin 复制代码
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioNoBouncy,
               stiffness = Spring.StiffnessMedium
           )
       )
)

4.查看 Design 窗格中的 WoofPreview(),然后使用交互模式或者在模拟器或设备上运行应用,以查看实际的弹簧动画效果。

测试无障碍功能

视力受损、色盲、听力受损、精细动作失能的人、以及有认知障碍和许多其他残疾人可以使用android设备处理他们日常生活中的各种事务。如果我们在开发应用的过程中考虑无障碍功能,那么我们便可以改善用户体验。

这一部分我们将使用TalkBack开关控制来测试Woof应用的无障碍功能。

TalkBack让用户可以无需使用双眼即可控制自己的设备

借助开关控制,用户可以使用开关(而不是触摸屏)在应用中导航

相关推荐
小镇学者2 小时前
【PHP】导入excel 报错Trying to access array offset on value of type int
android·php·excel
一笑的小酒馆5 小时前
Android11 Launcher3去掉抽屉改为单层
android
flushddd6 小时前
GOOUUU ESP32-S3-CAM 果云科技开发板开发指南(一)(超详细!)Vscode+espidf 通过摄像头拍摄照片并存取到SD卡中,文末附源码
ide·vscode·编辑器·esp32
louisgeek7 小时前
Git 根据不同目录设置不同账号
android
qq_390934748 小时前
MySQL中的系统库(简介、performance_schema)
android·数据库·mysql
whysqwhw9 小时前
Kotlin Flow 实现响应式编程指南
android
二流小码农9 小时前
鸿蒙开发:一文了解桌面卡片
android·ios·harmonyos
每次的天空9 小时前
Android第十七次面试总结(Java数据结构)
android·java·面试
梁同学与Android9 小时前
Android --- Handler的用法,子线程中怎么切线程进行更新UI
android·handler·子线程更新ui·切换到主线程
Fastcv9 小时前
这TextView也太闪了,咋做的?
android