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

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

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

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

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

توسيع نطاق "مظهر المواد"

إنشاء نماذج Materials عن كثب تخصيص التصميم المتعدد الأبعاد لجعل إرشادات المواد أمرًا بسيطًا وآمنًا في الكتابة. ومع ذلك، من المهم زيادة الألوان وأسلوب الخط ومجموعات الأشكال القيم.

أبسط طريقة هي إدراج خصائص الإضافات:

// 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 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.