خطوط المحاذاة في 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، لكي تتمكّن الدوال المركّبة الأخرى من المحاذاة مع الحد الأقصى والحد الأدنى لقيمة البيانات في الرسم البياني. تمت محاذاة عنصرَي نص، هما الحد الأقصى والحد الأدنى، إلى منتصف خطوط المحاذاة المخصّصة.

دالة 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)
        )
    }
}