خطوط المحاذاة في Jetpack Compose

يتيح لك نموذج تنسيق Compose استخدام AlignmentLine لإنشاء خطوط محاذاة مخصّصة يمكن أن تستخدمها التنسيقات الرئيسية لمحاذاة العناصر التابعة لها وتحديد موضعها. على سبيل المثال، يمكن أن تستخدم Row خطوط المحاذاة المخصّصة للعناصر التابعة لها لمحاذاتها.

عندما يقدّم تنسيق قيمة لـ AlignmentLine معيّن، يمكن أن تقرأ التنسيقات الرئيسية هذه القيمة بعد القياس، باستخدام عامل التشغيل Placeable.get على مثيل Placeable المقابل. استنادًا إلى موضع AlignmentLine، يمكن أن تحدّد التنسيقات الرئيسية بعد ذلك موضع العناصر التابعة.

تتضمّن بعض العناصر القابلة للإنشاء في Compose خطوط محاذاة. على سبيل المثال، يعرض العنصر القابل للإنشاء BasicText خطَّي المحاذاة FirstBaseline وLastBaseline.

في المثال التالي، يقرأ LayoutModifier مخصّص يُسمى firstBaselineToTop الـ FirstBaseline لإضافة مساحة ترك مسافة إلى Text بدءًا من السطر الأساسي الأول.

تعرض هذه الصورة الفرق بين إضافة مساحة متروكة عادية إلى عنصر وتطبيق مساحة متروكة على خط الأساس لعنصر نصي.
الشكل 1. يعرض الفرق بين إضافة مساحة ترك مسافة عادية إلى عنصر وتطبيق مساحة ترك مسافة على السطر الأساسي لعنصر `Text`.

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp,
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

@Preview
@Composable
private fun TextWithPaddingToBaseline() {
    MaterialTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

لقراءة FirstBaseline في المثال، يتم استخدام placeable [FirstBaseline] في مرحلة القياس.

إنشاء خطوط محاذاة مخصّصة

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

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

عنصر BarChart قابل للإنشاء مع عنصر Text محاذٍ لأعلى قيمة وأدنى قيمة للبيانات.
الشكل 2. العنصر القابل للإنشاء BarChart مع نص محاذٍ للحد الأقصى و الحد الأدنى لقيمة البيانات.

يتم تحديد خطوط المحاذاة المخصّصة كمتغيّرات على المستوى الأعلى في مشروعك.

/**
 * AlignmentLine defined by the maximum data value in a [BarChart]
 */
private val MaxChartValue = HorizontalAlignmentLine(merger = { old, new ->
    min(old, new)
})

/**
 * AlignmentLine defined by the minimum data value in a [BarChart]
 */
private val MinChartValue = HorizontalAlignmentLine(merger = { old, new ->
    max(old, new)
})

إنّ خطوط المحاذاة المخصّصة التي سيتم إنشاؤها في مثالنا هي من النوع HorizontalAlignmentLine، لأنّها تُستخدَم لمحاذاة العناصر التابعة عموديًا. يتم تمرير سياسة الدمج كمعلَمة في حال قدّمت تنسيقات متعدّدة قيمة لخطوط المحاذاة هذه. بما أنّ إحداثيات نظام تنسيق Compose وإحداثيات Canvas تمثّل [0, 0]، فإنّ أعلى يسار الصفحة وإحداثيات المحورَين x وy تكون موجبة للأسفل، لذا ستكون قيمة MaxChartValue دائمًا أصغر من MinChartValue. لذلك، تكون سياسة الدمج min للسطر الأساسي للحد الأقصى لقيمة بيانات الرسم البياني، وmax للسطر الأساسي للحد الأدنى لقيمة بيانات الرسم البياني.

عند إنشاء Layout أو LayoutModifier مخصّص، حدِّد خطوط المحاذاة المخصّصة في طريقة MeasureScope.layout، التي تأخذ معلَمة alignmentLines: Map<AlignmentLine, Int>.

@Composable
private fun BarChart(
    dataPoints: List<Int>,
    modifier: Modifier = Modifier,
) {
    val maxValue: Float = remember(dataPoints) { dataPoints.maxOrNull()!! * 1.2f }

    BoxWithConstraints(modifier = modifier) {
        val density = LocalDensity.current
        with(density) {
            // ...
            // Calculate baselines
            val maxYBaseline = // ...
            val minYBaseline = // ...
            Layout(
                content = {},
                modifier = Modifier.drawBehind {
                    // ...
                }
            ) { _, constraints ->
                with(constraints) {
                    layout(
                        width = if (hasBoundedWidth) maxWidth else minWidth,
                        height = if (hasBoundedHeight) maxHeight else minHeight,
                        // Custom AlignmentLines are set here. These are propagated
                        // to direct and indirect parent composables.
                        alignmentLines = mapOf(
                            MinChartValue to minYBaseline.roundToInt(),
                            MaxChartValue to maxYBaseline.roundToInt()
                        )
                    ) {}
                }
            }
        }
    }
}

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

@Composable
private fun BarChartMinMax(
    dataPoints: List<Int>,
    maxText: @Composable () -> Unit,
    minText: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Layout(
        content = {
            maxText()
            minText()
            // Set a fixed size to make the example easier to follow
            BarChart(dataPoints, Modifier.size(200.dp))
        },
        modifier = modifier
    ) { measurables, constraints ->
        check(measurables.size == 3)
        val placeables = measurables.map {
            it.measure(constraints.copy(minWidth = 0, minHeight = 0))
        }

        val maxTextPlaceable = placeables[0]
        val minTextPlaceable = placeables[1]
        val barChartPlaceable = placeables[2]

        // Obtain the alignment lines from BarChart to position the Text
        val minValueBaseline = barChartPlaceable[MinChartValue]
        val maxValueBaseline = barChartPlaceable[MaxChartValue]
        layout(constraints.maxWidth, constraints.maxHeight) {
            maxTextPlaceable.placeRelative(
                x = 0,
                y = maxValueBaseline - (maxTextPlaceable.height / 2)
            )
            minTextPlaceable.placeRelative(
                x = 0,
                y = minValueBaseline - (minTextPlaceable.height / 2)
            )
            barChartPlaceable.placeRelative(
                x = max(maxTextPlaceable.width, minTextPlaceable.width) + 20,
                y = 0
            )
        }
    }
}
@Preview
@Composable
private fun ChartDataPreview() {
    MaterialTheme {
        BarChartMinMax(
            dataPoints = listOf(4, 24, 15),
            maxText = { Text("Max") },
            minText = { Text("Min") },
            modifier = Modifier.padding(24.dp)
        )
    }
}