【问题标题】:How to create constructor of custom view with Kotlin如何使用 Kotlin 创建自定义视图的构造函数
【发布时间】:2014-01-07 09:07:11
【问题描述】:

我正在尝试在我的 Android 项目中使用 Kotlin。我需要创建自定义视图类。每个自定义视图都有两个重要的构造函数:

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

MyView(Context)用于在代码中实例化视图,MyView(Context, AttributeSet)在从XML膨胀布局时被布局膨胀器调用。

this question 的回答建议我使用带有默认值或工厂方法的构造函数。但这就是我们所拥有的:

工厂方法:

fun MyView(c: Context) = MyView(c, attrs) //attrs is nowhere to get
class MyView(c: Context, attrs: AttributeSet) : View(c, attrs) { ... }

fun MyView(c: Context, attrs: AttributeSet) = MyView(c) //no way to pass attrs.
                                                        //layout inflater can't use 
                                                        //factory methods
class MyView(c: Context) : View(c) { ... }

具有默认值的构造函数:

class MyView(c: Context, attrs: AttributeSet? = null) : View(c, attrs) { ... }
//here compiler complains that 
//"None of the following functions can be called with the arguments supplied."
//because I specify AttributeSet as nullable, which it can't be.
//Anyway, View(Context,null) is not equivalent to View(Context,AttributeSet)

如何解决这个难题?


更新:似乎我们可以使用View(Context, null) 超类构造函数而不是View(Context),因此工厂方法方法似乎是解决方案。但即使那样我也无法让我的代码工作:

fun MyView(c: Context) = MyView(c, null) //compilation error here, attrs can't be null
class MyView(c: Context, attrs: AttributeSet) : View(c, attrs) { ... }

fun MyView(c: Context) = MyView(c, null) 
class MyView(c: Context, attrs: AttributeSet?) : View(c, attrs) { ... }
//compilation error: "None of the following functions can be called with 
//the arguments supplied." attrs in superclass constructor is non-null

【问题讨论】:

  • 在您的工厂方法中,您说 attrs 无处可去,但阻止您传递 null 而不是 attrs?
  • @AndreyBreslav 通常在定义 Android 视图子类构造函数时,我们会调用相应的超类构造函数(如 Java 示例所示)。调用super(context, null)AdapterView 子类有效,但我不确定框架中的所有其他视图类不会有任何副作用,因此能够调用特定的超类构造函数会很好。跨度>
  • @AndreyBreslav 请查看更新。好像你是对的,因为attrs 在超类构造函数中被定义为非空值,所以我不能将空值传递给它。
  • 在目前的 Kotlin 状态下似乎不能很好地解决这个问题,但是您可以尝试默认传递一个空的 AttributeSet 实例...

标签: android constructor kotlin


【解决方案1】:

自 2015 年 3 月 19 日发布的 M11 以来,Kotlin 支持多个构造函数。语法如下:

class MyView : View {
    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
        // ...
    }
 
    constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {}
}

更多信息herehere

编辑:您还可以使用@JvmOverloads 注解,以便 Kotlin 自动为您生成所需的构造函数:

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

但请注意,因为这种方法有时可能会导致意外结果,具体取决于您继承的类如何定义其构造函数。 that article 中给出了对可能发生的情况的很好解释。

【讨论】:

  • ...如果你想在 onFinishInflate():stackoverflow.com/a/3264647/2736039 之后执行 findViewById,我不得不返回 java 发现问题不在 kotlin impl 中
  • 如果禁止子类化,defStyle 有什么用?要么创建类open,要么摆脱defStyle
【解决方案2】:

你应该使用注解JvmOverloads(就像在 Kotlin 1.0 中的样子),你可以这样写代码:

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

这将生成 3 个您最可能想要的构造函数。

引用docs:

对于每个具有默认值的参数,这将生成一个 额外的重载,它有这个参数和所有参数 把它在参数列表中的右边去掉。

【讨论】:

  • "...考虑到这个解决方案并不适用于所有类型的视图。其中一些(例如TextView)需要调用super parent 来初始化样式。” – Custom Views in Android with Kotlin (KAD 06)
  • 正如 travis 报告的那样,使用这种模式是不好的做法,因为您必须在每次创建自定义视图时挖掘父构造函数实现以相应地设置默认属性。好吧,我想如果你警告 doc 就可以了。
  • ...如果你想在 onFinishInflate():stackoverflow.com/a/3264647/2736039 之后执行 findViewById,我不得不返回 java 发现问题不在 kotlin impl 中
  • 如何覆盖两个版本的构造函数?我想以编程方式创建只提供上下文的 CustomView
  • @SazzadHissainKhan 和 @JvmOverloads 和参数的默认值将在 Java 中生成几个单独的构造函数。如果你想调用不同的 super(...) 变体,那么你还需要在 Kotlin 中实现单独的构造函数
【解决方案3】:

使用 kotlin 自定义 View 这是示例代码。

class TextViewLight : TextView {

constructor(context: Context) : super(context) {
    val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
    setTypeface(typeface)
}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
    setTypeface(typeface)
}

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
    setTypeface(typeface)
}

}

【讨论】:

    【解决方案4】:

    TL;DR 大多数时候,只需将自定义视图定义为:

    class MyView(context: Context, attrs: AttributeSet?) : FooView(context, attrs)
    

    鉴于此 Java 代码:

    public final class MyView extends View {
        public MyView(Context context) {
            super(context);
        }
    
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }
    

    它的 Kotlin 等价物将使用辅助构造函数:

    class MyView : View {
        constructor(context: Context) : super(context)
    
        constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    }
    

    当您真的想根据视图是在代码中创建还是从 XML 扩展来调用不同的超类构造函数时,该语法很有用。我所知道的唯一情况是当您直接扩展 View 类时。

    您可以使用带有默认参数和@JvmOverloads 注释的主构造函数:

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

    如果您不打算从 Java 调用它,则不需要 @JvmOverloads constructor

    并且如果您只从 XML 中扩充视图,那么您可以使用最简单的方法

    class MyView(context: Context, attrs: AttributeSet?) : View(context, attrs)
    

    如果你的类是 open 用于扩展并且你需要保留父类的样式,你想回到只使用辅助构造函数的第一个变体:

    open class MyView : View {
        constructor(context: Context) : super(context)
        constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
        constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
    }
    

    但是,如果您想要一个覆盖父样式并让其子类也覆盖它的 open 类,那么您应该可以使用 @JvmOverloads

    open class MyView @JvmOverloads constructor(
            context: Context,
            attrs: AttributeSet? = null,
            defStyleAttr: Int = R.attr.customStyle,
            defStyleRes: Int = R.style.CustomStyle
    ) : View(context, attrs, defStyleAttr, defStyleRes)
    

    【讨论】:

      【解决方案5】:

      这似乎是个问题。我从来没有遇到过这种情况,因为我的自定义视图要么仅在 xml 中创建,要么仅在代码中创建,但我可以看到这会出现在哪里。

      据我所知,有两种解决方法:

      1) 使用带有属性的构造函数。在 xml 中使用视图可以正常工作。在代码中,您需要使用视图所需的标签来扩充 xml 资源,并将其转换为属性集:

      val parser = resources.getXml(R.xml.my_view_attrs)
      val attrs = Xml.asAttributeSet(parser)
      val view = MyView(context, attrs)
      

      2) 使用没有属性的构造函数。不能直接在xml中放置视图,但是在xml中放置一个FrameLayout并通过代码添加视图很容易。

      【讨论】:

      • 感谢您的建议,它们似乎是一个解决方案,但我希望有办法保留两个构造函数。
      • 你能提供一个 R.xml.my_view_attrs 的例子吗?
      • Kotlin M11 添加了对多个构造函数的支持:blog.jetbrains.com/kotlin/2015/03/kotlin-m11-is-out
      【解决方案6】:

      有几种方法可以覆盖你的构造函数,

      当您需要默认行为时

      class MyWebView(context: Context): WebView(context) {
          // code
      }
      

      当您需要多个版本时

      class MyWebView(context: Context, attr: AttributeSet? = null): WebView(context, attr) {
          // code
      }
      

      当你需要在里面使用参数时

      class MyWebView(private val context: Context): WebView(context) {
          // you can access context here
      }
      

      当您想要更简洁的代码以获得更好的可读性时

      class MyWebView: WebView {
      
          constructor(context: Context): super(context) {
              mContext = context
              setup()
          }
      
          constructor(context: Context, attr: AttributeSet? = null): super(context, attr) {
              mContext = context
              setup()
          }
      }
      

      【讨论】:

        【解决方案7】:

        添加了通过使用多个构造函数扩展 XML 布局来创建自定义视图的完整示例

        class MyCustomView : FrameLayout {
            private val TAG = MyCustomView ::class.simpleName
        
            constructor(context: Context): super(context) {
                initView()
            }
        
            constructor(context: Context, attr: AttributeSet? = null): super(context, attr) {
                initView()
            }
        
            constructor(
                context: Context,
                attrs: AttributeSet?,
                defStyleAttr: Int
            ):   super(context, attrs, defStyleAttr) {
                initView()
            }
        
            /**
             * init View Here
             */
            private fun initView() {
               val rootView = (context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
                    .inflate(R.layout.layout_custom_view, this, true)
        
               // Load and use rest of views here
               val awesomeBG= rootView.findViewById<ImageView>(R.id.awesomeBG)
              
        }
        

        在 XML 中添加您的 layout_custom_view 视图文件

        <?xml version="1.0" encoding="utf-8"?>
        <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        
          
            <ImageView
                android:id="@+id/awesomeBG"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:contentDescription="@string/bg_desc"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop" />
        
            <!--ADD YOUR VIEWs HERE-->
         
           </FrameLayout>
        

        【讨论】:

          【解决方案8】:

          您可以为 JetBrains 的 Kotlin 尝试新的库 Anko(您也可以在 github 上做出贡献)。 目前它处于测试阶段,但您可以使用此类代码创建视图

              button("Click me") {
                   textSize = 18f
                   onClick { toast("Clicked!") }
              }
          

          看看这个库

          【讨论】:

          • 这不是一个错误的答案,但它与问题无关=)
          猜你喜欢
          • 1970-01-01
          • 2011-02-22
          • 2023-01-22
          • 1970-01-01
          • 2013-04-28
          • 2021-02-27
          • 1970-01-01
          • 2016-07-02
          • 1970-01-01
          相关资源
          最近更新 更多