מדידות מהותיות בפריסות אימייל

אחד מהכללים של Compose הוא שצריך למדוד את הצאצאים רק פעם אחת. מדידת צאצאים פעמיים גורמת לחריגה בזמן הריצה. אבל לפעמים צריך לקבל מידע על הצאצאים לפני שמודדים אותם.

בעזרת Intrinsics אפשר לשלוח שאילתות לצאצאים לפני שהם נמדדים בפועל.

אפשר לבקש מפונקציה קומפוזבילית את ה-IntrinsicSize.Min או ה-IntrinsicSize.Max שלה:

  • Modifier.width(IntrinsicSize.Min) – מה הרוחב המינימלי שנדרש כדי להציג את התוכן כמו שצריך?
  • Modifier.width(IntrinsicSize.Max) – מה הרוחב המקסימלי שנדרש כדי להציג את התוכן כמו שצריך?
  • Modifier.height(IntrinsicSize.Min) – מה הגובה המינימלי שנדרש כדי להציג את התוכן כמו שצריך?
  • Modifier.height(IntrinsicSize.Max) – מה הגובה המקסימלי שנדרש כדי להציג את התוכן כמו שצריך?

לדוגמה, אם שואלים את minIntrinsicHeight של Text עם אילוצים אינסופיים של width בפריסה מותאמת אישית, הוא מחזיר את height של Text כשהטקסט משורטט בשורה אחת.

התכונות המובנות בפעולה

אפשר ליצור רכיב Composable שמציג שני טקסטים במסך, שמופרדים באמצעות קו מפריד:

שני רכיבי טקסט זה לצד זה, עם קו הפרדה אנכי ביניהם

כדי לעשות את זה, משתמשים ב-Row עם שני רכיבי Text שניתנים ליצירה וממלאים את המקום הפנוי, וב-Divider באמצע. ה-Divider צריך להיות בגובה של ה-Text הכי גבוה, והוא צריך להיות דק (width = 1.dp).

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

ה-Divider מתרחב לכל המסך, וזו לא ההתנהגות הרצויה:

שני רכיבי טקסט זה לצד זה, עם קו מפריד ביניהם, אבל הקו המפריד נמתח מתחת לתחתית הטקסט

זה קורה כי Row מודד כל צאצא בנפרד, ואי אפשר להשתמש בגובה של Text כדי להגביל את Divider.

כדי שה-Divider ימלא את המרחב הזמין בגובה נתון, משתמשים במשתנה height(IntrinsicSize.Min).

הגובה של הצאצאים של height(IntrinsicSize.Min) יהיה הגובה הפנימי המינימלי שלהם. מכיוון שהמשתנה הזה הוא רקורסיבי, הוא שולח שאילתה ל-minIntrinsicHeight של Row ושל הצאצאים שלו.

כדי שהקוד יפעל כמו שצריך, צריך להוסיף לו את המאפיין הזה:

@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        VerticalDivider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),

            text = text2
        )
    }
}

// @Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

עם תצוגה מקדימה:

שני רכיבי טקסט זה לצד זה, עם קו הפרדה אנכי ביניהם

הגובה של Row נקבע באופן הבא:

  • ה-minIntrinsicHeight של ה-composable‏ Row הוא ה-minIntrinsicHeight המקסימלי של הצאצאים שלו.
  • הערך של minIntrinsicHeight ברכיב Divider הוא 0, כי הוא לא תופס מקום אם לא מוגדרות מגבלות.
  • השדה Text minIntrinsicHeight הוא של הטקסט של width ספציפי.
  • לכן, האילוץ height של הרכיב Row הופך לערך המקסימלי של minIntrinsicHeight של ה-Text.
  • לאחר מכן, ה-Divider מרחיב את ה-height שלו ל-height של האילוץ שניתן על ידי ה-Row.

מאפיינים פנימיים בפריסות בהתאמה אישית

כשיוצרים משתנה משנה מותאם אישית של Layout או layout, המידות הפנימיות מחושבות באופן אוטומטי על סמך הערכות. לכן, יכול להיות שהחישובים לא יהיו נכונים לכל הפריסות. ממשקי ה-API האלה כוללים אפשרויות לשינוי ברירות המחדל האלה.

כדי לציין את המידות הפנימיות של Layout בהתאמה אישית, צריך לשנות את הערכים של minIntrinsicWidth,‏ minIntrinsicHeight,‏ maxIntrinsicWidth ו-maxIntrinsicHeight בממשק MeasurePolicy כשיוצרים אותו. ההגדרה החדשה צריכה להתאים לפעולה של measure. לדוגמה, בעמודה בהתאמה אישית שבה הצאצאים מוערמים אנכית וכל צאצא מקבל את רוחב העמודה המלא, הגובה הפנימי המינימלי של width נתון הוא סכום הגבהים הפנימיים המינימליים של הצאצאים שלו עבור אותו width:

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                val childConstraints = constraints.copy(minHeight = 0)
                val placeables = measurables.map { measurable ->
                    measurable.measure(childConstraints)
                }
                val width = constraints.constrainWidth(
                    placeables.maxOfOrNull { it.width } ?: 0
                )
                val height = constraints.constrainHeight(
                    placeables.sumOf { it.height }
                )
                return layout(width, height) {
                    var yPosition = 0
                    placeables.forEach { placeable ->
                        placeable.placeRelative(x = 0, y = yPosition)
                        yPosition += placeable.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                return measurables.sumOf { measurable ->
                    measurable.minIntrinsicHeight(width)
                }
            }

            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )
}

כשיוצרים משתנה layout מותאם אישית, צריך לשנות את השיטות הקשורות בממשק LayoutModifier. החישוב הפנימי צריך להתאים ל-measure. לדוגמה, עבור height נתון, משתנה שמוסיף מרווח פנימי קבוע בכל הצדדים מחסיר את המרווח הפנימי האנכי לפני שהוא שולח שאילתה ל-minIntrinsicWidth של הצאצא, ומוסיף את המרווח הפנימי האופקי לתוצאה:

fun Modifier.myCustomModifier(padding: Dp = 8.dp) =
    this then object : LayoutModifier {
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val placeable = measurable.measure(
                constraints.offset(-horizontal, -vertical)
            )
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {
                placeable.place(paddingPx, paddingPx)
            }
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurable: IntrinsicMeasurable,
            height: Int
        ): Int {
            val paddingPx = padding.roundToPx()
            val horizontal = paddingPx * 2
            val vertical = paddingPx * 2
            val childHeight = if (height == Constraints.Infinity) {
                Constraints.Infinity
            } else {
                (height - vertical).coerceAtLeast(0)
            }
            return measurable.minIntrinsicWidth(childHeight) + horizontal
        }

        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    }