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

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

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

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

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

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

Compose Material از موضوع‌دهی Material به‌طور دقیق مدل‌سازی می‌کند تا پیروی از دستورالعمل‌های 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

هنگام گسترش «موضوع‌دهی 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»، شاید بخواهید یک یا چند سیستم (ColorScheme، 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 = { /* ... */ }) {
            /* ... */
        }
    }
}

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

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

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

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

بااین‌حال، سیستم‌های طراحی به مفاهیمی که 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 نگاه کنید.