如何通过编程创建Android主题风格?

37

有很多文档和教程介绍如何通过XML创建或自定义Android主题风格,但是我找不到一种在代码中创建这样的方式。您有没有任何想法可以在代码中创建风格而不是使用XML?

以下是需要在代码中以编程方式创建的示例XML:

<resources>
  <style name="AppTheme" parent="android:Theme.Material">
    <item name="android:colorPrimary">@color/primary</item>
    <item name="android:colorPrimaryDark">@color/primary_dark</item>
    <item name="android:colorAccent">@color/accent</item>
  </style>
</resources>

1
你想以编程方式完成它的原因是什么?如果你只是想在运行时动态更改样式,可以查看这个链接https://dev59.com/dnA75IYBdhLWcg3wipmH。如果你只是想在代码中简单地检索样式属性-请查看这个链接:https://dev59.com/MGYr5IYBdhLWcg3wYZSD。 - random
@random 没有特定的原因,只是厌倦了 XML,所以想试试是否可以动态创建主题。 - Mercury
11
我有理由这么做。我需要动态地设置颜色,目前还没有决定好颜色。因此,我在XML中编写了上述主题,但我只想在第一个屏幕上动态设置颜色,以便在之后的所有屏幕上都能应用该设定。 - Ajay Sharma
@AjaySharma 但是你可以在xml中设置样式并从代码中加载以解决你的问题。 - Renjith Thankachan
@Mercury 在我的回答中更新了更多关于调用流程的细节,提到在没有XML资源的情况下无法将主题应用到窗口/活动。 - Renjith Thankachan
显示剩余4条评论
2个回答

7
TL;DR: 不行,这就是Android的工作方式,而且永远不会改变。
在 Android 中,程序化创建主题永远不可能实现的一个简单原因是,当应用程序启动时,Android 创建一个虚拟窗口,显示应用程序的 android:windowBackground。此时,应用程序进程仍在初始化中,在应用执行开始和启动的活动的 Activity.onCreate 方法返回时,虚拟窗口将被应用程序窗口替换,该窗口显示该活动。
因此,事实是,由于 android:windowBackground 由主题设置,但 Android 必须在应用程序甚至启动之前访问它,所以必须将主题作为资源进行访问,以便从任何进程(包括系统进程)中访问。
此外,主题是资源,因此它们完全是不可变的。这是 Android 的工作方式。它不能突然改变,并且很可能永远不会改变。资源永远无法被动态修改的另一个原因是,这直接意味着 APK 本身也需要被修改,而这并不是 APK 的设计目的。
有人可能会说“一切都必须在幕后以编程方式完成”。好吧,这是正确的。但是, android.content.res.Resources.Theme 类是最终的,这是有原因的,目的是确保没有任何被覆盖,以便其行为保证反映资源所指示的内容,这对于系统进程访问活动主题的 android:windowBackground 与应用程序启动后的行为一致非常重要。同样,当涉及到 android.content.Context.obtainStyledResources 方法时也是如此,该方法也是最终的。如果应用程序可以覆盖该方法,它将能够返回与资源不匹配的值,而这是一个问题,因为这只会在应用程序进程启动后、已显示原始的真实 android:windowBackground 时发生。

2
这个参数通常是有意义的,但它让我想知道为什么会存在以下方法:https://developer.android.com/reference/android/content/res/Resources#newTheme()Resources公开了一个newTheme()方法。 - Zach Sperske
1
@ZachSperske,虽然我不知道newTheme()方法,但我所描述的是Android实际工作方式的基础,因此我的答案应该是正确的。我猜测你提到的方法是为Resources类的实现创建一个Theme对象。 - Davide Cannizzo
1
这样,应该可以实现一个自定义的“资源”类,为属性提供自定义值,并为其创建一个主题对象,你将能够将其设置为活动的主题,从而在运行时实际创建主题。但是,清单中指定的活动主题必须是XML中定义的一个主题,而当应用程序的进程尚未创建时,活动仍将显示XML主题,而通过编程方式创建的自定义主题只能在活动完全加载后才能显示。 - Davide Cannizzo
@ZachSperske,很高兴听到你说你已经找到了实现你一直希望的功能的方法。然而,我认为可以用不同的方式更好地完成它——而不是从XML样式编程创建不同的主题并将其直接应用于Drawable,更明智的做法是将XML样式作为主题应用于实际持有drawable的View。 - Davide Cannizzo
如果该视图在XML布局中,您可能希望使用XML android:theme属性来实现这一点,或者您可以使用ContextThemeWrapper类动态创建一个主题上下文,并使用该上下文创建视图。我总是一次写两个注释,哈哈。 - Davide Cannizzo
显示剩余4条评论

6
简短回答:无法通过编程方式创建主题并将其设置为应用程序主题(即使我们成功创建了Theme对象),因为没有主题资源ID。
详情:
当您调用setTheme函数时,实际上是调用ContextWrapper的一个方法,该方法最终会使用资源ID指针调用AssetManager。AssetManager类保存了应用程序主题应用的方法,这是JNI调用。
native static final void applyThemeStyle(long theme, int res, boolean force);

如上所述,我们只能传递资源ID来应用主题样式。但是可能的选择是:

  1. 虽然它只限于 Window 类特性常量。我们可以使用 setFeatureDrawable 和特性常量设置一些绘图,例如:FEATURE_ACTION_BARFEATURE_CONTEXT_MENU 等。
  2. 使用活动中的 setTheme 函数,我们可以从样式资源设置主题,这将解决由 AjaySharmaNathan 在评论中提到的问题。

这是2020年了。这还不可能吗? - andylamax
@andylamax,我的回答对于你的问题也是有效的。 - Davide Cannizzo
3
使用Jetpack Compose,您可以在运行时动态更改主题属性,所以...2020年没有辜负我们。XML和UIKit已死,长存Compose和SwiftUI。 - flopshot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接