如何使用 Kotlin 创建自定义视图的构造函数

我想在我的安卓项目中使用 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 扩充布局时由布局扩充程序调用。

这个问题的回答建议我使用具有默认值或工厂方法的构造函数:

工厂方法:

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
59303 次浏览

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

在我看来,有两种方法可以解决这个问题:

1)在 attrs 中使用构造函数。在 xml 中使用视图可以很好地工作。在代码中,需要使用视图所需的标记来充实 xml 资源,并将其转换为属性集:

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

2)使用不带 attrs 的构造函数。您不能将视图直接放置在 xml 中,但是可以很容易地将 FrameLayout 放置在 xml 中并通过代码将视图添加到其中。

Kotlin 支持多个构造函数,自从19.03.2015发布的 M11以来,语法如下:

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

constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {}
}

更多信息 给你给你

编辑 : 您也可以使用@jvmOverload 注释,这样 Kotlin 就可以为您自动生成所需的构造函数:

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

但是要注意,这种方法有时可能会导致意外的结果,这取决于您从继承的类如何定义其构造函数。在 那篇文章中对可能发生的情况给出了很好的解释。

你可以从 JetBrains 尝试新的 Library安科 for Kotlin (你也可以在 Github上做贡献)。 目前它还处于测试阶段,但是您可以使用这样的代码创建视图

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

看看这个图书馆

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

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

这将生成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)
}


}

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

当您需要默认行为时

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

当你需要多个版本

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

当需要在内部使用 params 时

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()
}
}

在大多数情况下,只需将自定义视图定义为:

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)

添加了一个通过使用多个构造函数扩展 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>

看起来,构造函数参数是由 类型和顺序修正的,但是我们可以像这样添加自己的:

class UpperMenu @JvmOverloads
constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,parentLayout: Int,seeToolbar: Boolean? = false)


: Toolbar(context, attrs, defStyleAttr) {}

parentLayout ,seeToolbar加入其中,以便:

 val upper= UpperMenu (this,null,0,R.id.mainParent, true)