如何在安卓中进行样式探索

14

我正试图在Android中为我的应用程序设置主题。然而,每个小部件本身都是一个痛苦的过程:我必须搜索适用于该特定小部件的主题,然后创建一种希望源自小部件使用的样式的样式。

当然,关于主题化特定小部件的答案并不总是包含有关基础样式的信息,只有特定的颜色。

因此,与其接受鱼来吃,不如教我如何钓鱼?

我该如何解释小部件构造函数中的ObtainStyledAttributes()调用并从中提取样式?我该如何递归处理它们?

特别地,您能向我展示AlertDialog按钮颜色的过程吗?哪种样式定义了棒棒糖平面按钮 + 青色文本颜色?如果我从AlertDialog源代码和ObtainStyledAttributes调用开始,我该如何到达该样式?

2个回答

39
我发现样式设计就是通过框架进行调试。 “什么”(几乎总是)来自小部件的实现。而“何处”,我发现到处都有。 我将尽力通过您特定的用例 - AlertDialog的按钮,来解释这个过程。
开始:
您已经明白了:我们从小部件的源代码开始。 我们特别想找出 - AlertDialog按钮从哪里获取文本颜色。 因此,我们开始查看这些按钮来自哪里。 它们是在运行时显式创建的吗? 还是它们在被填充的xml布局中定义?
在源代码中,我们发现mAlert处理按钮选项以及其他事项:
public void setButton(int whichButton, CharSequence text, Message msg) {
    mAlert.setButton(whichButton, text, null, msg);
}

mAlertAlertController 的一个实例。在它的构造函数中,我们可以发现属性 alertDialogStyle 定义了 xml 布局:

TypedArray a = context.obtainStyledAttributes(null,
            com.android.internal.R.styleable.AlertDialog,
            com.android.internal.R.attr.alertDialogStyle, 0);

    mAlertDialogLayout = 
            a.getResourceId(
            com.android.internal.R.styleable.AlertDialog_layout,
            com.android.internal.R.layout.alert_dialog);

因此,我们应该查看的布局是 alert_dialog.xml - [sdk_folder]/platforms/android-21/data/res/layout/alert_dialog.xml

布局xml非常长。这是相关部分:

<LinearLayout>

....
....

<LinearLayout android:id="@+id/buttonPanel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="54dip"
    android:orientation="vertical" >
    <LinearLayout
        style="?android:attr/buttonBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingTop="4dip"
        android:paddingStart="2dip"
        android:paddingEnd="2dip"
        android:measureWithLargestChild="true">
        <LinearLayout android:id="@+id/leftSpacer"
            android:layout_weight="0.25"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:visibility="gone" />
        <Button android:id="@+id/button1"
            android:layout_width="0dip"
            android:layout_gravity="start"
            android:layout_weight="1"
            style="?android:attr/buttonBarButtonStyle"
            android:maxLines="2"
            android:layout_height="wrap_content" />
        <Button android:id="@+id/button3"
            android:layout_width="0dip"
            android:layout_gravity="center_horizontal"
            android:layout_weight="1"
            style="?android:attr/buttonBarButtonStyle"
            android:maxLines="2"
            android:layout_height="wrap_content" />
        <Button android:id="@+id/button2"
            android:layout_width="0dip"
            android:layout_gravity="end"
            android:layout_weight="1"
            style="?android:attr/buttonBarButtonStyle"
            android:maxLines="2"
            android:layout_height="wrap_content" />
        <LinearLayout android:id="@+id/rightSpacer"
            android:layout_width="0dip"
            android:layout_weight="0.25"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:visibility="gone" />
    </LinearLayout>

我们现在知道按钮的样式由属性buttonBarButtonStyle决定。

前往[sdk_folder]/platforms/android-21/data/res/values/themes.material.xml并搜索buttonBarButtonStyle

<!-- Defined under `<style name="Theme.Material">` -->
<item name="buttonBarButtonStyle">@style/Widget.Material.Button.ButtonBar.AlertDialog</item>

<!-- Defined under `<style name="Theme.Material.Light">` -->
<item name="buttonBarButtonStyle">@style/Widget.Material.Light.Button.ButtonBar.AlertDialog</item>

根据您的活动的父主题,buttonBarButtonStyle将引用这两种样式中的一种。现在,假设您的活动主题扩展了Theme.Material。我们来看看@style/Widget.Material.Button.ButtonBar.AlertDialog:

打开[sdk_folder]/platforms/android-21/data/res/values/styles_material.xml并搜索Widget.Material.Button.ButtonBar.AlertDialog:

<!-- Alert dialog button bar button -->
<style name="Widget.Material.Button.ButtonBar.AlertDialog" parent="Widget.Material.Button.Borderless.Colored">
    <item name="minWidth">64dp</item>
    <item name="maxLines">2</item>
    <item name="minHeight">@dimen/alert_dialog_button_bar_height</item>
</style>

好的。但是这些值并不能帮助我们确定按钮的文本颜色。接下来我们应该查看父级样式 - Widget.Material.Button.Borderless.Colored
<!-- Colored borderless ink button -->
<style name="Widget.Material.Button.Borderless.Colored">
    <item name="textColor">?attr/colorAccent</item>
    <item name="stateListAnimator">@anim/disabled_anim_material</item>
</style>

最后,我们找到了textColor - 它由attr/colorAccent 提供并在Theme.Material中初始化。
<item name="colorAccent">@color/accent_material_dark</item>

对于 Theme.Material.LightcolorAccent 被定义为:
<item name="colorAccent">@color/accent_material_light</item>

浏览到 [sdk_folder]/platforms/android-21/data/res/values/colors_material.xml 并找到以下颜色:

<color name="accent_material_dark">@color/material_deep_teal_200</color>
<color name="accent_material_light">@color/material_deep_teal_500</color>

<color name="material_deep_teal_200">#ff80cbc4</color>
<color name="material_deep_teal_500">#ff009688</color>

AlertDialog的截图和相应的文本颜色:

enter image description here

快捷方式:

有时,阅读颜色值(如上图)并使用AndroidXRef搜索它会更容易。在您的情况下,这种方法将没有用处,因为#80cbc4只会指出它是强调颜色。您仍然需要定位Widget.Material.Button.Borderless.Colored并将其与属性buttonBarButtonStyle联系起来。

更改按钮的文本颜色:

理想情况下,我们应该创建一个扩展Widget.Material.Button.ButtonBar.AlertDialog的样式,在其中覆盖android:textColor,并将其分配给属性buttonBarButtonStyle。但是,这不会起作用 - 您的项目无法编译。这是因为Widget.Material.Button.ButtonBar.AlertDialog是一个非公共样式,因此无法扩展。您可以通过检查链接来确认这一点。

我们将执行次佳操作 - 扩展Widget.Material.Button.ButtonBar.AlertDialog的父样式 - 公共的Widget.Material.Button.Borderless.Colored

<style name="CusButtonBarButtonStyle" 
       parent="@android:style/Widget.Material.Button.Borderless.Colored">
    <!-- Yellow -->
    <item name="android:textColor">#ffffff00</item>

    <!-- From Widget.Material.Button.ButtonBar.AlertDialog -->
    <item name="android:minWidth">64dp</item>
    <item name="android:maxLines">2</item>
    <item name="android:minHeight">@dimen/alert_dialog_button_bar_height</item>
</style>

请注意,我们在覆盖android:textColor后添加了3个更多的项目。这些项目来自非公共样式Widget.Material.Button.ButtonBar.AlertDialog。由于我们无法直接扩展它,因此必须包含它定义的项目。注意:必须查找dimen值并将其转移到适当的res/values(-xxxxx)/dimens.xml文件中。
属性buttonBarButtonStyle将被分配为样式CusButtonBarButtonStyle。但问题是,AlertDialog如何知道这一点?从源代码中看:
protected AlertDialog(Context context) {
    this(context, resolveDialogTheme(context, 0), true);
}

0 作为 resolveDialogTheme(Context, int) 的第二个参数传递,将会导致进入 else 子句:

static int resolveDialogTheme(Context context, int resid) {
    if (resid == THEME_TRADITIONAL) {
        ....
    } else {
        TypedValue outValue = new TypedValue();
        context.getTheme().resolveAttribute(
                com.android.internal.R.attr.alertDialogTheme,
                outValue, true);
        return outValue.resourceId;
    }
}

我们现在知道主题由alertDialogTheme属性控制。接下来,我们看看alertDialogTheme指向什么。这个属性的值将取决于您活动的父主题。浏览到sdk文件夹并找到android-21中的values/themes_material.xml。搜索alertDialogTheme。结果:

<!-- Defined under `<style name="Theme.Material">` -->
<item name="alertDialogTheme">@style/Theme.Material.Dialog.Alert</item>

<!-- Defined under `<style name="Theme.Material.Light">` -->
<item name="alertDialogTheme">@style/Theme.Material.Light.Dialog.Alert</item>

<!-- Defined under `<style name="Theme.Material.Settings">` -->
<item name="alertDialogTheme">@style/Theme.Material.Settings.Dialog.Alert</item>

根据你的活动基础主题,alertDialogTheme 将拥有以下三个值之一。为了让 AlertDialog 知道 CusButtonBarButtonStyle,我们需要在应用程序的主题中覆盖属性 alertDialogTheme。比如说,我们使用 Theme.Material 作为基础主题。

<style name="AppTheme" parent="android:Theme.Material">
    <item name="android:alertDialogTheme">@style/CusAlertDialogTheme</item>
</style>

从上文可以得知,当你的应用程序的基本主题是Theme.Material时,alertDialogTheme指向Theme.Material.Dialog.Alert。因此,CusAlertDialogTheme的父级应该是Theme.Material.Dialog.Alert

<style name="CusAlertDialogTheme" 
       parent="android:Theme.Material.Dialog.Alert">
    <item name="android:buttonBarButtonStyle">@style/CusButtonBarButtonStyle</item>
</style> 

结果:

在此输入图片描述

所以,与其接受鱼来吃,你能教我如何捕鱼吗?

至少,我希望已经解释了鱼在哪里。

P.S. 我意识到我发了一篇巨大的文章。


3
哇,真是让人惊叹。这就是我所想象的那个混乱场面。谷歌在API开发方面仍然有很多需要学习的地方 :( 至少 Material 试图解决这些问题。现在,看看我能否将在这里学到的内容翻译到我正在使用的 Appcompat 主题中。 - velis
1
@velis 哈哈,太对了。如果你在自定义Appcompat主题方面遇到了问题,请告诉我是否能够使用这种方法。如果你遇到了困难,我们可以进一步讨论。顺便说一句,很棒的问题,感谢你的悬赏。 - Vikram
在Android-24上,主题文件是themes_material.xml。 - dazza5000
1
感谢您将我曾经艰难学习的内容写成文字。这个答案今天仍然很相关,如果您开始研究小部件的appcompat与非appcompat版本,可能会有更多的混淆,因为它们在样式上常常存在微妙的差异。 - JavierIEH

0
除了@Vikram的优秀答案之外,值得注意的是Android Studio可以极大地简化您的工作。您只需要将鼠标悬停在主题上,它会显示类似以下内容的东西。
actionBarStyle = @style/Widget.AppCompat.Light.ActionBar.Solid 
=> @style/Widget.AppCompat.Light.ActionBar.Solid

你也可以使用鼠标单击来在样式之间导航,就像在普通的Java代码中所做的那样。

你可以在以下路径找到支持库的资源 <sdk root>/extras/android/m2repository/com/android/support/<support library name>/<version number>/<support library>.aar/res

但是*.aar/res/values/values.xml包含了所有的值,而且不容易阅读。你可以在https://android.googlesource.com/platform/frameworks/support/+/master中获取原始的支持库代码和资源。

有一个名为tgz的按钮可下载当前快照。


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