Как создать конструктор нестандартного представления с Kotlin
Я пытаюсь использовать Kotlin в своем Android-проекте. Мне нужно создать собственный класс представления. Каждый пользовательский вид имеет два важных конструктора:
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.
Ответ на этот вопрос предполагает, что я использую конструктор со значениями по умолчанию или factory. Но вот что мы имеем:
Factory:
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)
Как решить эту проблему?
UPDATE: Похоже, мы можем использовать конструктор View(Context, null)
суперкласса вместо View(Context)
, поэтому метод метода factory представляется решением. Но даже тогда я не могу заставить свой код работать:
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
Ответы
Ответ 1
Kotlin поддерживает несколько конструкторов начиная с M11, который был выпущен 19.03.2015. Синтаксис выглядит следующим образом:
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)
Однако остерегайтесь, так как этот подход может иногда приводить к неожиданным результатам, в зависимости от того, как класс, который вы наследуете, определяет его конструкторы. Хорошее объяснение того, что может произойти, дается в этой статье.
Ответ 2
Вы должны использовать аннотацию JvmOverloads
(как это выглядит в Kotlin 1.0), вы можете написать код следующим образом:
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attrs, defStyle)
Это создаст 3 конструктора, как вы, скорее всего, захотите.
Цитата из docs:
Для каждого параметра со значением по умолчанию это будет генерировать один дополнительная перегрузка, которая имеет этот параметр и все параметры для справа от него в списке параметров удалено.
Ответ 3
Custome View
с kotlin здесь пример кода.
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
Это похоже на проблему. Я никогда не сталкивался с этим, потому что мои пользовательские представления либо были созданы только в 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 и добавить представление к нему через код.
Ответ 5
Есть несколько способов переопределить ваши конструкторы,
Когда вам нужно поведение по умолчанию
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()
}
}
Ответ 6
Учитывая этот код Java:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
его котлинский эквивалент будет использовать вторичные конструкторы:
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)
И если вы только раздуваете представления из 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)
Ответ 7
Вы можете попробовать новую библиотеку Anko для Kotlin из JetBrains (также вы можете внести вклад в github).
В настоящее время он находится в бета-версии, но вы можете создавать представления с таким кодом
button("Click me") {
textSize = 18f
onClick { toast("Clicked!") }
}
Посмотрите на эту библиотеку