65.9K
CodeProject 正在变化。 阅读更多。
Home

创建自己的 Android 自定义控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (5投票s)

2017 年 11 月 12 日

CPOL

10分钟阅读

viewsIcon

18851

使用 Java 和 XML 在 Android 中创建具有自定义属性的自定义控件。

引言

在 Android 中,我们有很多方法可以将布局“分组”在一起,使其可重用。在我们的 XML 设计中有一个 <include> 标签(以及鲜为人知的 <merge>),它只是将另一个布局包含到当前布局中;还有 Fragments;当然,我们可以随时从任何基类派生,例如 ViewLinearLayout

本文将更深入地探讨后一种方法。

  • 如何编写一个可以膨胀自己布局的控件?
  • 如何为我的控件创建自定义属性?
  • 这些如何与样式和主题混合在一起?

本文的范围是膨胀自己布局的控件,因此我们将从匹配的基类派生,具体是 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 中指定的属性,*包括我们的自定义属性* barSizeshowTitle。所以这已经揭示了第一个谜团:我们如何将 XML 中的值获取到我们的控件中?答案是:通过 AttributeSet。我们如何从中获取值将在讨论 init(...) 方法时介绍。

将您的 XML 枚举值映射到代码值

我非常喜欢这些 @interfaces,所以我为 barSize 创建了一个。如果您不知道那是什么:它或多或少是一种将整数或字符串组合在一起的替代方法。Android SDK 在很多地方都这样做,您也遇到过它们。举个简单的例子:无论何时将某物设置为 View.VISIBLEView.GONE,您都在接触其中之一。

在类的顶部,您可以看到声明

 private @MbarBarSize int barSize = MbarBarSize.SMALL;

这与 XML 设计中的这一行相关

mbar:barSize="small"

当我们稍后讨论这个自定义属性的声明时,您会看到术语“small”和“large”代表值“0”和“1”。现在,我们当然希望在 Java 中以相同的名称(SMALL 和 LARGE)处理这些值,并且我们不想使用 01

那么这个 @MbarBarSize 是什么?它有什么作用? @annotation 告诉我们,这个 int 变量只能保存 @MbarBarSize 中定义的值。如果您尝试分配其他值,您会收到 Lint 警告。

为了声明成员的此类限制,@interface 声明就派上了用场。MbarBarSize 定义如下

@Retention(RetentionPolicy.CLASS)
public @interface MbarBarSize {
   int SMALL = 0;
   int LARGE = 1;
}

通过这样的声明,您可以为通常也允许其他值的数据类型分配我称之为 value-restrictionvalue-constraint 的东西。

保留策略 (RetentionPolicy)

重要的是您要了解何时使用哪种策略。有三种策略可用

  • CLASS: (默认)。此策略*可以*在任何地方使用,但我主要在库中使用它。Class 意味着,这个 @interface 将在编译后仍然存在,并对您的库用户可用。他们可以使用 SMALLLARGE 值,就好像他们在自己的应用程序/库中定义了它们一样。这是您想要的,如果您需要它作为方法参数,并且希望 lint 在用户分配不允许的值时能够警告您的库用户。在*库*中,您*希望*您的值以给定的名称使用。您不希望强制您的库用户将“0”和“1”作为参数值提供给您的方法。他们也应该使用 SMALLLARGE
  • SOURCE:此策略告诉编译器放弃定义。对于人类思维而言,这意味着:当您定义一个不需要在编译过程中存活的 @interface 时,可以使用 SOURCE 策略。举个例子:在您的应用程序中,当您的应用程序之外没有任何东西需要访问具有给定名称的值(在上述示例中为 SMALLLARGE)。在您当前项目之外,SMALLLARGE 是*未知*的。用户必须提供“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 的整数表示形式的 01。您的控件的用户可以在 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=SMALLshowTitle=true(参见本文第一个代码块 - 构造函数,这是类成员的设置方式)。

这里最重要的部分是从 XML 获取属性。这是通过 obtainStyledAttributes 方法完成的,该方法与我们 context 的当前主题相关联。由于我们从 LinearLayout 派生,我们有一个 getContext() 方法可用,所以我们不需要参数或其他方式来获取有效的上下文。我们已经有一个了。

参数如下:

  • attrs。这是我们想要获取值的 AttributeSet。它是构造函数中提供的那个。
  • R.styleable.MbarBar。我相信,到目前为止,您已经知道那是什么了。我们在 *attrs.xml* 中定义的自定义属性。我们想要获取的正是*这些*值。
  • defStyleAttrRes。一切都经过主题和样式设计。Android 将应用当前主题和样式的任何修改,并将其考虑在我们将获得的值中。

此调用之后,我们可以像访问 BundleExtras 一样轻松地访问我们的属性。通过 getIntgetBoolgetWhatYouNeed。非常简单的接口。这些调用中的第二个参数是未找到时的默认值。

紧随其后的是一个 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 - 错别字修改
© . All rights reserved.