В этой статье пойдет речь о вынисении UI в отдельный блок, компоновкой стандартных элементов.
Я расскажу о проблемах с которыми встретился сам, для искушенных пользователей, прилагаю ссылку на более подробный ресурс.
Основной пример будет рассмотрен на простой задаче когда нам необходим Switch в котором будет и текст и описание.
Как создать что-то подобное и не нагружать верстку ?
По началу может показаться что это муторно, однако в больших проектах без этого не обойтись, иначе будет много вложенностей и код постепенно станет не читабельным.
первое что потребуется сделать это выделить один элемент и вынести верстку из файла xml.
Создаем файл CusomSwitch.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="22dp"
android:paddingEnd="22dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.example.customviewblock.CustomSwitch
android:id="@+id/first"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="false"
app:cs_subtitle="SubTitle"
android:title="Title" />
<com.example.customviewblock.CustomSwitch
android:id="@+id/second"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:checked="true"
app:cs_subtitle="SubTitle"
android:title="Title" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Теперь создаем в попке с UI папку view (для хранения кодовой части ваших кастомных view). И создаем файл CustomSwitch.kt
class CustomSwitch (
context: Context,
attributeSet: AttributeSet
) : LinearLayout(context, attributeSet) {
private var binding: CustomSwitchBinding =
CustomSwitchBinding.inflate(LayoutInflater.from(context), this, false)
var addOnChangeListener: ((Boolean) -> Unit)? = null
var isChecked: Boolean
get() = binding.switchSC.isChecked
set(value) {
binding.switchSC.isChecked = value
}
init {
addView(binding.root)
val typedArray = context.theme.obtainStyledAttributes(
attributeSet,
R.styleable.CustomSwitch, 0, 0
)
binding.apply {
val title = typedArray.getString(R.styleable.CustomSwitch_android_title)
val subTitle = typedArray.getString(R.styleable.CustomSwitch_cs_subtitle)
val isChecked = typedArray.getBoolean(R.styleable.CustomSwitch_android_checked, false)
switchSC.isChecked = isChecked
titleTV.text = title
subTitleTV.text = subTitle
switchSC.setOnCheckedChangeListener { _, _isChecked ->
addOnChangeListener?.invoke(_isChecked)
}
}
}
fun setTitle(text: String){
binding.titleTV.text = text
}
fun setSubTitle(text: String) {
binding.subTitleTV.text = text
}
}
Сейчас этот файл будет ругаться на то что он не может найти R.styleable.CustomSwitch.
В этом файле вы будете указывать какие атрибуты можно задать для вашего кастового view в файле xml. давайте создадим его.
идем в res далее в values и создаем файл attrs.
<declare-styleable name="CustomSwitch">
<attr name="cs_subtitle" format="string"/>
<attr name="android:title"/>
<attr name="android:checked" />
</declare-styleable>
в поле name - обязательно нужно указывать имя вашего класса в нашем случае это был CustomSwitch.kt - соответственно поле name будет CustomSwitch. если назвать его иначе то в файле xml мы не будем получать подсказок, какие поля еще имеются для заполнения.
весьма полезная функция чтобы ее потерять)
Также хочу заметить что в этом файле имеется два типа полей, те которые добавляются из анроида и те которые мы создаем лично.
поле checked имеется в платформе и соответственно может переиспользываться, также как и title. вы можете переиспользовать их сколь угодно раз.
subtitle так же имеется в платформе, однако для примера я не стал указывать что хочу переиспользовать его и не указал слово android:.
будте внимательны так как новое имя может использоваться только в одной кастовой вю по этому я добавил абривиатуру cs - которая обозначает что этот атрибут будет использоваться только с CustomSwitch.
в файле CustomSwitch - обязательно в блоке init() не забудьте указать addView(binding.root). что привяжет вашу верстку к файлу kt.
теперь все готово к спользыванию. далее с этой вю мы работаем как и с любым другим экраном, у нас имеется binding c помощью которого мы можем обращаться к вю и файл в котором мы можем размещать наш функционал.
В примере имеются несколько базовых функций для работы с этой вю:
setTitle, setSubTitle - для установки соответствующих полей, isChecked - для проверки/устаноки состояния и addOnChangeListener - для прослушивания нажатий.
вот как можно применить это в коде:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.run {
first.isChecked = !first.isChecked // get or set checked
first.addOnChangeListener = { boolean -> // boolean on change
}
first.setTitle("text") // set title
first.setSubTitle("text2") // set subTitle
}
}
}
anegin
"не нагружать верстку" - это только про читаемость верстки. быстрее оно работать не станет. это скорее не Custom View, а Custom ViewGroup или Compound Layout
выложенная xml-верстка не соответствует тому как она используется в CustomView.kt. больше похоже на activity_main.xml. и по ней тоже есть нюансы - зачем внутри LinearLayout внутри ConstraintLayout, зачем у LinearLayout стоит width=match_parent.
подозреваю, что верстка для CustomView содержит внутри какой-то свой ViewGroup, и это все оборачивается в LinearLayout (CustomView), а оптимальнее будет делать через <merge>