أنظمة التصميم المخصصة في Compose

على الرغم من أنّ Material هو نظام التصميم الذي ننصح به، وأنّ Jetpack Compose يتضمّن عملية تنفيذ لنظام Material، إلا أنّه ليس عليك استخدامه. تم إنشاء Material بالكامل باستخدام واجهات برمجة تطبيقات عامة، لذا يمكنك إنشاء نظام التصميم الخاص بك بالطريقة نفسها.

يمكنك اتّباع عدة طرق:

قد تحتاج أيضًا إلى مواصلة استخدام مكونات Material مع نظام تصميم مخصّص. يمكنك إجراء ذلك، ولكن هناك بعض النقاط التي يجب مراعاتها لتناسب النهج الذي اتّبعته.

لمزيد من المعلومات حول البِنى وواجهات برمجة التطبيقات ذات المستوى الأدنى التي تستخدمها MaterialTheme وأنظمة التصميم المخصّصة، يمكنك الاطّلاع على دليل بنية السمة في Compose.

توسيع نطاق Material Theme

تتّبع Compose 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

عند توسيع نطاق Material Theming، يتم الاحتفاظ بقيم 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 Theming، يمكنك استبدال نظام واحد أو أكثر، مثل 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 كما هي إلى ظهور قيم غير مرغوب فيها للون المادة أو نوعها أو شكلها.

إذا كنت تريد استخدام قيم بديلة في المكوّنات، عليك تضمينها في دوال قابلة للإنشاء خاصة بك، مع ضبط القيم مباشرةً للنظام ذي الصلة، وعرض القيم الأخرى كمعلَمات للعنصر القابل للإنشاء الذي يحتوي على هذه القيم.

@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: أنظمة تخصيص التصميم المتعدد الأبعاد
  • TextSelectionColors: الألوان المستخدَمة لتحديد النص بواسطة Text وTextField
  • Ripple وRippleTheme: تنفيذ المواد 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.