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

79

我正在尝试在我的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填充布局时被布局膨胀器调用。

回答这个问题的建议是使用具有默认值的构造函数或工厂方法。但以下是我们现在所拥有的:

工厂方法:

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? - Andrey Breslav
1
通常在定义Android视图子类的构造函数时,我们会调用相应的超类构造函数(就像Java示例中所示)。对于AdapterView子类,调用super(context, null)是有效的,但我不确定在框架中的所有其他视图类中是否会产生任何副作用,因此最好能够调用特定的超类构造函数。 - Andrii Chernenko
1
@AndreyBreslav请看一下更新。似乎你是对的,因为attrs在超类构造函数中被定义为非空,我不能将null传递给它。 - Andrii Chernenko
看起来在目前的 Kotlin 状态下无法很好地解决这个问题,但您可以尝试默认传递一个空的 AttributeSet 实例... - Andrey Breslav
10个回答

105

Kotlin自M11版本(于2015年3月19日发布)开始支持多个构造函数,语法如下:

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

更多信息在这里在这里

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

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

需要注意的是,这种方法有时可能会导致意想不到的结果,具体取决于您继承的类如何定义其构造函数。关于可能发生的情况有很好的解释,请参阅该文章


...如果你想要执行findViewById,在onFinishInflate()之后操作:https://dev59.com/qHA75IYBdhLWcg3wfpKr#3264647。我不得不返回Java代码,才发现问题不是在Kotlin实现中。 - Ultimo_m
如果禁止子类化,那么defStyle有什么用处呢?要么将类设置为open,要么摆脱defStyle - arekolek

72

你应该使用注解JvmOverloads(就像在Kotlin 1.0中看起来一样),你可以编写如下代码:

-->

使用注解JvmOverloads(与 Kotlin 1.0 中相似),您可以编写以下代码:

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

这将生成 3 个构造函数,正如您最可能想要的那样。

引自文档

对于每个具有默认值的参数,将生成一个额外的重载,该重载删除此参数及其右侧所有参数。


4
请注意,这种解决方案并不适用于所有类型的视图。其中一些(例如TextView)需要调用super父类来初始化样式。 - Travis
正如 Travis 报告的那样,使用这种模式是不好的做法,因为每次创建自定义视图都必须深入研究父构造函数实现以相应地设置默认属性。嗯,我猜如果你警告一下文档的话还可以接受。 - PedroCactus
如果你想要使用findViewById,请在onFinishInflate()之后执行:https://dev59.com/qHA75IYBdhLWcg3wfpKr#3264647,我不得不回到Java才发现问题不在Kotlin实现中。 - Ultimo_m
我该如何重写两种构造函数的版本?我想要创建一个仅提供上下文的CustomView程序。 - Sazzad Hissain Khan
使用 @JvmOverloads 和参数默认值,Java 将生成多个单独的构造函数。如果您想调用不同的 super(...) 变体,则还需要在 Kotlin 中实现单独的构造函数。 - colriot
如果您不将类设置为“open”,为什么需要“defStyle”参数呢?只需摆脱它并避免可能引起的问题即可。 - arekolek

15

这里提供了使用 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)
}

}

11

简而言之:大多数情况下,只需要将自定义视图定义为以下内容即可:

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

但是如果您想要一个开放的类,覆盖父类样式并允许其子类也覆盖它,那么您可以使用@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) 使用没有attrs的构造函数。你不能直接在xml中放置视图,但是通过代码将视图添加到FrameLayout中很容易。


1
谢谢您的建议,它们似乎是一个解决方案,但我希望有一种方法可以保留两个构造函数。 - Andrii Chernenko
1
你能提供一个 R.xml.my_view_attrs 的例子吗? - Tudor Luca
2
说句题外话,Kotlin M11已经支持多个构造函数了:http://blog.jetbrains.com/kotlin/2015/03/kotlin-m11-is-out/ - ajselvig

1
有几种方法可以覆盖您的构造函数,
当您需要默认行为时
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()
    }
}

0

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

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)

0

当您有一些视图(BottomSheetDialog)已经可以显示文本并且想要添加格式化字符串时,您应该添加两个构造函数。

class SomeDialog : BottomSheetDialog {

    private val binding = DialogSomeBinding.inflate(layoutInflater)

    // Base constructor that cannot be called directly
    private constructor(
        context: Context,
        title: CharSequence
    ) : super(context) {
        setContentView(binding.root)
        binding.title.text = title
    }

    // Constructor with simple CharSequence message
    constructor(
        context: Context,
        title: CharSequence,
        message: CharSequence
    ) : this(context, title) {
        binding.message.text = message
    }

    // Constructor with formatted SpannableString message
    constructor(
        context: Context,
        title: CharSequence,
        message: SpannableString
    ) : this(context, title) {
        binding.message.text = message
    }
}

使用方法:

val span = SpannableString(getString(R.string.message, name))
...

SomeDialog(
    context = requireContext(),
    title = getString(R.string.title),
    message = span
).show()

0

添加了一个完整的示例,演示如何通过使用多个构造函数来从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>

-6
你可以尝试来自JetBrains的Kotlin新库Anko(同时你也可以在github上做出贡献)。 目前它还处于beta版本,但你可以使用以下代码创建视图。
    button("Click me") {
         textSize = 18f
         onClick { toast("Clicked!") }
    }

看看这个库


1
这不是一个错误的答案,但它与问题无关 =) - Arda Kaplan
Anko已被弃用,请删除这个无关的回答。 - CoolMind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接