Урок является продолжением MOKO Widgets #7 - lists on widgets. Для выполнения данного урока нужно иметь проект, полученный в результате выполнения предыдущего урока.
Результатом прошлого урока было приложение с навигацией, стилизацией экранов, различными действиями на экранах, кастомными фабриками, платформенным экраном и списком друзей.
На этом уроке мы реализуем новый виджет - слайдер (для выбора значения в заданном диапазоне).
Любой виджет состоит из 2 частей - описание виджета в общем коде (наследник от Widget) и фабрика виджета с реализацией на каждой платформе (наследник от ViewFactory).
Начинается создание нового виджета с создания наследника от Widget: mpp-library/src/commonMain/kotlin/org/example/mpp/SliderWidget.kt:
class SliderWidget<WS : WidgetSize>(
override val size: WS
) : Widget<WS>()
Каждый виджет должен реализовывать метод сборки виджета, но для реализации требуется фабрика виджета. Добавим это: mpp-library/src/commonMain/kotlin/org/example/mpp/SliderWidget.kt:
class SliderWidget<WS : WidgetSize>(
...
private val factory: ViewFactory<SliderWidget<out WidgetSize>>
) : ... {
override fun buildView(viewFactoryContext: ViewFactoryContext): ViewBundle<WS> {
return factory.build(this, size, viewFactoryContext)
}
}
Так же у всех виджетов должен быть аргумент id и интерфейс либо RequiredId (обязательно требуется указывать id - для интерактивных элементов, чтобы android мог сохранить состояние экрана), либо OptionalId. mpp-library/src/commonMain/kotlin/org/example/mpp/SliderWidget.kt:
class SliderWidget<WS : WidgetSize>(
...
override val id: Id
) : Widget<WS>(), RequireId<SliderWidget.Id> {
...
interface Id : Theme.Id<SliderWidget<out WidgetSize>>
}
Positive : Id это интерфейс наследующийся от специального интерфейса Theme.Id. За счет строгой типизации компилятор проверяет корректные ли операции применяются по id или нет.
Базовые свойства виджета уже добавлены, теперь нужно добавить данные этого виджета - то, что не касается визуального оформления элемента. Эти данные будут считываться фабрикой виджета и применяться к ui элементу. mpp-library/src/commonMain/kotlin/org/example/mpp/SliderWidget.kt:
class SliderWidget<WS : WidgetSize>(
...
val minValue: Int,
val maxValue: Int,
val value: MutableLiveData<Int>
) : ... {
...
}
Данная конфигурация виджета позволяет создавать виджет следующим образом:
SliderWidget(
size = WidgetSize.WrapContent,
factory = MyFactory(),
id = Ids.Slider,
minValue = -5,
maxValue = 5,
value = MutableLiveData(initialValue = 0)
)
То есть фабрику нам нужно самостоятельно где-то получить и передать в виджет. Если же мы хотим использовать возможности Theme с подстановкой фабрики и создавать виджет следующим образом:
theme.slider(
size = WidgetSize.WrapContent,
id = Ids.Slider,
minValue = -5,
maxValue = 5,
value = MutableLiveData(initialValue = 0)
)
то нам требуется подключить специальный gradle plugin dev.icerock.mobile.multiplatform-widgets-generator: mpp-library/build.gradle.kts:
plugins {
...
id("dev.icerock.mobile.multiplatform-widgets-generator")
}
И добавить аннотацию, а так-же категорию (для системы категорий в Theme) mpp-library/src/commonMain/kotlin/org/example/mpp/SliderWidget.kt:
@WidgetDef(SliderViewFactory::class)
class SliderWidget<WS : WidgetSize>(
...
) : ... {
...
interface Category : Theme.Category<SliderWidget<out WidgetSize>>
object DefaultCategory : Category
}
В аннотации WidgetDef аргументом указывается класс фабрики по-умолчанию для виджета (то есть та фабрика, которая будет использоваться даже если в Theme не делалось никаких настроек). Мы указали SliderViewFactory, остается только создать эту фабрику.
Аналогично уроку MOKO Widgets #5 - custom ViewFactory нужно создать новую фабрику, но для нашего виджета. Для простоты не будем давать никакой параметризации.
mpp-library/src/commonMain/kotlin/org/example/mpp/SliderViewFactory.kt:
expect class SliderViewFactory() : ViewFactory<SliderWidget<out WidgetSize>>
Для создания UI элемента на android используем SeekBar.
mpp-library/src/androidMain/kotlin/org/example/mpp/SliderViewFactory.kt:
actual class SliderViewFactory : ViewFactory<SliderWidget<out WidgetSize>> {
override fun <WS : WidgetSize> build(
widget: SliderWidget<out WidgetSize>,
size: WS,
viewFactoryContext: ViewFactoryContext
): ViewBundle<WS> {
val context = viewFactoryContext.androidContext
val lifecycleOwner = viewFactoryContext.lifecycleOwner
val slider = SeekBar(context).apply {
max = widget.maxValue - widget.minValue
}
widget.value.bindNotNull(lifecycleOwner) { slider.progress = it - widget.minValue }
slider.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
val fixedProgress = progress + widget.minValue
if (widget.value.value == fixedProgress) return
widget.value.value = fixedProgress
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
return ViewBundle(
view = slider,
size = size,
margins = null
)
}
}
Для создания UI элемента на iOS используем UISlider.
mpp-library/src/iosX64Main/kotlin/org/example/mpp/SliderViewFactory.kt:
actual class SliderViewFactory : ViewFactory<SliderWidget<out WidgetSize>> {
override fun <WS : WidgetSize> build(
widget: SliderWidget<out WidgetSize>,
size: WS,
viewFactoryContext: ViewFactoryContext
): ViewBundle<WS> {
val slider = UISlider(frame = CGRectZero.readValue()).apply {
translatesAutoresizingMaskIntoConstraints = false
minimumValue = widget.minValue.toFloat()
maximumValue = widget.maxValue.toFloat()
}
widget.value.bind { slider.value = it.toFloat() }
slider.setEventHandler(UIControlEventValueChanged) {
val value = slider.value.toInt()
slider.value = value.toFloat()
if (widget.value.value == value) return@setEventHandler
widget.value.value = value
}
return ViewBundle(
view = slider,
size = size,
margins = null
)
}
}
Для проверки результата добавим на экран профиля слайдер и текст, в котором выведем текущее выбранное в слайдере значение.
mpp-library/src/commonMain/kotlin/org/example/mpp/profile/ProfileScreen.kt:
class ProfileScreen(
...
) : ... {
...
override fun createContentWidget() = with(theme) {
val sliderValue = MutableLiveData<Int>(initialValue = 0)
constraint(size = WidgetSize.AsParent) {
...
val slider = +slider(
size = WidgetSize.WidthAsParentHeightWrapContent,
id = Ids.Slider,
minValue = -5,
maxValue = 5,
value = sliderValue
)
val valueText = +text(
size = WidgetSize.WidthAsParentHeightWrapContent,
text = sliderValue.map { it.toString().desc() as StringDesc }
)
constraints {
...
slider bottomToTop editButton offset 16
slider leftRightToLeftRight root
valueText bottomToTop slider offset 16
valueText leftRightToLeftRight root
}
}
}
object Ids {
object Slider : SliderWidget.Id
}
}
В результате получаем:
android app | ios app |
|
|