سیستم‌های طراحی سفارشی در Compose

اگرچه Material سیستم طراحی توصیه‌شده ما است و Jetpack Compose پیاده‌سازی Material را ارائه می‌دهد، مجبور نیستید از آن استفاده کنید. «متریال» کاملاً براساس میاناهای برنامه‌سازی کاربردی عمومی ساخته شده است، بنابراین می‌توانید سیستم طراحی خودتان را به همین روش ایجاد کنید.

چندین رویکرد می‌توانید اتخاذ کنید:

همچنین ممکن است بخواهید به استفاده از عناصر Material با سیستم طراحی سفارشی ادامه دهید. انجام این کار ممکن است اما باید نکاتی را درنظر داشته باشید تا با رویکردی که اتخاذ کرده‌اید مطابقت داشته باشد.

برای آشنایی بیشتر با ساختارهای سطح پایین‌تر و میاناهای برنامه‌سازی کاربردی که MaterialTheme و سیستم‌های طراحی سفارشی استفاده می‌کنند، راهنمای ساختار درونمایه در Compose را بررسی کنید.

گسترش موضوع‌دهی Material

«ترکیب Material» از نزدیک Material Theming را مدل‌سازی می‌کند تا پیروی از رهنمودهای Material را ساده و ازنظر نوع ایمن کند. بااین‌حال، می‌توان مجموعه‌های رنگ، طرح حروف، و شکل را با مقادیر اضافی گسترش داد. ساده‌ترین رویکرد افزودن دارایی‌های افزونه است:

// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
    @Composable
    get() = if (isSystemInDarkTheme()) Red300 else Red700

// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
    get() = TextStyle(/* ... */)

// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
    get() = RoundedCornerShape(size = 20.dp)

این کار باعث می‌شود با میاناهای برنامه‌سازی کاربردی MaterialTheme سازگاری ایجاد شود. نمونه‌ای از این تعریف‌شده توسط خود Compose surfaceColorAtElevation است که رنگ سطحی را که باید بسته به ارتفاع استفاده شود تعیین می‌کند.

رویکرد دیگر تعریف زمینه گسترده‌ای است که MaterialTheme و مقادیر آن را «می‌پوشاند».

فرض کنید می‌خواهید دو رنگ دیگر — caution و onCaution، رنگ زردی که برای کنش‌های نیمه‌خطرناک استفاده می‌شود — اضافه کنید و درعین‌حال رنگ‌های موجود Material را حفظ کنید:

@Immutable
data class ExtendedColors(
    val caution: Color,
    val onCaution: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        caution = Color.Unspecified,
        onCaution = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        caution = Color(0xFFFFCC02),
        onCaution = Color(0xFF2C2D30)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

// Use with eg. ExtendedTheme.colors.caution
object ExtendedTheme {
    val colors: ExtendedColors
        @Composable
        get() = LocalExtendedColors.current
}

این شبیه به میاناهای برنامه‌سازی کاربردی استفاده MaterialTheme است. همچنین از چند موضوع پشتیبانی می‌کند زیرا می‌توانید ExtendedTheme را به همان روش MaterialTheme تودرتو کنید.

استفاده از عناصر «طراحی مواد»

هنگام گسترش «زمینه Material»، مقادیر MaterialTheme موجود حفظ می‌شود و مؤلفه‌های Material همچنان پیش‌فرض‌های معقولی دارند.

اگر می‌خواهید از مقادیر توسعه‌یافته در عناصر استفاده کنید، آن‌ها را در توابع ترکیب‌پذیر خودتان بپیچید، مقادیری را که می‌خواهید تغییر دهید مستقیماً تنظیم کنید، و مقادیر دیگر را به‌عنوان پارامتر در ترکیب‌پذیر دربرگیرنده نمایان کنید:

@Composable
fun ExtendedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = ExtendedTheme.colors.caution,
            contentColor = ExtendedTheme.colors.onCaution
            /* Other colors use values from MaterialTheme */
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

سپس باید موارد استفاده از Button را در جاهایی که مناسب است با ExtendedButton جایگزین کنید.

@Composable
fun ExtendedApp() {
    ExtendedTheme {
        /*...*/
        ExtendedButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

جایگزین کردن زیرسیستم‌های Material

به‌جای گسترش «زمینه Material»، ممکن است بخواهید یک یا چند سیستم — Colors، Typography، یا Shapes — را با پیاده‌سازی سفارشی جایگزین کنید و درعین‌حال بقیه را حفظ کنید.

فرض کنید می‌خواهید سیستم‌های نوع و شکل را جایگزین کنید و سیستم رنگ را حفظ کنید:

@Immutable
data class ReplacementTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class ReplacementShapes(
    val component: Shape,
    val surface: Shape
)

val LocalReplacementTypography = staticCompositionLocalOf {
    ReplacementTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalReplacementShapes = staticCompositionLocalOf {
    ReplacementShapes(
        component = RoundedCornerShape(ZeroCornerSize),
        surface = RoundedCornerShape(ZeroCornerSize)
    )
}

@Composable
fun ReplacementTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val replacementTypography = ReplacementTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val replacementShapes = ReplacementShapes(
        component = RoundedCornerShape(percent = 50),
        surface = RoundedCornerShape(size = 40.dp)
    )
    CompositionLocalProvider(
        LocalReplacementTypography provides replacementTypography,
        LocalReplacementShapes provides replacementShapes
    ) {
        MaterialTheme(
            /* colors = ... */
            content = content
        )
    }
}

// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
    val typography: ReplacementTypography
        @Composable
        get() = LocalReplacementTypography.current
    val shapes: ReplacementShapes
        @Composable
        get() = LocalReplacementShapes.current
}

استفاده از عناصر Material

وقتی یک یا چند سیستم MaterialTheme جایگزین شده است، استفاده از عناصر Material به‌همان شکل ممکن است منجر به مقادیر ناخواسته رنگ، نوع، یا شکل Material شود.

اگر می‌خواهید از مقادیر جایگزین در عناصر استفاده کنید، آن‌ها را در توابع ترکیب‌پذیر خودتان بپیچید، مقادیر را مستقیماً برای سیستم مربوطه تنظیم کنید، و بقیه را به‌عنوان پارامتر در ترکیب‌پذیر دربرگیرنده نمایان کنید.

@Composable
fun ReplacementButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        shape = ReplacementTheme.shapes.component,
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = ReplacementTheme.typography.body
            ) {
                content()
            }
        }
    )
}

سپس باید موارد استفاده از Button را در جاهایی که مناسب است با ReplacementButton جایگزین کنید.

@Composable
fun ReplacementApp() {
    ReplacementTheme {
        /*...*/
        ReplacementButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

پیاده‌سازی سیستم طراحی کاملاً سفارشی

ممکن است بخواهید «زمینه با طراحی مواد» را با سیستم طراحی کاملاً سفارشی جایگزین کنید. درنظر بگیرید که MaterialTheme سیستم‌های زیر را ارائه می‌دهد:

  • ‫Colors،‏ Typography، و Shapes: سیستم‌های موضوعی‌سازی Material
  • ‫TextSelectionColors: رنگ‌های استفاده‌شده برای انتخاب نوشتار توسط Text و TextField
  • ‫Ripple و RippleTheme: پیاده‌سازی مادی Indication

اگر می‌خواهید به استفاده از عناصر «طراحی مواد» ادامه دهید، باید برخی‌از این سیستم‌ها را در زمینه‌های سفارشی‌تان جایگزین کنید یا سیستم‌ها را در عناصرتان مدیریت کنید تا از رفتار ناخواسته جلوگیری شود.

بااین‌حال، سیستم‌های طراحی به مفاهیمی که Material بر آن‌ها تکیه دارد محدود نمی‌شوند. می‌توانید سیستم‌های موجود را اصلاح کنید و سیستم‌های کاملاً جدیدی را — با کلاس‌ها و انواع جدید — معرفی کنید تا مفاهیم دیگر را با موضوعات سازگار کنید.

در کد زیر، سیستم رنگ سفارشی را مدل‌سازی می‌کنیم که شامل گرادیان‌ها (List<Color>)، سیستم نوع، سیستم ارتفاع جدید، و سیستم‌های دیگری است که MaterialTheme ارائه می‌دهد:

نماگرفت از میانای کاربر برنامه تلفن همراه که سیستم طراحی سفارشی را با عناصری که از گرادیان برای رنگ‌ها، طرح حروف سفارشی، و ارتفاع استفاده می‌کنند نشان می‌دهد.

@Immutable
data class CustomColors(
    val content: Color,
    val component: Color,
    val background: List<Color>
)

@Immutable
data class CustomTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class CustomElevation(
    val default: Dp,
    val pressed: Dp
)

val LocalCustomColors = staticCompositionLocalOf {
    CustomColors(
        content = Color.Unspecified,
        component = Color.Unspecified,
        background = emptyList()
    )
}
val LocalCustomTypography = staticCompositionLocalOf {
    CustomTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalCustomElevation = staticCompositionLocalOf {
    CustomElevation(
        default = Dp.Unspecified,
        pressed = Dp.Unspecified
    )
}

@Composable
fun CustomTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val customColors = CustomColors(
        content = Color(0xFFDD0D3C),
        component = Color(0xFFC20029),
        background = listOf(Color.White, Color(0xFFF8BBD0))
    )
    val customTypography = CustomTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val customElevation = CustomElevation(
        default = 4.dp,
        pressed = 8.dp
    )
    CompositionLocalProvider(
        LocalCustomColors provides customColors,
        LocalCustomTypography provides customTypography,
        LocalCustomElevation provides customElevation,
        content = content
    )
}

// Use with eg. CustomTheme.elevation.small
object CustomTheme {
    val colors: CustomColors
        @Composable
        get() = LocalCustomColors.current
    val typography: CustomTypography
        @Composable
        get() = LocalCustomTypography.current
    val elevation: CustomElevation
        @Composable
        get() = LocalCustomElevation.current
}

استفاده از عناصر Material

وقتی MaterialTheme وجود ندارد، استفاده از عناصر Material به‌همان شکل منجر به مقادیر و رفتار نشانگر ناخواسته در رنگ، نوع، و شکل Material می‌شود.

اگر می‌خواهید از مقادیر سفارشی در عناصر استفاده کنید، آن‌ها را در توابع ترکیب‌پذیر خودتان بپیچید، مقادیر را مستقیماً برای سیستم مربوطه تنظیم کنید، و بقیه را به‌عنوان پارامتر در ترکیب‌پذیر دربرگیرنده نمایان کنید.

توصیه می‌کنیم به مقادیری که از زمینه سفارشی‌تان تنظیم کرده‌اید دسترسی پیدا کنید. یا اگر زمینه شما Color، TextStyle، Shape، یا سیستم‌های دیگر را ارائه نمی‌دهد، می‌توانید آن‌ها را کدگذاری سخت کنید.

@Composable
fun CustomButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.component,
            contentColor = CustomTheme.colors.content,
            disabledContainerColor = CustomTheme.colors.content
                .copy(alpha = 0.12f)
                .compositeOver(CustomTheme.colors.component),
            disabledContentColor = CustomTheme.colors.content
                .copy(alpha = 0.38f)

        ),
        shape = ButtonShape,
        elevation = ButtonDefaults.elevatedButtonElevation(
            defaultElevation = CustomTheme.elevation.default,
            pressedElevation = CustomTheme.elevation.pressed
            /* disabledElevation = 0.dp */
        ),
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = CustomTheme.typography.body
            ) {
                content()
            }
        }
    )
}

val ButtonShape = RoundedCornerShape(percent = 50)

اگر انواع کلاس جدیدی معرفی کرده‌اید — مثلاً List<Color> برای نشان دادن گرادیان‌ها — بهتر است به‌جای پیچیدن آن‌ها، مؤلفه‌ها را از ابتدا پیاده‌سازی کنید. برای مثال، به JetsnackButton از نمونه Jetsnack نگاهی بیندازید.