В этой статье пойдет речь о вынисении 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
        }
    }
}

Комментарии (1)


  1. anegin
    19.11.2022 18:24

    • "не нагружать верстку" - это только про читаемость верстки. быстрее оно работать не станет. это скорее не Custom View, а Custom ViewGroup или Compound Layout

    • выложенная xml-верстка не соответствует тому как она используется в CustomView.kt. больше похоже на activity_main.xml. и по ней тоже есть нюансы - зачем внутри LinearLayout внутри ConstraintLayout, зачем у LinearLayout стоит width=match_parent.

    • подозреваю, что верстка для CustomView содержит внутри какой-то свой ViewGroup, и это все оборачивается в LinearLayout (CustomView), а оптимальнее будет делать через <merge>