使用XML声明自定义Android UI元素

489

我该如何使用XML声明一个Android UI元素?


16
如果有人正在寻找支持的内置属性格式列表,可以在这里找到(例如:https://dev59.com/03A75IYBdhLWcg3wKlqM)。 - Marcin Orlowski
1
一个很好的入门教程 -> 在Android上创建复合视图 - Gayan Weerakutti
6个回答

849

Android开发者指南有一个名为构建自定义组件的章节。不幸的是,关于XML属性的讨论仅涵盖了在布局文件中声明控件,而没有涉及如何在类初始化中处理这些值。步骤如下:

1. 在values\attrs.xml中声明属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyCustomView">
        <attr name="android:text"/>
        <attr name="android:textColor"/>            
        <attr name="extraInformation" format="string" />
    </declare-styleable>
</resources>

注意在declare-styleable标签中使用未限定名称。非标准的Android属性,如extraInformation,需要声明其类型。在超类中声明的标签将在子类中可用,无需重新声明。

2. 创建构造函数

由于有两个使用AttributeSet进行初始化的构造函数,因此创建一个单独的初始化方法供构造函数调用是很方便的。

private void init(AttributeSet attrs) { 
    TypedArray a=getContext().obtainStyledAttributes(
         attrs,
         R.styleable.MyCustomView);

    //Use a
    Log.i("test",a.getString(
         R.styleable.MyCustomView_android_text));
    Log.i("test",""+a.getColor(
         R.styleable.MyCustomView_android_textColor, Color.BLACK));
    Log.i("test",a.getString(
         R.styleable.MyCustomView_extraInformation));

    //Don't forget this
    a.recycle();
}

R.styleable.MyCustomView是一个自动生成的int[]资源,其中每个元素都是属性的ID。通过将属性名附加到元素名来生成XML中的每个属性。例如,R.styleable.MyCustomView_android_text包含MyCustomViewandroid_text属性。然后可以使用各种get函数从TypedArray中检索属性。如果在XML中未定义该属性,则返回null。当然,如果返回类型是基本类型,则返回第二个参数。

如果不想检索所有属性,则可以手动创建此数组。标准Android属性的ID包含在android.R.attr中,而此项目的属性位于R.attr中。

int attrsWanted[]=new int[]{android.R.attr.text, R.attr.textColor};

请注意,根据此线程,您不应使用android.R.styleable中的任何内容,因为它将来可能会更改。它仍然在文档中,因为查看所有这些常量的位置很有用。
3. 将其用于布局文件,例如layout\main.xml 在顶级xml元素中包含命名空间声明xmlns:app="http://schemas.android.com/apk/res-auto"。命名空间提供了一种方法,可以避免不同模式使用相同元素名称时发生的冲突(有关更多信息,请参见此文章)。URL只是唯一标识模式的方式 - 实际上不需要在该URL上托管任何内容。如果似乎没有做任何事情,那是因为除非需要解决冲突,否则实际上不需要添加命名空间前缀。
<com.mycompany.projectname.MyCustomView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent"
    android:text="Test text"
    android:textColor="#FFFFFF"
    app:extraInformation="My extra information"
/> 

使用完全限定名称引用自定义视图。

Android LabelView示例

如果您需要完整的示例,请查看Android标签视图示例。

LabelView.java

 TypedArray a=context.obtainStyledAttributes(attrs, R.styleable.LabelView);
 CharSequences=a.getString(R.styleable.LabelView_text);

attrs.xml

<declare-styleable name="LabelView">
    <attr name="text"format="string"/>
    <attr name="textColor"format="color"/>
    <attr name="textSize"format="dimension"/>
</declare-styleable>

custom_view_1.xml

<com.example.android.apis.view.LabelView
    android:background="@drawable/blue"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    app:text="Blue" app:textSize="20dp"/>

这是包含在一个具有命名空间属性的LinearLayout中的内容:xmlns:app="http://schemas.android.com/apk/res-auto"

链接


14
我想补充一点,如果你的根元素需要使用你自己的命名空间,你需要同时添加标准的Android命名空间和你自己的自定义命名空间,否则可能会导致构建错误。 - Chase
11
感谢你,Casebash,提供了互联网上最清晰的有关自定义XML参数的资源。 - Artem Russakovskii
2
由于某种原因,可视化编辑器拒绝使用 android:text 的书面文本值,但设备可以正常使用。为什么会这样? - android developer
2
@androiddeveloper 看起来Eclipse编辑器拒绝使用所有android:属性的值。我想知道这是一个特性还是一个bug。 - deej
4
xmlns:app命名空间和res-auto有什么目的? - IgorGanapolsky
显示剩余7条评论

91

非常好的参考资料。谢谢!

另外补充一点:

如果您所使用的库项目中声明了自定义属性用于自定义视图,那么您必须声明您的项目命名空间,而不是库项目的命名空间。例如:

假设库的包名为 "com.example.library.customview",工作项目的包名为 "com.example.customview",则以下方式将无法工作(显示错误 " error: No resource identifier found for attribute 'newAttr' in package 'com.example.library.customview'"):

<com.library.CustomView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res/com.example.library.customview"
        android:id="@+id/myView"
        app:newAttr="value" />

可以正常工作:

<com.library.CustomView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res/com.example.customview"
        android:id="@+id/myView"
        app:newAttr="value" />

48
这个问题在ADT 17预览版中已经被解决。如果要从库中使用应用的命名空间,请声明xmlns:app="http://schemas.android.com/apk/res-auto"。请参见http://code.google.com/p/android/issues/detail?id=9656中的第57条评论。 - nmr
2
包含您的自定义命名空间现在会返回错误“可疑的命名空间:您是否意味着http://schemas.android.com/apk/res-auto” - Ben Wilkinson
自定义命名空间以res-auto结尾,因为我们使用的是Android Studio和Gradle。否则(例如某些Eclipse版本),它通常会以lib / [您的软件包名称] 结尾。 - Universe
自定义命名空间以 res-auto 结尾,因为我们使用的是 Android Studio 和 Gradle。否则(例如某些 Eclipse 版本),它通常会以 lib/[your package name] 结尾。即 http://schemas.android.com/apk/lib/[your package name] - Universe

27

除了最受欢迎的答案之外,还有以下补充。

obtainStyledAttributes()

我想补充一些关于obtainStyledAttributes()用法的内容,当我们使用android:xxx预定义属性创建自定义视图时,特别是当我们使用TextAppearance时。
正如在“2. 创建构造函数”中提到的那样,自定义视图在创建时会获得AttributeSet。主要用途可以在TextView源代码(API 16)中看到。

final Resources.Theme theme = context.getTheme();

// TextAppearance is inspected first, but let observe it later

TypedArray a = theme.obtainStyledAttributes(
            attrs, com.android.internal.R.styleable.TextView, defStyle, 0);

int n = a.getIndexCount();
for (int i = 0; i < n; i++) 
{
    int attr = a.getIndex(i);
    // huge switch with pattern value=a.getXXX(attr) <=> a.getXXX(a.getIndex(i))
}
a.recycle();

这里我们能看到什么?
obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
根据文档,主题会处理属性集。属性值是逐步编译的。首先,属性从主题中填充,然后值被样式替换,最后特殊视图实例的确切值替换其他值。
请求属性数组- com.android.internal.R.styleable.TextView
这是一个普通的常量数组。如果我们请求标准属性,可以手动构建此数组。

文档中没有提到的是- TypedArray元素的顺序。当在attrs.xml中声明自定义视图时,将生成用于属性索引的特殊常量。我们可以通过以下方式提取值:a.getString(R.styleable.MyCustomView_android_text)。但对于手动创建的int[],则没有常量。我认为,getXXXValue(arrayIndex)会很好地工作。

另一个问题是:“我们如何替换内部常量并请求标准属性?”我们可以使用android.R.attr.* 值。

因此,如果我们想在自定义视图中使用标准TextAppearance属性并在构造函数中读取其值,则可以按照以下方式修改TextView代码:

ColorStateList textColorApp = null;
int textSize = 15;
int typefaceIndex = -1;
int styleIndex = -1;

Resources.Theme theme = context.getTheme();

TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.CustomLabel, defStyle, 0);
TypedArray appearance = null;
int apResourceId = a.getResourceId(R.styleable.CustomLabel_android_textAppearance, -1);
a.recycle();
if (apResourceId != -1)
{
    appearance = 
        theme.obtainStyledAttributes(apResourceId, new int[] { android.R.attr.textColor, android.R.attr.textSize, 
            android.R.attr.typeface, android.R.attr.textStyle });
}
if (appearance != null)
{
    textColorApp = appearance.getColorStateList(0);
    textSize = appearance.getDimensionPixelSize(1, textSize);
    typefaceIndex = appearance.getInt(2, -1);
    styleIndex = appearance.getInt(3, -1);

    appearance.recycle();
}

CustomLabel的定义位置:

<declare-styleable name="CustomLabel">
    <!-- Label text. -->
    <attr name="android:text" />
    <!-- Label text color. -->
    <attr name="android:textColor" />
    <!-- Combined text appearance properties. -->
    <attr name="android:textAppearance" />
</declare-styleable>

Maybe,我在某些方面可能有误解,但是Android关于obtainStyledAttributes()的文档非常不好。

扩展标准UI组件

同时,我们可以通过扩展标准UI组件来使用其声明的所有属性。 这种方法并不太好,因为例如TextView声明了很多属性。而且在重载onMeasure()和onDraw()时无法实现全部功能。

但是我们可以牺牲理论上广泛重用自定义组件的优点。说“我确切地知道我将使用哪些功能”,并且不与任何人共享代码。

然后,我们可以实现构造函数CustomComponent(Context, AttributeSet, defStyle)。在调用super(...)之后,我们将拥有所有解析的属性,并可以通过getter方法访问。


在 Eclipse GUI 设计器中,Android 的预定义属性 android:xxx 能够正常工作吗? - deej
这些属性可以在Eclipse ADT插件的属性编辑器中识别。如果某些值未定义,我可以从我的样式中看到默认值。 并且不要忘记在您的类中添加@RemoteView注释。 - yuriy.weiss
无法使其工作。Eclipse在getText中不断加载null,并在getResourceId中抛出android.content.res.Resources$NotFoundException,尽管应用程序在设备上运行良好。 - deej
抱歉,我无法帮助您。我只创建了演示项目来测试可能性,并没有遇到这样的错误。 - yuriy.weiss
这比将自定义视图的自定义属性映射到包含在内置视图中的内置属性要好得多。 - samus

13

看起来Google已经更新了开发者页面,并添加了各种培训内容。

其中之一涉及自定义视图的创建,可以在这里找到。


5
非常感谢你的第一次回答。
对于我来说,我只遇到了一个问题。在膨胀我的视图时,我遇到了一个错误:java.lang.NoSuchMethodException : MyView(Context, Attributes) 我通过创建一个新的构造函数来解决它:
public MyView(Context context, AttributeSet attrs) {
     super(context, attrs);
     // some code
}

希望这能有所帮助!

0

您可以在其他布局文件中包含任何布局文件,如下所示:

             <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_marginRight="30dp" >

                <include
                    android:id="@+id/frnd_img_file"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    layout="@layout/include_imagefile"/>

                <include
                    android:id="@+id/frnd_video_file"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    layout="@layout/include_video_lay" />

                <ImageView
                    android:id="@+id/downloadbtn"
                    android:layout_width="30dp"
                    android:layout_height="30dp"
                    android:layout_centerInParent="true"
                    android:src="@drawable/plus"/>
            </RelativeLayout>

这里的包含标签中的布局文件是同一 res 文件夹中的其他 .xml 布局文件。


1
我已经尝试过这个,但问题是它包含的布局不能被“适应”,也不能创建泛型。例如,当我以类似的方式包含一个按钮时,如果我尝试在xml中设置文本,它不起作用。 - cfl

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