简体   繁体   English

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

[英]How to create constructor of custom view with Kotlin

I'm trying to use Kotlin in my Android project.我正在尝试在我的 Android 项目中使用 Kotlin。 I need to create custom view class.我需要创建自定义视图类。 Each custom view has two important constructors:每个自定义视图都有两个重要的构造函数:

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

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

MyView(Context) is used to instantiate view in code, and MyView(Context, AttributeSet) is called by layout inflater when inflating layout from XML. MyView(Context)用于在代码中实例化视图, MyView(Context, AttributeSet)在从 XML 扩充布局时由布局扩充器调用。

Answer to this question suggests that I use constructor with default values or factory method. 这个问题的答案表明我使用具有默认值的构造函数或工厂方法。 But here's what we have:但这就是我们所拥有的:

Factory method:工厂方法:

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

or要么

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

Constructor with default values:具有默认值的构造函数:

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)

How can this puzzle be resolved?如何解决这个难题?


UPDATE: Seems like we can use View(Context, null) superclass constructor instead of View(Context) , so factory method approach seems to be the solution.更新:似乎我们可以使用View(Context, null)超类构造函数而不是View(Context) ,因此工厂方法方法似乎是解决方案。 But even then I can't get my code to work:但即使那样我也无法让我的代码工作:

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

or要么

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

Kotlin supports multiple constructors since M11 which was released 19.03.2015.自 2015 年 3 月 19 日发布的 M11 以来,Kotlin 支持多个构造函数。 The syntax is as follows:语法如下:

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

More info here and here .更多信息 在这里这里

Edit : you can also use @JvmOverloads annotation so that Kotlin auto-generates the required constructors for you:编辑:您还可以使用 @JvmOverloads 注释,以便 Kotlin 为您自动生成所需的构造函数:

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

Beware, though, as this approach may sometimes lead to the unexpected results, depending on how the class you inherit from defines its constructors.但是请注意,这种方法有时可能会导致意外结果,具体取决于您继承的类如何定义其构造函数。 Good explanation of what might happen is given in that article . 那篇文章对可能发生的事情进行了很好的解释。

You should use annotation JvmOverloads (as it looks like in Kotlin 1.0), you can write code like this:您应该使用注释JvmOverloads (就像在 Kotlin 1.0 中一样),您可以编写如下代码:

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

This will generate 3 constructors just as you most likely wanted.这将按照您最可能想要的方式生成 3 个构造函数。

Quote from docs :来自文档的引用:

For every parameter with a default value, this will generate one additional overload, which has this parameter and all parameters to the right of it in the parameter list removed.对于每个具有默认值的参数,这将生成一个额外的重载,该重载将删除该参数及其右侧的所有参数列表中的参数。

Custome View with kotlin here's sample code.带有 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)
}

}

This does seem to be an issue.这似乎是一个问题。 I've never run into this because my custom views have either been created only in xml or only in code, but I can see where this would come up.我从来没有遇到过这种情况,因为我的自定义视图要么只在 xml 中创建,要么只在代码中创建,但我可以看到这会出现在哪里。

As far as I can see, there are two ways around this:据我所知,有两种方法可以解决这个问题:

1) Use constructor with attrs. 1) 使用带属性的构造函数。 Using the view in xml will work fine.使用 xml 中的视图可以正常工作。 In code, you need to inflate an xml resource with the desired tags for your view, and convert it to an attribute set:在代码中,您需要使用您的视图所需的标签来扩充 xml 资源,并将其转换为属性集:

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

2) Use the constructor without attrs. 2) 使用没有 attrs 的构造函数。 You can't place the view directly in your xml, but it's easy about to place a FrameLayout in the xml and add the view to it through code.您不能将视图直接放置在 xml 中,但是很容易将 FrameLayout 放置在 xml 中并通过代码将视图添加到其中。

TL;DR most of the time, it should be enough to just define your custom view as: TL;DR大多数时候,只需将自定义视图定义为:

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

Given this Java code:鉴于此 Java 代码:

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

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

its Kotlin equivalent would use secondary constructors:它的 Kotlin 等效项将使用辅助构造函数:

class MyView : View {
    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
}

That syntax is useful when you really want to call different super-class constructors depending on whether the view is created in code or inflated from XML.当您确实想根据视图是在代码中创建还是从 XML 膨胀来调用不同的超类构造函数时,该语法很有用。 The only case that I know of for this to be true is when you are extending the View class directly.我所知道的唯一情况是当您直接扩展View类时。

You can use a primary constructor with default arguments and a @JvmOverloads annotation otherwise:否则,您可以使用带有默认参数和@JvmOverloads注释的主构造函数:

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

You don't need @JvmOverloads constructor if you don't plan to call it from Java.如果您不打算从 Java 调用它,则不需要@JvmOverloads constructor

And if you only inflate views from XML, then you can just go with the simplest :如果您只从 XML 膨胀视图,那么您可以使用最简单的方法

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

If your class is open for extension and you need to retain the style of the parent, you want to go back to the first variant that uses secondary constructors only:如果您的类对扩展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)
}

But if you want an open class that overrides the parent style and lets its subclasses override it too, you should be fine with @JvmOverloads :但是如果你想要一个覆盖父样式并让它的子类也覆盖它的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)

There are several ways to override your constructors,有几种方法可以覆盖构造函数,

When you need default behavior当您需要默认行为时

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

When you need multiple version当您需要多个版本时

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

When you need to use params inside当你需要在里面使用 params 时

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

When you want cleaner code for better readability当您想要更简洁的代码以提高可读性时

class MyWebView: WebView {

    constructor(context: Context): super(context) {
        mContext = context
        setup()
    }

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

super() gives me Primary constructor call expected (Kotlin 1.3.61 ). super()给了我Primary constructor call expected (Kotlin 1.3.61 )。

class SomeView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
): View(context, attrs, defStyleAttr, defStyleRes) {
    constructor(context: Context) : this(context, null, 0, 0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)
}

Added a complete example of creating a custom view by inflating XML layout with multiple constructors添加了通过使用多个构造函数膨胀 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)
      
}

in XML add your layout_custom_view view file在 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>

It seems, constructor parameters are fixed by type and order , but we can add own like this:看起来,构造函数参数是由类型和顺序固定的,但我们可以这样添加自己的:

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

    : Toolbar(context, attrs, defStyleAttr) {}

where parentLayout,seeToolbar are added to it so:parentLayout,seeToolbar添加到其中:

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

When you have some view (BottomSheetDialog) that already can show text and want to add formatted string, you should add two constructors.当您有一些已经可以显示文本并想要添加格式化字符串的视图(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
    }
}

Usage:用法:

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

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

You can try new Library Anko for Kotlin from JetBrains (also you can contribute on github ).您可以从 JetBrains 尝试 Kotlin 的新库Anko (您也可以在github 上做出贡献)。 Currently it is in beta, but you can create views with such code目前它处于测试阶段,但您可以使用此类代码创建视图

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

Have a look at this library看看这个图书馆

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM