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

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

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

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

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

توسيع نطاق تخصيص التصميم المتعدد الأبعاد

تتّبع Compose 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 الحالية وتظل مكوّنات التصميم المتعدد الأبعاد تتضمّن الإعدادات التلقائية المناسبة.

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

@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 الفرعية

بدلاً من توسيع نطاق تخصيص التصميم المتعدد الأبعاد، قد تحتاج إلى استبدال نظام واحد أو أكثر من الأنظمة (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 = { /* ... */ }) {
            /* ... */
        }
    }
}

تنفيذ نظام تصميم مخصّص بالكامل

قد تحتاج إلى استبدال "تخصيص التصميم المتعدد الأبعاد" بنظام تصميم مخصّص بالكامل. لنفترض أنّ MaterialTheme يوفّر الأنظمة التالية:

  • ‫ColorScheme وTypography وShapes: أنظمة تخصيص التصميم المتعدد الأبعاد
  • ‫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.