Я пытаюсь создать пользовательскую иерархию представлений для Android в Kotlin, и вот чего я хотел бы достичь:
- Неинстанцируемый класс
TileView
, который определяет пользовательское представление с меткой, некоторым текстовым содержимым и значком - в основном определяет макет плитки. - Неинстанцируемый класс
TextInputView
, расширяющий TileView
, который открывает диалоговое окно, позволяющее для ввода текста в представление текстового содержимого родителя. - Реализуемый класс
CommentView
, расширяющий TextInputView
, чтобы позволить пользователю написать комментарий.
Эта иерархия позволила бы мне реализовать другие пользовательские представления, такие как NameView
или PhoneNoView
, простирающиеся от TextInputView
, или, возможно, DateView
с использованием DatePickerDialog
для ввода даты и отобразить его в TileView
.
Я пробовал разные подходы, но все заканчиваются исключениями времени выполнения или предупреждениями IDE. Когда я пытаюсь надуть макет в init {}
-методе TileView
, я получаю следующую ошибку:
Утечка 'this' в конструкторе неконечного класса
Вместо этого я реализовал свой собственный init()
-метод, который можно вызывать и перезаписывать из дочернего представления. Пожалуйста, обратите внимание на мою пояснительную документацию по классу, которая, мы надеемся, может быть удалена, если я найду лучшую / более простую стратегию реализации.
view_tile. xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:minHeight="@dimen/two_line_tile_height">
<ImageView
android:id="@+id/tileIconView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginTop="24dp"
android:contentDescription="@string/cd_icon"
android:src="@drawable/ic_sd_gray_24dp" />
<TextView
android:id="@+id/tileLabelView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/content_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
tools:text="Label" />
<TextView
android:id="@+id/tileContentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tileLabelView"
android:layout_alignStart="@id/tileLabelView"
android:layout_alignEnd="@id/tileLabelView"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:ellipsize="end"
tools:hint="Content" />
<!--
Using singleLine even though it is deprecated,
because we cannot change maxLines at runtime,
while also using ellipsize. See this issue:
https://issuetracker.google.com/issues/36950033
-->
</RelativeLayout>
TileView.kt
import kotlinx.android.synthetic.main.view_tile.view.*
/**
* This is a tile view to be shown in input forms throughout the app.
* The TileView is meant to be extended, and only defines the presentation of the view.
* Since the TileView is abstract and therefore not final, the init method must be called from the
* init method of the child view, to inflate the root layout.
* The views functionality should be implemented in a child view.
*/
abstract class TileView @JvmOverloads internal constructor(context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0) :
FrameLayout(context, attrs, defStyleAttr, defStyleRes), View.OnClickListener {
interface OnContentChangedListener {
fun onContentChanged(view: TileView, text: String?)
}
var onContentChangedListener: OnContentChangedListener? = null
val label: String
get() = tileLabelView.text.toString()
val hint: String
get() = tileContentView.hint.toString()
var text: String?
get() = tileContentView.text.toString()
set(value) {
if (value != tileContentView.text.toString()) {
tileContentView.text = value
onContentChangedListener?.onContentChanged(this, value)
}
}
@CallSuper
open fun init() {
inflate(context, R.layout.view_tile, this)
setOnClickListener(this)
}
fun isPopulated(): Boolean = !text.isNullOrBlank()
protected fun setDrawableRes(@DrawableRes drawableRes: Int) {
val drawable = ContextCompat.getDrawable(context, drawableRes)
tileIconView.setImageDrawable(drawable)
}
protected fun setLabelRes(@StringRes label: Int) {
tileLabelView.text = resources.getString(label)
}
protected fun setHintRes(@StringRes hint: Int) {
tileContentView.hint = resources.getString(hint)
}
protected fun setSingleLine() {
tileContentView.setSingleLine()
}
}
TextInputView.kt
/**
* This is a widget, that when clicked, provides the user with a text input option.
* The text can be obtained by calling getText(), which is a trimmed string.
* Since the TextInputView is abstract and therefore not final, the init method must be called from
* the init method of the child view, to inflate the root layout.
*/
abstract class TextInputView @JvmOverloads internal constructor(context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0) :
TileView(context, attrs, defStyleAttr, defStyleRes) {
open var maxLength = 0
open var keyListener: KeyListener? = null
open var inputType = InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
InputType.TYPE_TEXT_FLAG_MULTI_LINE
@CallSuper
override fun init() {
super.init()
}
override fun onClick(v: View) {
showEditTextDialog()
}
private fun showEditTextDialog() {
val dialog = createEditTextDialog(createEditText())
showDialogWithVisibleKeyboard(dialog)
}
private fun createEditText() = EditText(context)
.apply {
this.setText(super.text)
this.hint = super.hint
setSelection(text.length)
setInputType(this)
setKeyListener(this)
setMaxLength(this)
requestFocus()
}
private fun setInputType(editText: EditText) {
editText.inputType = inputType
}
private fun setKeyListener(editText: EditText) {
keyListener?.let { editText.keyListener = it }
}
private fun setMaxLength(editText: EditText) {
if (maxLength != 0) {
editText.filters = arrayOf(InputFilter.LengthFilter(maxLength))
}
}
private fun createEditTextDialog(editText: EditText) = AlertDialog.Builder(context)
.setTitle(label)
.setPositiveButton(R.string.action_ok) { _, _ ->
text = editText.text.toString().trim()
}
.setNegativeButton(R.string.action_cancel, null)
.create()
.apply {
setDialogContent(editText, this)
}
private fun setDialogContent(content: View, dialog: AlertDialog) {
val margin = resources.getDimensionPixelSize(R.dimen.standard_margin)
dialog.setView(content, margin, margin, margin, 0)
}
private fun showDialogWithVisibleKeyboard(dialog: AlertDialog) {
val window = dialog.window
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
dialog.show()
}
}
CommentView.kt
/**
* This is a widget, that when clicked, provides the user with a comment input option.
* The comment can be obtained by calling getText(), which is a trimmed string.
* Label text and hint text can be set via XML.
*/
class CommentView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0) :
TextInputView(context, attrs, defStyleAttr, defStyleRes) {
init {
super.init()
context.theme.obtainStyledAttributes(attrs, R.styleable.CommentView, defStyleAttr, defStyleRes)
.apply {
try {
val type = getInt(R.styleable.CommentView_commentType, COMMENT)
initLayout(type)
} finally {
recycle()
}
}
setDrawableRes(R.drawable.ic_chat_bubble_outline_gray_24dp)
}
private fun initLayout(type: Int) {
when (type) {
COMMENT -> {
setLabelRes(R.string.comment)
setHintRes(R.string.add_comment)
}
NOTICE -> {
setLabelRes(R.string.notice)
setHintRes(R.string.add_notice)
}
PURPOSE -> {
setLabelRes(R.string.purpose)
setHintRes(R.string.add_purpose)
}
}
}
companion object {
// These values must match those in the attrs declaration
private const val COMMENT = 1
private const val NOTICE = 2
private const val PURPOSE = 3
}
}
Вопросы:
- Я не считаю, что вышеуказанный метод
@CallSuper init()
является лучшим решением, но я не знаю, как реализовать его каким-либо другим способом. Как я могу реализовать это лучше, чтобы достичь того, что я хочу? Можно ли как-то использовать встроенный init {}
-метод без утечки памяти? - Я бы хотел, чтобы переменная
TileView
text
содержала прямую ссылку на поле tileContentView.text
, поэтому я Я могу установить null
-значение и быть уверенным, что всегда получаю пустую строку вместо null
(как стандарт TextView
в настоящее время ведет себя).