Хочу поделиться примером простого составного представления со слайдером на основе SeekBar. Эта статья будет интересна тем, кто только начал строить составные представления. Но рано или поздно вы создадите составные представления для каждого элемента на экране, потому что вам нужно управлять, настраивать и контролировать свое представление.

В одном из моих бывших проектов дизайнер попросил реализовать ввод для размера порции таким образом (см. GIF-файл вверху экрана). На самом деле это легко применять каждый раз, когда пользователю нужно выбрать 1 из 10 значений (не больше — иначе у нас будет слишком много точек, но у нас есть ограничение по ширине экрана). С точки зрения пользователя это интереснее, чем ввод с клавиатуры, поэтому полезно для нашего UX.

Давайте посмотрим на slider_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<merge 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="wrap_content"
    android:padding="12dp"
    tools:background="@drawable/rounded_15_shape_light"
    tools:context=".SliderView"
    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="4dp"
        android:textAllCaps="false"
        android:textColor="#6a6b86"
        android:textSize="12sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Serving size" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/personCounter"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textColor="#2a2957"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="@id/title"
        app:layout_constraintTop_toBottomOf="@id/title"
        tools:text="1 person" />

    <androidx.appcompat.widget.AppCompatSeekBar
        android:id="@+id/personsQuantitySlider"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:max="10"
        android:min="1"
        android:progress="1"
        android:progressBackgroundTint="@color/white"
        android:splitTrack="false"
        android:thumb="@drawable/ic_slider"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/personCounter" />

</merge>

Все просто — объедините title, output и slider-input внутри тега ‹merge›.

Класс SliderView.kt

class SliderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    private var binding: SliderViewBinding

    init {
        binding = SliderViewBinding.inflate(LayoutInflater.from(context), this)
        build()
    }

    fun build(): SliderView {
        //because of tag merge need to duplicate here root-element's settings
        this.setBackgroundResource(R.drawable.rounded_15_shape_light)
        val padding = 12F.toDp().toInt()
        this.setPadding(padding)
        //initial value. changable according goals
        binding.personCounter.text = context.getString(R.string.person, 1)
        binding.title.text = context.getString(R.string.serving_size)

        //magic is here. get seekbar progress changes and do what you need with it
        binding.personsQuantitySlider.setOnSeekBarChangeListener(object :
            SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(p0: SeekBar?, progress: Int, p2: Boolean) {
                binding.personCounter.text = context.getString(
                    if (progress == 1) R.string.person else R.string.persons,
                    progress
                )
            }
            override fun onStartTrackingTouch(p0: SeekBar?) {
            }
            override fun onStopTrackingTouch(p0: SeekBar?) {
            }
        })

        return this
    }
}

Просто реализуйте метод onProgressChanged() SeekBar и настройте вывод для пользователя.

Как использовать — просто поместите его внутрь вашего фрагмента

<com.example.SliderView
    android:layout_width="0dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_height="wrap_content"/>

Бонус! Миниатюра ручной работы ic_slider.xml

<vector android:autoMirrored="true" android:height="54dp"
    android:viewportHeight="54" android:viewportWidth="54"
    android:width="54dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#e43895" android:pathData="M22,9L32,9A10,10 0,0 1,42 19L42,29A10,10 0,0 1,32 39L22,39A10,10 0,0 1,12 29L12,19A10,10 0,0 1,22 9z"/>
    <path android:fillColor="#80c70875" android:pathData="M24,19L30,19A2,2 0,0 1,32 21L32,27A2,2 0,0 1,30 29L24,29A2,2 0,0 1,22 27L22,21A2,2 0,0 1,24 19z"/>
</vector>

… и волнистый rounded_15_shape_light.xml для фона

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/white">
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="15dp"/>
            <solid android:color="#f3f3f5"/>
        </shape>
    </item>
</ripple>

+ расширение toDp(), позволяющее учитывать экранные метрики при конвертации. "Подробности"

fun Float.toDp() = TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP,
    this,
    Resources.getSystem().displayMetrics
)

Ваше здоровье!