【问题标题】:Do I need all three constructors for an Android custom view?Android 自定义视图是否需要所有三个构造函数?
【发布时间】:2012-03-01 00:42:24
【问题描述】:

在创建自定义视图时,我注意到很多人似乎是这样操作的:

public MyView(Context context) {
  super(context);
  // this constructor used when programmatically creating view
  doAdditionalConstructorWork();
}

public MyView(Context context, AttributeSet attrs) {
  super(context, attrs);
  // this constructor used when creating view through XML
  doAdditionalConstructorWork();
}

private void doAdditionalConstructorWork() {

  // init variables etc.
}

我的第一个问题是,构造函数MyView(Context context, AttributeSet attrs, int defStyle) 呢?我不确定它在哪里使用,但我在超类中看到它。我需要它吗?它在哪里使用?

有another part to this question。

【问题讨论】:

    标签: android android-custom-view


    【解决方案1】:

    如果您要从xml 添加您的自定义View 也喜欢:

     <com.mypack.MyView
          ...
          />
    

    您将需要构造函数 public MyView(Context context, AttributeSet attrs),否则当 Android 尝试膨胀您的 View 时,您将得到一个 Exception。

    如果您从xml 添加您的View 并指定android:style 属性,例如:

     <com.mypack.MyView
          style="@styles/MyCustomStyle"
          ...
          />
    

    在应用显式 XML 属性之前,还将调用第二个构造函数并将样式默认为 MyCustomStyle。

    当您希望应用程序中的所有视图都具有相同的样式时,通常使用第三个构造函数。

    【讨论】:

    • 那么什么时候使用第一个构造函数?
    • @OvidiuLatcu 能否请您展示第三个 CTOR 的示例(带有 3 个参数)?
    • 我可以在构造函数中添加额外的参数吗?如何使用它们?
    • 关于第三个构造函数,这实际上是完全错误的。 XML 总是 调用两个参数的构造函数。三参数(和four-argument)构造函数由子类调用,如果他们想指定一个包含默认样式的属性,或者直接指定一个默认样式(在四参数构造函数的情况下)
    • 我刚刚提交了一个编辑以使答案正确。我还在下面提出了一个替代答案。
    【解决方案2】:

    如果你必须包含三个像现在讨论的那样的构造函数,你也可以这样做。

    public MyView(Context context) {
      this(context,null,0);
    }
    
    public MyView(Context context, AttributeSet attrs) {
      this(context,attrs,0);
    }
    
    public MyView(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      doAdditionalConstructorWork();
    
    }
    

    【讨论】:

    • @Jin 这在很多情况下是个好主意,但在很多情况下这也是安全的(例如:RelativeLayout、FrameLayout、RecyclerView 等)。所以,我想说这可能是一个个案要求,在决定是否级联之前应该检查基类。本质上,如果基类中的 2-param 构造函数只是调用 this(context, attrs, 0),那么在自定义视图类中这样做也是安全的。
    • @IanWong,当然会调用,因为第一个和第二个方法在调用第三个。
    【解决方案3】:

    如果您覆盖所有三个构造函数,请不要 CASCADE this(...) CALLS。你应该这样做:

    public MyView(Context context) {
        super(context);
        init(context, null, 0);
    }
    
    public MyView(Context context, AttributeSet attrs) {
        super(context,attrs);
        init(context, attrs, 0);
    }
    
    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs, defStyle);
    }
    
    private void init(Context context, AttributeSet attrs, int defStyle) {
        // do additional work
    }
    

    原因是父类可能在其自己的构造函数中包含默认属性,您可能会不小心覆盖这些属性。例如这是TextView的构造函数:

    public TextView(Context context) {
        this(context, null);
    }
    
    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }
    
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }
    

    如果你没有调用super(context),你就没有正确设置R.attr.textViewStyle作为样式属性。

    【讨论】:

    • 这是扩展 ListView 时的基本建议。作为上述 this-cascading 的(以前的)粉丝,我记得我花了数小时追踪一个微妙的错误,当我为每个构造函数调用正确的超级方法时,该错误消失了。
    • 顺便说一句 @Jin 我在这个答案中使用了代码:stackoverflow.com/a/22780035/294884 这似乎是基于你的答案 - 但请注意作者包括使用充气机?
    • 我认为没有必要在所有构造函数中调用 init,因为当您遵循调用层次结构时,无论如何您都会在默认构造函数中以编程方式创建视图 View(Context context){}
    • 我在做同样的事情,但未能在我的自定义视图中可用的 textview 中设置值我想从活动中设置值
    • 我怎么不知道这个?
    【解决方案4】:

    MyView(上下文上下文)

    在以编程方式实例化视图时使用。

    MyView(上下文上下文,AttributeSet attrs)

    LayoutInflater 使用它来应用 xml 属性。如果此属性之一被命名为style,则属性将在样式中查找,然后再在布局 xml 文件中查找显式值。

    MyView(上下文上下文, AttributeSet attrs, int defStyleAttr)

    假设您想对所有小部件应用默认样式,而不必在每个布局文件中指定style。例如,默认情况下将所有复选框设为粉红色。您可以使用 defStyleAttr 执行此操作,框架将在您的主题中查找默认样式。

    请注意,defStyleAttr 前一段时间被错误地命名为 defStyle,关于这个构造函数是否真的需要有一些讨论。见https://code.google.com/p/android/issues/detail?id=12683

    MyView(上下文上下文, AttributeSet attrs, int defStyleAttr, int defStyleRes)

    如果您可以控制应用程序的基本主题,则第三个构造函数可以很好地工作。这适用于谷歌,因为他们将小部件与默认主题一起发送。但是假设您正在编写一个小部件库,并且您希望设置一个默认样式,而不需要您的用户调整他们的主题。您现在可以使用defStyleRes 来执行此操作,方法是将其设置为前两个构造函数中的默认值:

    public MyView(Context context) {
      super(context, null, 0, R.style.MyViewStyle);
      init();
    }
    
    public MyView(Context context, AttributeSet attrs) {
      super(context, attrs, 0, R.style.MyViewStyle);
      init();
    }
    

    总之

    如果您要实现自己的视图,则只需要前 2 个构造函数,并且可以由框架调用。

    如果您希望您的视图是可扩展的,您可以为您的类的孩子实现第四个构造函数,以便能够使用全局样式。

    我没有看到第三个构造函数的真正用例。如果您没有为小部件提供默认样式但仍希望您的用户能够这样做,则可能是一个快捷方式。不应该发生那么多。

    【讨论】:

      【解决方案5】:

      第三个构造函数要复杂得多。我举个例子。

      Support-v7 SwitchCompact 包支持thumbTint 和trackTint 属性,因为 24 版本而 23 版本不支持它们。现在您想在 23 版本中支持它们,您将如何实现这一目标?

      我们假设使用自定义视图 SupportedSwitchCompact 扩展 SwitchCompact。

      public SupportedSwitchCompat(Context context) {
          this(context, null);
      }
      
      public SupportedSwitchCompat(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
      
      public SupportedSwitchCompat(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          init();
      }
      
      private void init(){
          mThumbDrawable = getThumbDrawable();
          mTrackDrawable = getTrackDrawable();
          applyTint();
      }
      

      这是一种传统的代码风格。请注意,我们在这里将 0 传递给第三个参数。运行代码你会发现getThumbDrawable()总是返回null是多么的奇怪,因为方法getThumbDrawable()是它的超类SwitchCompact的方法。

      如果你将R.attr.switchStyle 传递给第三个参数,一切顺利。那为什么?

      第三个参数是一个简单的属性。该属性指向一个样式资源。在上述情况下,系统会在当前主题中找到switchStyle属性,幸运的是系统找到了它。

      在frameworks/base/core/res/res/values/themes.xml,你会看到:

      <style name="Theme">
          <item name="switchStyle">@style/Widget.CompoundButton.Switch</item>
      </style>
      

      【讨论】:

        【解决方案6】:

        Kotlin 似乎消除了很多这种痛苦:

        class MyView
        @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
            : View(context, attrs, defStyle)
        

        @JvmOverloads 将生成所有必需的构造函数(参见该注解的 documentation),其中每个构造函数都可能调用 super()。然后,只需用 Kotlin init {} 块替换您的初始化方法。样板代码不见了!

        【讨论】:

        • 使用这种方法,您的类定义默认参数值,以防使用 1 或 2 个参数调用构造函数,而 stackoverflow.com/a/39280344/2011622 认为直接调用具有相同数量参数的超级构造函数更安全所以超类有责任提供默认值。简洁的语法很诱人,但不是推荐的方法。
        猜你喜欢
        • 2011-02-22
        • 2013-10-25
        • 1970-01-01
        • 2014-04-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多