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

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

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

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

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

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

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 قابل للإنشاء مع محاذاة النص مع الحد الأقصى والأدنى لقيمة البيانات.
الشكل 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)
        )
    }
}