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

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

هناك العديد من الأساليب التي يمكنك اتباعها:

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

للاطّلاع على مزيد من المعلومات عن العناصر وواجهات برمجة التطبيقات ذات المستوى الأدنى التي يستخدمها MaterialTheme وأنظمة التصميم المخصّصة، اطّلِع على دليل بنية المظهر في 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

بدلاً من تمديد استخدام "تخصيص التصميم المتعدد الأبعاد"، قد يكون من الأفضل استبدال نظام واحد أو أكثر، مثل 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: أنظمة "تخصيص التصميم المتعدد الأبعاد"
  • TextSelectionColors: الألوان المستخدَمة لاختيار النص من خلال Text و TextField
  • Ripple وRippleTheme: تنفيذ Indication في المواد

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

ومع ذلك، لا تقتصر أنظمة التصميم على المفاهيم التي يعتمد عليها Material Design. يمكنك تعديل الأنظمة الحالية وتقديم أنظمة جديدة تمامًا، باستخدام فئات وأنواع جديدة، لجعل المفاهيم الأخرى متوافقة مع المظاهر.

في الرمز البرمجي التالي، نضع نموذجًا لنظام ألوان مخصّص يتضمّن تدرّجات 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 كما هي إلى قيم غير مرغوب فيها للون ونوع وشكل "المادة" وسلوك المؤشر.

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

ننصحك بالوصول إلى القيم التي تحدّدها من تصميمك المخصّص. بدلاً من ذلك، إذا كان المظهر لا يوفّر 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.