创建自己的 Android 自定义控件






4.50/5 (5投票s)
使用 Java 和 XML 在 Android 中创建具有自定义属性的自定义控件。
引言
在 Android 中,我们有很多方法可以将布局“分组”在一起,使其可重用。在我们的 XML 设计中有一个 <include>
标签(以及鲜为人知的 <merge>
),它只是将另一个布局包含到当前布局中;还有 Fragments
;当然,我们可以随时从任何基类派生,例如 View
或 LinearLayout
。
本文将更深入地探讨后一种方法。
- 如何编写一个可以膨胀自己布局的控件?
- 如何为我的控件创建自定义属性?
- 这些如何与样式和主题混合在一起?
本文的范围是膨胀自己布局的控件,因此我们将从匹配的基类派生,具体是 LinearLayout
还是 RelativeLayout
,取决于我们的自定义布局是如何构建的。
我将在这里使用我自己的一个控件作为示例,向您展示它是如何完成的。这个控件是一个简单的工具栏,带有四个按钮,支持我的软件品牌的一些标准功能,例如给我发送电子邮件、打开我的 G+ 页面、打开我在 Google Play 上的开发者页面以及评价当前正在运行的应用程序。
它非常简单,因此是一个很好的分析对象。
运行时,我的控件看起来像这样
这是从我使用深色主题的应用程序的屏幕截图中截取的。
XML 设计中的声明
<mbar.ui.controls.MbarBar
android:id="@+id/contact_mbar_button_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/contact_mbar_credit_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/mbar_default_control_distance"
mbar:barSize="small"/>
实现此目的可能有不同的方法
- 只需
<include>
预先绘制的库布局,并在代码中分配按钮监听器。 - 在每个应用程序中手动绘制(开玩笑...连想都不要想!:))
- 创建一个控件,并按上述方法进行操作。
当然,本文我们将采用第三种方法。您在此 XML 片段中可以看到,该控件至少使用了一个自定义属性:barSize
。它是一个 enum
类型属性,其值为“small
”(0)和“large
”(1)。我们将很快创建它,以及第二个自定义属性 showTitle
,一个简单的布尔值。
本文假设您的库/项目已设置为 minAPI 17。
步骤 1:创建您的控件类
最好的开始是:创建你的类并决定你的基类。在我们的例子中,它是一个简单的 LinearLayout
。
你需要知道什么
有几个构造函数可用,并非所有构造函数都*需要*提供,但我总是尝试尽可能完整地覆盖基类。
因此,当您启动一个 extends LinearLayout
的类时,有 4 个构造函数可用
public class MbarBar extends LinearLayout {
private @MbarBarSize int barSize = MbarBarSize.SMALL;
private boolean showTitle = true;
// <editor-fold desc="Constructors">
public MbarBar(Context context) {
super(context);
init(null, 0, 0);
}
public MbarBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs, 0, 0);
}
public MbarBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr, 0);
}
@TargetApi(21)
public MbarBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
}
// </editor-fold>
您可以看到,我将它们全部重定向到一个 init(...)
方法。我们稍后会讨论,现在让我们专注于构造函数。
第四个构造函数周围有一个 @TargetApi(21)
注解,因为这个构造函数只在 21+ 版本可用。
系统提供给构造函数的值是什么?
第一条规则:将它们传递给超类,除非你有一个非常好的理由不这样做!
第二:对于我们开发者来说,第二个参数 AttributeSet
最重要,因为它包含了 XML 中指定的属性,*包括我们的自定义属性* barSize
和 showTitle
。所以这已经揭示了第一个谜团:我们如何将 XML 中的值获取到我们的控件中?答案是:通过 AttributeSet
。我们如何从中获取值将在讨论 init(...)
方法时介绍。
将您的 XML 枚举值映射到代码值
我非常喜欢这些 @interfaces
,所以我为 barSize
创建了一个。如果您不知道那是什么:它或多或少是一种将整数或字符串组合在一起的替代方法。Android SDK 在很多地方都这样做,您也遇到过它们。举个简单的例子:无论何时将某物设置为 View.VISIBLE
或 View.GONE
,您都在接触其中之一。
在类的顶部,您可以看到声明
private @MbarBarSize int barSize = MbarBarSize.SMALL;
这与 XML 设计中的这一行相关
mbar:barSize="small"
当我们稍后讨论这个自定义属性的声明时,您会看到术语“small
”和“large
”代表值“0
”和“1
”。现在,我们当然希望在 Java 中以相同的名称(SMALL 和 LARGE
)处理这些值,并且我们不想使用 0
和 1
。
那么这个 @MbarBarSize
是什么?它有什么作用? @annotation
告诉我们,这个 int
变量只能保存 @MbarBarSize
中定义的值。如果您尝试分配其他值,您会收到 Lint 警告。
为了声明成员的此类限制,@interface
声明就派上了用场。MbarBarSize
定义如下
@Retention(RetentionPolicy.CLASS)
public @interface MbarBarSize {
int SMALL = 0;
int LARGE = 1;
}
通过这样的声明,您可以为通常也允许其他值的数据类型分配我称之为 value-restriction
或 value-constraint
的东西。
保留策略 (RetentionPolicy)
重要的是您要了解何时使用哪种策略。有三种策略可用
- CLASS: (默认)。此策略*可以*在任何地方使用,但我主要在库中使用它。
Class
意味着,这个@interface
将在编译后仍然存在,并对您的库用户可用。他们可以使用SMALL
和LARGE
值,就好像他们在自己的应用程序/库中定义了它们一样。这是您想要的,如果您需要它作为方法参数,并且希望 lint 在用户分配不允许的值时能够警告您的库用户。在*库*中,您*希望*您的值以给定的名称使用。您不希望强制您的库用户将“0
”和“1
”作为参数值提供给您的方法。他们也应该使用SMALL
和LARGE
! - SOURCE:此策略告诉编译器放弃定义。对于人类思维而言,这意味着:当您定义一个不需要在编译过程中存活的
@interface
时,可以使用 SOURCE 策略。举个例子:在您的应用程序中,当您的应用程序之外没有任何东西需要访问具有给定名称的值(在上述示例中为SMALL
和LARGE
)。在您当前项目之外,SMALL
和LARGE
是*未知*的。用户必须提供“0
”和“1
”作为参数值。这不适用于您的public
接口和方法,但您可以将其用于库中的private
内容。 - RUNTIME:这是所有策略中最广泛的。它不仅在编译过程中得以保留,并可供您的用户使用,甚至在程序已经*运行*时也可用,并且可以通过反射进行访问!除此之外,它的行为与 **CLASS** 类似。
步骤 2:定义自定义属性
好的,我们已经了解了如何将属性值映射到 Java 代码,但是这个属性是如何**定义**的呢?
您可以通过向项目的 *values* 文件夹添加一个名为 *attrs.xml* 的文件来创建自定义属性。右键单击项目资源管理器中的 values
节点,然后选择 **New** -> **Values resource file**。将文件命名为 *attrs.xml*。
在这个文件中,你可以创建自定义属性。语法并不令人惊讶,很容易理解。我在这里向你展示 MbarBar
控件的完整属性集
<declare-styleable name="MbarBar">
<attr name="barSize" format="enum">
<enum name="small" value="0"/>
<enum name="large" value="1"/>
</attr>
<attr name="showTitle" format="boolean"/>
</declare-styleable>
您在此处声明了一个 styleable
资源,这将使其可用于 XML 设计器。
有多种格式可用,如果在这里全部涵盖会超出本文的范围,但 enum
格式是其中一个比较有趣的,我们将看看这个
- 最重要的是:
<declare-styleable name="class_name_of_your_control">
在这里,您**不能**随意选择“name
”属性!这已经是与您的控件类的连接(以及我们首先创建类,然后创建属性的原因)!我们的控件名为MbarBar
,这里就是这个确切的控件/类名。通过这一行,您在此 styleable *内部*声明的所有内容都将附加到MbarBar
类。
然后,声明了两个自定义属性
- 定义为
format="enum"
,然后我们可以添加任意数量的<enum value
条目列表。我们给它们一个名称和一个值。您会看到,这里的“0
”和“1
”对应于我们@interface
的整数表示形式的0
和1
。您的控件的用户可以在 XML 布局中将值指定为“small
”和“large
”。 - 定义为
format="boolean"
,我们添加了一个简单的开关来显示/隐藏标题文本“More mbar Software
”,以便为控件提供更多自定义功能。
步骤 3:将所有部分组合在一起:init(...) 方法
现在,您已经定义了自定义属性,也看到了它在 XML 布局中的样子,让我们将它们组合起来,看看 AttributeSet
,以获取开发人员在 XML 中输入的值。
方法在前,解释在后
private void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
if (attrs != null) {
TypedArray array = getContext().getTheme().obtainStyledAttributes
(attrs, R.styleable.MbarBar, defStyleAttr, defStyleRes);
barSize = array.getInt(R.styleable.MbarBar_barSize, 0);
showTitle = array.getBoolean(R.styleable.MbarBar_showTitle, true);
}
switch (barSize) {
case MbarBarSize.SMALL:
View smallMe = inflate(getContext(), R.layout.mbar_button_frame_small, this);
break;
case MbarBarSize.LARGE:
View largeMe = inflate(getContext(), R.layout.mbar_button_frame, this);
break;
}
setTitleBarVisibility();
connectListeners();
}
其中一个构造函数(第一个)将发送 null
作为 attrs 值到此方法,因此我们需要一个 null
检查。在这种情况下,控件将使用所有默认值:barSize=SMALL
和 showTitle=true
(参见本文第一个代码块 - 构造函数,这是类成员的设置方式)。
这里最重要的部分是从 XML 获取属性。这是通过 obtainStyledAttributes
方法完成的,该方法与我们 context
的当前主题相关联。由于我们从 LinearLayout
派生,我们有一个 getContext()
方法可用,所以我们不需要参数或其他方式来获取有效的上下文。我们已经有一个了。
参数如下:
attrs
。这是我们想要获取值的AttributeSet
。它是构造函数中提供的那个。R.styleable.MbarBar
。我相信,到目前为止,您已经知道那是什么了。我们在 *attrs.xml* 中定义的自定义属性。我们想要获取的正是*这些*值。defStyleAttr
和Res
。一切都经过主题和样式设计。Android 将应用当前主题和样式的任何修改,并将其考虑在我们将获得的值中。
此调用之后,我们可以像访问 Bundle
的 Extras
一样轻松地访问我们的属性。通过 getInt
、getBool
、getWhatYouNeed
。非常简单的接口。这些调用中的第二个参数是未找到时的默认值。
紧随其后的是一个 switch
语句,它会膨胀两个预定义布局中的一个(小按钮框架和大按钮框架)。这些布局没有什么特别之处,它们的设计方式与其他布局一样。只是一堆图片和文本视图。标准。您可以像膨胀任何其他布局一样膨胀它。
然后调用了一些其他支持方法,比如隐藏标题栏和连接点击监听器,但它们不是本文的范围。我们想创建一个带有自定义属性的自定义控件:)
很酷的事情:这在设计时就已经可见了!如果您的预览窗口是打开的,您在处理布局时就能看到膨胀的设计!
如果您在 XML 设计器中更改任何自定义属性(例如将“showTitle
”设置为“false
”),它会立即反映在布局中,正如您所期望的那样。
我们创造了什么
- 我们定义了一个派生自
LinearLayout
的新控件类。 - 我们在可样式化资源中创建了两个自定义属性。
- 我们创建了一个
@interface
,以在代码中以相同的名称反映自定义属性的enum
值。 - 我们通过
AttributeSet
在 Java 代码中访问了自定义属性。 - 我们在控件中填充了一个自定义布局。
还有一件事
第一次使用自定义控件时,当您键入自定义属性的名称时,如果在 XML 中没有 IntelliSense,请不要感到困惑!
您需要知道,您将无法在 android:
命名空间中找到您的属性,也无法在 app:
命名空间中找到。您将使用任何您喜欢的命名空间名称作为前缀(对于此控件,如您在顶部 XML 中所见,我使用了 mbar:
)。
当你开始输入一个新的命名空间名称时,Android Studio 会提示你插入...
xmlns:mbar="http://schemas.android.com/apk/res-auto"
...使用 Alt+Enter。接受它(=按 Alt+Enter)。然后您的自定义属性将带有此前缀。
所以……我们来了!
我希望本文能帮助您迈出自定义控件的第一步,并稍微揭开那部分的面纱。
一如既往地欢迎评论,我将尽力回答任何问题!
历史
- 2017-11-12 - 首次发布
- 2017-11-20 - 错别字修改