Урок является продолжением MOKO Widgets #2 - routing. Для выполнения данного урока нужно иметь проект, полученный в результате выполнения предыдущего урока.

Результатом прошлого урока было приложение с экранами:

На этом уроке мы добавим стилизацию экранов. Это включает в себя:

В результате предыдущего урока получено приложение, в котором стартовый экран выглядит так:

android app

ios app

android-app

ios-app

Теперь изменим цвет статусбара и навбара на Android, а так же акцент цвет на обеих платформах (синий на iOS и ярко зеленый на Android).

Android

На Android изменения этих цветов выполняются через тему приложения в файле android-app/src/main/res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- your app branding color for the app bar -->
        <item name="colorPrimary">#FF4caf50</item>
        <!-- darker variant for the status bar and contextual app bars -->
        <item name="colorPrimaryDark">#FF087f23</item>
        <!-- theme UI controls like checkboxes and text fields -->
        <item name="colorAccent">#FF80e27e</item>
    </style>
</resources>

collorAccent отвечает за цвет указателя в тексте, лейбла и подчеркивания по умолчанию и используется в других подобных местах для обращения внимания. Цвет указателя в тексте можно заменить только через данный файл стилей. colorPrimaryDark это цвет статусбара, тоже можно менять только через данный файл.

Сменим цвет на оранжевую тему:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- your app branding color for the app bar -->
        <item name="colorPrimary">#FFffb74d</item>
        <!-- darker variant for the status bar and contextual app bars -->
        <item name="colorPrimaryDark">#FFc88719</item>
        <!-- theme UI controls like checkboxes and text fields -->
        <item name="colorAccent">#FFffbd45</item>
    </style>
</resources>

В результате получим: android-app

iOS

На iOS можно задать глобальный tint цвет, который выдаст нужный нам результат. Для этого в ios-app/src/AppDelegate.swift нужно добавить:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
    ...

    window = UIWindow(frame: UIScreen.main.bounds)
    ...
    window?.tintColor = UIColor(red:1.00, green:0.74, blue:0.27, alpha:1.0)
    window?.makeKeyAndVisible()

    return true
}

И получим: ios-app

Следующий этап стилизации - оформление всех кнопок в соответствии дизайну. По дизайну требуется чтобы был фон кнопок оранжевого цвета, текст черный с размером шрифта 15.

Для применения данных настроек нам нужно воспользоваться возможностями класса Theme. В нашем проекте уже есть создание объекта этого класса и он используется для дальнейшего создания всех виджетов. Сейчас нам нужно задать что в данной теме все кнопки будут создаваться с кастомным стилем.

Оформление по умолчанию устанавливается через категорию ButtonWidget.DefaultCategory в Theme, его нам и нужно кастомизировать.

Positive : Если провести аналогию с CSS, то DefaultCategory это стиль наложенный на все элементы данного типа:

button {
  color: red;
  text-align: center;
}

mpp-library/src/commonMain/kotlin/org/example/mpp/App.kt:

class App : BaseApplication() {
    override fun setup(): ScreenDesc<Args.Empty> {
        val theme = Theme()

        ...
    }
}

На данный момент объект создается без предедачи каких либо аргументов, но Theme может принимать 2 аргумента:

Сейчас используем лямбду настройки:
mpp-library/src/commonMain/kotlin/org/example/mpp/App.kt:

val Colors.orange get() = Color(0xff8a65FF)
val Colors.orangeLight get() = Color(0xffbb93FF)
val Colors.orangeDark get() = Color(0xc75b39FF)

class App : BaseApplication() {
    override fun setup(): ScreenDesc<Args.Empty> {
        val theme = Theme() {
            factory[ButtonWidget.DefaultCategory] = SystemButtonViewFactory(
                background = StateBackground(
                    normal = Background(
                        fill = Fill.Solid(color = Colors.orangeLight)
                    ),
                    pressed = Background(
                        fill = Fill.Solid(color = Colors.orange)
                    ),
                    disabled = Background(
                        fill = Fill.Solid(color = Colors.orangeDark)
                    )
                ),
                textStyle = TextStyle(color = Colors.black, size = 15)
            )
        }

        ...
    }
}

В итоге получаем следующий результат:

android app

ios app

android-app

ios-app

Оформление группами реализуется через указание элементам отдельной Category и применение для этой Category своей фабрики в Theme.

Positive : Если провести аналогию с CSS, то Category это стиль наложенный на class:

.myclass {
  color: red;
  text-align: center;
}

Объявим в App.kt новую категорию кнопок. Назовем ее SubmitButtonsCategory:

object SubmitButtonsCategory: ButtonWidget.Category

Positive : За счет специального типизации через вложенный интерфейс ButtonWidget.Category мы можем использовать данную категорию только с кнопками и применять фабрики только кнопок.

Далее на экранах входа используем данную категорию. Чтобы не создавать прямую зависимость из пакета auth в App сделаем в AuthFactory специальное свойство:
mpp-library/src/commonMain/kotlin/org/example/mpp/auth/AuthFactory.kt:

class AuthFactory(
    ...
    private val submitButtons: ButtonWidget.Category
) {
    fun createInputPhoneScreen(routeInputCode: Route<String>): InputPhoneScreen {
        return InputPhoneScreen(
            ...
            submitButtons = submitButtons
        )
    }

    fun createInputCodeScreen(routeMain: Route<Unit>): InputCodeScreen {
        return InputCodeScreen(
            ...
            submitButtons = submitButtons
        )
    }
}

и в классах экранов InputPhoneScreen, InputCodeScreen добавим в конструктор данный аргумент, который применим к кнопке:
mpp-library/src/commonMain/kotlin/org/example/mpp/auth/InputCodeScreen.kt:

class InputCodeScreen(
    ...
    private val submitButtons: ButtonWidget.Category
) : ... {
    ...
    
    override fun createContentWidget() = with(theme) {
        ...

        constraint(size = WidgetSize.AsParent) {
            ...

            val submitButton = +button(
                ...
                category = submitButtons
            )

            ...
        }
    }
}

Аналогично в InputPhoneScreen.

Остается назначить данной категории другую фабрику: mpp-library/src/commonMain/kotlin/org/example/mpp/App.kt:

class App : BaseApplication() {
    override fun setup(): ScreenDesc<Args.Empty> {
        val theme = Theme() {
            ...

            factory[SubmitButtonsCategory] = SystemButtonViewFactory(
                background = StateBackground(
                    normal = Background(
                        fill = Fill.Solid(color = Colors.black)
                    ),
                    pressed = Background(
                        fill = Fill.Solid(color = Colors.black.copy(alpha = 0xAA))
                    ),
                    disabled = Background(
                        fill = Fill.Solid(color = Colors.black.copy(alpha = 0xEE))
                    )
                ),
                textStyle = TextStyle(color = Colors.white, size = 15)
            )
        }

        val authFactory = AuthFactory(theme, submitButtons = SubmitButtonsCategory)
        ...
    }
}

И в результате получаем кастомизированные кнопки только на двух экранах, остальные экраны используют вариант по умолчанию:

Android

phone

code

android-app

android-app

iOS

phone

code

ios-app

ios-app

Оформление группами реализуется через указание элементам определенного Id и применение для этого Id своей фабрики в Theme.

Positive : Если провести аналогию с CSS, то Id это стиль наложенный на id:

#myclass {
  color: red;
  text-align: center;
}

Например сделаем чтобы лейбл поля ввода телефона был оранжевым. Так как все интерактивные элементы требуют указание Id (для сохранения состояния на Android), то у нас уже есть назначенный этому полю идентификатор и мы можем это использовать:
mpp-library/src/commonMain/kotlin/org/example/mpp/App.kt:

factory[InputPhoneScreen.Ids.Phone] = SystemInputViewFactory(
    labelTextStyle = TextStyle(color = Colors.orangeDark)
)

И получим результат:

android app

ios app

android-app

ios-app

Theme может быть унаследована от другой, взяв всю кастомизацию родителя и дополнив своей. Например мы хотим сделать чтобы кнопки на экране профиля все были визуально другие, мы можем сделать это через отдельную категорию и прокидывать ее через конструкторы, либо же сделать другую тему, которую дадим в экран профиля.

val profileTheme = Theme(parent = theme) {
    factory[ButtonWidget.DefaultCategory] = SystemButtonViewFactory(
        background = StateBackground(
            normal = Background(
                fill = Fill.Solid(color = Colors.orangeLight)
            ),
            pressed = Background(
                fill = Fill.Solid(color = Colors.orange)
            ),
            disabled = Background(
                fill = Fill.Solid(color = Colors.orangeDark)
            )
        ),
        textStyle = TextStyle(color = Colors.black, size = 24)
    )
}

...
val profileFactory = ProfileFactory(profileTheme)

Мы создали на основе темы theme новую тему, заменив в ней фабрику по умолчанию для кнопок. Теперь на экранах профиля (они создаются фабрикой ProfileFactory) будут кнопки с большим текстом.

Android

phone

code

android-app

android-app

iOS

phone

code

ios-app

ios-app

class MainBottomNavigationScreen(
    router: Router,
    builder: BottomNavigationItem.Builder.() -> Unit
) : BottomNavigationScreen(router, builder), NavigationItem {
    override val navigationBar: NavigationBar = NavigationBar.None
    
    init {
        bottomNavigationColor = Colors.orangeDark
    }
}

И получим результат:

android app

ios app

android-app

ios-app