Android应用中的主题和样式


无论如何,每个Android开发人员都必须使用样式。 有人对他们感到自信,某人只有肤浅的知识,而这些知识往往使他们无法自行解决问题。


考虑到深色主题的发布,决定在内存中刷新与Android应用程序中的主题和样式有关的所有信息。


将讨论什么:


  • 考虑一下Android应用程序中主题和样式的基本概念,看看它们为我们提供了哪些机会;
  • 让我们使用Material Components创建一个简单的主题,并重新定义样式。
  • 让我们看看黑暗主题是如何工作的;
  • 提出使用样式的建议。

让我们从基础开始


在主题结构中,主题和样式具有共同的结构:


<style name="MyStyleOrTheme"> <item name="key">value</item> </style> 

要创建,请使用style标签。 每个样式都有一个名称,并存储key-value参数。


一切都很简单。 但是主题和样式之间有什么区别?


唯一的区别是我们如何使用它们。


主题


主题是一组适用于整个应用程序,活动或视图组件的参数。 它包含应用程序的基本颜色,用于呈现应用程序所有组件的样式以及各种设置。


示例主题:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!--Base colors--> <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorOnPrimary">@color/color_on_primary</item> <item name="colorOnError">@color/color_on_error</item> <!--Style attributes--> <item name="textAppearanceHeadline1">@style/TextAppearance.MyTheme.Headline1</item> <item name="bottomSheetDialogTheme">@style/ThemeOverlay.MyTheme.BottomSheetDialog</item> <item name="chipStyle">@style/Widget.MaterialComponents.Chip.Action</item> <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.FilledBox</item> <!--Params--> <item name="android:windowTranslucentStatus">true</item> <!-- ... --> </style> 

主题重新定义了应用程序的主要颜色( colorPrimarycolorSecondary ),文本的样式( textAppearanceHeadline1 )和一些标准应用程序组件,以及透明状态栏的选项。


为了使样式成为一个真实的主题,有必要从该主题的默认实现中继承(我们将在后面讨论继承)。


款式


样式是用于设置单个View组件样式的一组参数。


TextInputLayout的示例样式:


 <style name="Widget.MyApp.CustomTextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.FilledBox"> <item name="boxBackgroundMode">outline</item> <item name="boxStrokeColor">@color/color_primary</item> <item name="shapeAppearanceOverlay">@style/MyShapeAppearanceOverlay</item> </style> 

属性


属性是样式或主题键。 这些都是小砖块,可以用来建造所有东西:


 colorPrimary colorSecondary colorOnError boxBackgroundMod boxStrokeColor shapeAppearanceOverlay ... 

所有这些键都是标准属性。


我们可以创建自己的属性:


 <attr name="myFavoriteColor" format="color|reference" /> 

myFavoriteColor属性将指向一种颜色或指向一种颜色资源的链接。


在格式中,我们可以指定相当标准的值:


  • 颜色
  • 参考
  • 枚举
  • 分数
  • 尺寸
  • 布尔值
  • 标志
  • 飘浮
  • 整数

本质上,属性是接口 。 它必须在主题中实现:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- ... --> <item name="myFavoriteColor">@color/color_favorite</item> </style> 

现在我们可以参考它。 上诉的一般结构如下:



 1 —   ,     ; 2 — namespace   (   Material Components Library); 3 —   ,     (); 4 —  . 

好吧,最后,让我们更改例如字段的文本颜色:


 <androidx.appcompat.widget.AppCompatTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="?attr/myFavoriteColor"/> 

由于这些属性,我们可以添加将在主题内更改的任何种类的抽象。


继承主题和风格


与在OOP中一样,我们可以采用现有实现的功能。 有两种方法可以做到这一点:


  • 显式(显式)
  • 隐式(隐式)

对于显式继承,我们使用parent关键字指定parent


 <style name="SnackbarStyle" parent="Widget.MaterialComponents.Snackbar"> <!-- ... --> </style> 

对于隐式继承,我们使用dot-notation表示父代:


 <style name="SnackbarStyle.Green"> <!-- ... --> </style> 

这些方法的工作没有区别


我们经常会遇到类似的风格:


 <style name="Widget.MyApp.Snackbar" parent="Widget.MaterialComponents.Snackbar"> <!-- ... --> </style> 

样式似乎是通过双重继承创建的。 实际上并非如此。 禁止多重继承。 在这个定义中, 显式继承总是赢


也就是说,将创建一个名为Widget.MyApp.Snackbar的样式,该样式是Widget.MyApp.Snackbar的后代。


主题叠加


ThemeOverlay-这些是特殊的“轻量级”主题,可让您覆盖 View组件的主要主题属性。


我们不会举一个例子,但是以我们的应用程序为例。 设计师认为我们需要创建一个标准的登录字段,该字段的颜色将与主要样式不同。


对于主主题,输入字段如下所示:



看起来不错,但设计师坚持认为该场地为棕色。


好的,我们如何解决这个问题?


  • 覆盖样式?


    是的,我们可以重新定义样式并手动更改视图的主要颜色,但是为此,您将需要编写大量代码,并且有可能我们会忘记一些东西。

  • 写您对准则和自定义参数的看法?


    一个不错的选择,因此我们可以满足所有设计师的愿望,同时满足泵技术的要求,但这都是费力的,并且可能导致不必要的错误。

  • 覆盖主题的主要颜色?


    我们发现,对于所需的类型,只需更改主题中的colorPrimary 。 一个可行的选择,但是通过这种方式,我们将影响其余组件的外观,但是我们不需要它。


正确的解决方案是使用ThemeOverlay


创建ThemeOverlay并重新定义主题的主要颜色


 <style name="ThemeOverlay.MyApp.Login" parent="ThemeOverlay.MaterialComponents.TextInputEditText"> <item name="colorPrimary">@color/colorBrown</item> </style> 

接下来,我们在TextInputLayout使用特殊的android:theme标签指定它:


 <com.google.android.material.textfield.TextInputLayout android:theme="@style/ThemeOverlay.MyApp.Login" android:hint="Login" ... > <com.google.android.material.textfield.TextInputEditText ... /> </com.google.android.material.textfield.TextInputLayout> 

一切都按我们需要工作。



当然,问题来了-它如何在引擎盖下工作?


这个魔术使您可以ContextThemeWrapper 。 在LayoutInflater创建视图时,将创建一个上下文,其中将以当前主题为基础,并在其中定义我们在Overlay主题中指定的参数。


 ../LayoutInflater.java final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); 

同样,我们可以独立覆盖应用程序中的任何主题参数。


将主题和样式应用于View组件的顺序



  1. 主要优先级是标记文件。 如果在其中定义了参数,则所有相似的参数都将被忽略。


     <Button android:textColor="@color/colorRed" ... /> 

  2. 下一个优先级是视图样式:


     <Button style=“@Widget.MyApp.ButtonStyle" ... /> 

  3. 以下对组件使用预定义的样式:


     <style name="Theme.MyApp.Main" parent="Theme..."> <item name=“materialButtonStyle”>@Widget.MyApp.ButtonStyle</item> <!-- ... --> </style> 

  4. 如果未找到任何参数,则使用主题属性:


     <style name="Theme.MyApp.Main" parent="Theme..."> <item name=“colorPrimary”>@colorPrimary</item> <!-- ... --> </style> 


通常,这是您开始处理主题所需的全部知识。 现在,让我们快速浏览一下更新的材料组件设计库。


可能材料组件随我们一起


材料组件是在Google I / O 2018上引入的,并替代了设计支持库。


该库使我们有机会使用Material Design 2.0中的更新组件。 另外,其中出现了许多有趣的自定义选项。 所有这些使您可以编写明亮而独特的应用程序。




以下是一些新样式的应用示例: OwlReplyCrane


让我们继续练习


要创建主题,您需要继承基础主题:


 Theme.MaterialComponents Theme.MaterialComponents.NoActionBar Theme.MaterialComponents.Light Theme.MaterialComponents.Light.NoActionBar Theme.MaterialComponents.Light.DarkActionBar Theme.MaterialComponents.DayNight Theme.MaterialComponents.DayNight.NoActionBar Theme.MaterialComponents.DayNight.DarkActionBar 

它们都与AppCompat主题非常相似,但是具有其他属性和设置。


您可以在material.io上了解有关新属性的更多信息。


如果由于某种原因您现在不能切换到新主题,则可以使用Bridge主题。 它们从AppCompat主题继承,并具有所有新的Material Components属性。 您只需要添加Bridge后缀并使用所有功能,不必担心:


 <!-- ... --> Theme.MaterialComponents.Light.Bridge <!-- ... --> 

这是我们的主题:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorSecondaryVariant">@color/color_secondary_variant</item> <style> 

原色(商标色)的名称已更改:


 colorPrimary —       (   AppCompat); colorPrimaryVariant —    ( colorPrimaryDark  AppCompat); colorSecondary —        ( colorAccent  AppCompat); colorSecondaryVariant —   . 

有关颜色的更多信息,请访问material.io


我已经提到主题包含每个View组件的标准样式。 例如,对于Snackbar样式将被称为snackbarStyle checkbox样式,对于checkbox - checkboxStyle ,则所有内容将相似。 一个示例将一切放置在适当的位置:



创建您自己的样式并将其应用于主题:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- ... --> <item name="snackbarStyle">@style/Widget.MyApp.SnackbarStyle</item> </style> <style name="Widget.MyApp.SnackbarStyle" parent="Widget.MaterialComponents.Snackbar"> <!-- ... --> </style> 

重要的是要了解,当您在主题中重新定义样式时,该样式将应用于应用程序(活动)中此类型的所有视图。


如果要将样式仅应用于一个特定的视图,则需要在文件中使用带有标记的style标签:


  <com.google.android.material.button.MaterialButton style="@style/Widget.MyApp.SnackbarStyle" ... /> 

使我印象深刻的创新之一是ShapeAppearance 。 它使您可以更改主题中组件的形状!


每个View组件都属于某个组:


  • shapeAppearance 小组


  • shapeAppearance 中型组件


  • shapeAppearance 大型组件



正如我们从名称中可以理解的那样,以不同大小的视图分组。



实践检查:


 <style name="Theme.MyApp.Main" parent="Theme.MaterialComponents.Light.NoActionBar"> <!-- ... --> <item name="shapeAppearanceSmallComponent">@style/Widget.MyApp.SmallShapeAppearance</item> </style> <style name="Widget.MyApp.SmallShapeAppearance" parent=“ShapeAppearance.MaterialComponents.SmallComponent”> <item name="cornerFamilyTopLeft">rounded</item> <item name="cornerFamilyBottomRight">cut</item> <item name="cornerSizeTopLeft">20dp</item> <item name="cornerSizeBottomRight">15dp</item> <!--<item name="cornerFamily">cut</item>--> <!--<item name="cornerSize">8dp</item>--> </style> 

我们为“小型”组件创建了Widget.MyApp.SmallShapeAppearance 。 我们将左上角四舍五入为20dp ,将右下角四舍五入为15dp


得到了这个结果:



看起来很有趣。 它会在现实生活中起作用吗? 时间会证明一切。


与样式一样,我们只能将ShapeAppearance应用于一个View组件。


黑暗主题有什么?


Android Q即将发布,随之而来的是官方的黑暗主题。


新版Android最有趣和最壮观的功能之一就是使用一行代码自动为整个应用程序使用深色主题。


听起来不错,请尝试。 我建议从terrakok选择最喜欢的gitlab客户


允许重新绘制应用程序(默认情况下禁用):


 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- ... --> <item name="android:forceDarkAllowed">true</item> </style> 

android:forceDarkAllowed可用于API 29(Android Q)。


我们开始,看看发生了什么:



同意对于一行代码来说,它看起来很酷。


当然,这里存在问题BottomNavigationBar与背景合并,加载程序保持白色,代码选择受到影响,而且看来,所有事情,至少没有什么让我震惊。


我敢肯定,不花太多时间就能解决主要问题。 例如,关闭各个视图的自动暗模式(是的,也可以android:forceDarkAllowed可用于标记文件中的View)。


请记住,此模式仅适用于浅色主题,如果您使用深色主题,则强制深色主题将不起作用。


工作建议可在文档material.io中找到


如果我们想自己做所有事情?


无论使用强制深色主题多么容易,此模式都缺乏灵活性。 实际上,所有事情都按照预定义的规则运行,这些规则可能不适合我们,更不适合客户。 我认为,这样的决定可以被认为是暂时的,直到我们执行一个黑暗的话题。


在API 8(Froyo)中,添加了-night限定词,该限定词在今天用于应用深色主题。 它允许您根据一天中的时间自动应用所需的主题。


DayNight主题中,已经使用了这样的实现,对于我们来说,从它们继承就足够了。


让我们尝试编写我们自己的:


 ../values/themes.xml <style name="Theme.DayNight.Base" parent="Theme.MaterialComponents.Light"/> <style name="Theme.MyApp.Main" parent="Theme.DayNight.Base> <!-- ... --> </style> ../values-night/themes.xml <style name="Theme.DayNight.Base" parent="Theme.MaterialComponents"/> 

在主题的常规资源( values/themes.xml )中,我们从浅色主题继承而来;在“ night”( values-night/themes.xml )中,我们从深色主题中继承。


仅此而已。 我们得到了一个黑暗主题的库实现。 现在,我们应该为两个主题提供资源。


要在应用程序运行时在主题之间切换,可以使用AppCompatDelegate.setDefaultNightMode ,它AppCompatDelegate.setDefaultNightMode以下参数:


  • MODE_NIGHT_NO浅色主题;
  • MODE_NIGHT_YES深色主题;
  • MODE_NIGHT_AUTO_BATTERY自动模式。 如果启用了省电模式,则暗色主题将打开;
  • MODE_NIGHT_FOLLOW_SYSTEM基于系统设置的模式。

处理主题和样式时应该考虑什么?


正如我指出的那样,谷歌已经正式开始强迫一个黑暗的话题。 我确信许多客户开始收到问题-“我们可以添加深色主题吗?”。 如果您从一开始就正确地做所有事情,那么这很好,而且您可以轻松地将浅色更改为深色,同时接收完全重新绘制的应用程序。


不幸的是,并非总是如此。 有一些旧的应用程序需要大量的精力才能进行必要的更改。


让我们尝试提出有关一起使用样式的建议:


1.拾色器


我认为每个开发人员都面临一种情况,即新的布局中出现了一些奇怪的颜色,而该颜色尚未在应用程序面板中定义。 在这种情况下该怎么办?


正确的答案是与设计师交谈,并尝试开发调色板。 现在,有许多程序(Zeplin,Sketch等)可让您渲染原色,然后重新使用它们。


您越早执行此操作,将来所遇到的麻烦就越少。


2.用正确的名称拼写颜色


在每种应用中,都有一种具有许多亮度选项的颜色。 您可以开始为它们发明名称:


 <color name="green_tiny">...</color> <color name="green_light">...</color> <color name="green_dark">...</color> 

同意,看起来不是很好。 问题立刻出现了-哪种颜色比tinylight颜色light ? 如果我们有十二种选择?


最好坚持使用Google概念,并在颜色名称中添加适当的亮度(Google将此颜色选项colorVariant ):


 <color name="material_green_300">...</color> <color name="material_green_700">...</color> <color name="material_green_900">...</color> 

通过这种方法,我们可以使用一种颜色的任意数量的亮度选项,而不必提供具体的名称,这确实很困难。


3.如果特定颜色在不同主题中发生变化,则从特定颜色中提取


由于我们正在编写一个至少包含两个主题的应用程序,因此,如果以不同的方式在主题中实现特定的颜色,我们将无法引用该特定的颜色。


让我们看一个例子:



我们看到,例如,在浅色主题中,工具栏为紫色,在黑暗中为深灰色。 我们如何仅使用主题功能来实现此行为?


一切都非常简单-如前所述,我们将创建一个属性,并以适当的颜色在浅色和深色主题中实现该属性。


Google建议将属性名称与用法语义相关联。


4.不要害怕创建资源文件


当在styles.xml中键入许多不同的样式,主题和属性时,将很难维护。


最好将所有内容分组到单独的文件中:


 themes.xml — Theme & ThemeOverlay styles.xml — Widget styles type.xml — TextAppearance, text size etc shape.xml — ShapeAppearance motion.xml — Animations styles system_ui.xml — Booleans, colors for UI control //may be other files 

这样简单的规则将避免使用上帝的文件 ,因此,将更易于维护样式。


5.重用到最大


如果我们要重新定义仅特定版本的API可用的属性,我们该怎么办?


我们可以创建两个单独的主题:


 ../values/themes.xml <style name="Theme.MyApp.Main" parent=”Theme.MaterialComponents.NoActionBar”> <!--Many definition--> </style> ../values-v27/themes.xml <style name="Theme.MyApp.Main" parent=”Theme.MaterialComponents.NoActionBar”> <!--Many definition--> <name="android:windowLightNavigationBar">...</item> </style> 

现在我们是否有一个主题,其中包含每个API版本的所有参数? 当然不是! 我们将创建一个基本主题,其中将确定可用于所有版本的API的基本属性,并在所需的API版本中从中继承这些属性:


 ../values/themes.xml <style name="Theme.MyApp.Base" parent=”Theme.MaterialComponents.DayNight.NoActionBar”> <!--Many definition--> </style> <style name="Theme.MyApp.Main" parent=”Theme.MyApp.Base”/> ../values-v27/themes.xml <style name="Theme.MyApp.Base.V27" parent="Theme.MyApp.Base"> <name="android:windowLightNavigationBar">...</item> </style> <style name="Theme.MyApp.Main" parent=”Theme.MyApp.Base.V27”/> 

按照此原则,将构建标准库中的所有主题。


6.使用向量资源和色彩


我认为不值得一提,为什么矢量资源很好。 每个人都已经知道(以防万一, 指向文档链接 )。 好吧, 着色将帮助我们为主题着色


本示例中,您可以看到什么是着色以及如何使用它。


7.?Android:attr / ... vs?Attr / ...


访问资源时,我们有机会同时使用系统属性和材料组件库中的属性。 重要的是要了解某些属性仅在特定版本的API中存在。 众所周知,访问不存在的资源会导致崩溃(当然,皮棉会告诉我们是否出了问题,但您不应该总是依靠它)


 android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless" 

在第一种情况下,我们访问系统资源,如android 。 在第二种情况下,是从库中实现向后兼容的属性。


最好始终使用第二个选项。


8.始终为样式指定父项


父样式中可以包含参数,否则会错误地呈现组件,因此您应始终指定父样式。


 <style name="Widget.MyApp.LoginInputLayout" parent="Widget.MaterialComponents.TextInputLayout.FilledBox"> <item name="errorTextColor">@color/colorError</item> </style> 

9.主题,风格还是...?


在创建自己的主题和样式时,如果您指定一个前缀来说明它是什么样式以及定义了什么样式,那将很棒。 这样的命名将使构造和扩展样式变得非常容易。


 <style name="Theme.MyApp.Main" parent=”...”/> <style name="Widget.MyApp.LoginInputLayout" parent="..."/> <style name="Widget.MyApp.LoginInputLayout.Brown"/> <style name="ThemeOverlay.MyApp.Login" parent=”...”/> 

10.使用TextAppearance


扩展文本的基本样式并在各处使用它们将是一个很好的基调。


Material Design: Typography , Typography Theming .


结论


, — , . - . Material Components. . Sketch — Material Theme Editor . . , .


Material Components GitHub — Modular and customizable Material Design UI components for Android . . , — sample, .


有用的链接:


Source: https://habr.com/ru/post/zh-CN461201/


All Articles