Targetbus12
Targetbus12

Reputation: 174

How to use xml:s android:id in Jetpack compose?

How can I use xml:s android:id like this:

<TextView android:id="@id/randomId">

In Jetpack compose? I'm thinking something like this:

Text(modifier = Modifier.layoutId("randomId"), text = text)

I'm asking because I use Appium which can identify android:ids and I would like to do the same for Compose too.

Upvotes: 2

Views: 8853

Answers (2)

Kenneth Ngedo
Kenneth Ngedo

Reputation: 95

You can make it work by setting in your parent layout

Modifier.semantics { testTagAsResourceId = true}

Then in your Compose View set the testTag. For example

Text(text = "Hello, World!", modifier = modifier.testTag("tvGreeting"))

This will prompt you to opt into Experimental Compose UI Api. Do that and you are fine.

Upvotes: 2

Thracian
Thracian

Reputation: 66546

Jetpack Compose is a declarative Ui which you create Ui components with functions with @Composable annotation unlike View system where you create instance of View or ViewGroup get it with id you defined to set attributes. In Jetpack Compose you declare attributes of a Composable as parameter of the function.

Modifier.layoutId is used when you build custom Composable with a specific layout to calculate dimensions and position of child Composables.

/**
 * Retrieves the tag associated to a composable with the [Modifier.layoutId] modifier.
 * For a parent data value to be returned by this property when not using the [Modifier.layoutId]
 * modifier, the parent data value should implement the [LayoutIdParentData] interface.
 *
 * Example usage:
 * @sample androidx.compose.ui.samples.LayoutTagChildrenUsage
 */
val Measurable.layoutId: Any?
    get() = (parentData as? LayoutIdParentData)?.layoutId

When you build a custom layout in summary it's as

@Composable
private fun MyLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {

    Layout(
        modifier = modifier,
        content = content
    ) { measurables: List<Measurable>, constraints: Constraints ->

        val placeables: List<Placeable> = measurables.map { measurable: Measurable ->
            measurable.measure(constraints)
        }

        // Width and height of parent composable depends on requirements
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEach { placeable: Placeable ->
                // place composables based on requirements vertically, horizontally, diagonal, etc.
            }
        }
    }
}

1- You create a Layout that needs a MeasurePolicy which we set with lambda and returns MeasureResult via layout(width, height) function.

2- Constraints are the limits that are set by size modifiers, scroll modifiers or how parent chose to limit size. In Constraints section i explained which modifier returns which bound. You can measure a measurable only within these bounds.

But you can modify Constraints if you want your Composable to choose different bounds via constraints.copy() or Constraints.fixed(), constraints.offset(). One important thing is max bound still bound to parent's maxWidth/height in constraints.

Then, you measure your measurables only once, if you try to measure again you get an exception.

However, sometimes you need to set dimensions of a Comosable based on another Composable. For instance, an Image is 100.dp which is set on runtime and you want Text to be adjusted 100.dp and you don't know in which order Image an Text is added in content.

You either need to use SubcomposeLayout which uses subcompose function to remeasure Composables or Modifier.layoutId to identify which Composable you measure first as in example below or as in this answer's Selectively measuring to match one Sibling to Another section

@Composable
private fun MyLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {

    Layout(
        modifier = modifier,
        content = content
    ) { measurables: List<Measurable>, constraints: Constraints ->

        val imageMeasurable: Measurable = measurables.find { it.layoutId == "image" }!!
        val textMeasurable: Measurable = measurables.find { it.layoutId == "text" }!!

        val imagePlaceable = imageMeasurable.measure(constraints.copy(minWidth = 0, minHeight = 0))

        // Limit text width to image width by setting min and max width at image width
        val textPlaceable = textMeasurable.measure(
            constraints.copy(
                minWidth = imagePlaceable.width,
                maxWidth = imagePlaceable.width
            )
        )

        val width = imagePlaceable.width
        val imagePlaceableHeight = imagePlaceable.height
        val height = imagePlaceableHeight + textPlaceable.height
        // Width and height of parent composable depends on requirements
        layout(width, height) {
            imagePlaceable.placeRelative(0, 0)
            textPlaceable.placeRelative(0, imagePlaceableHeight)
        }
    }
}

Demonstration

MyLayout(
    modifier = Modifier
        .fillMaxWidth()
        .border(1.dp, Color.Cyan)
) {
    Image(
        modifier = Modifier
            .size(150.dp)
            .layoutId("image"),
        painter = painterResource(id = R.drawable.landscape1),
        contentDescription = null,
        contentScale = ContentScale.FillBounds
    )

    Text(
        modifier = Modifier
            .layoutId("text")
            .border(2.dp, Color.Red),
        text = "Hello world some very long text that will be scaled"
    )
}

MyLayout(
    modifier = Modifier
        .fillMaxWidth()
        .border(1.dp, Color.Cyan)
) {

    Text(
        modifier = Modifier
            .layoutId("text")
            .border(2.dp, Color.Red),
        text = "Hello world some very long text that will be scaled"
    )

    Image(
        modifier = Modifier
            .size(100.dp)
            .layoutId("image"),
        painter = painterResource(id = R.drawable.landscape1),
        contentDescription = null,
        contentScale = ContentScale.FillBounds
    )

}

Result

enter image description here

As can be seen in image we used layoutId to get Image first then measured Text to match its width to Image. I don't think this would work with appium unless it has some kind of inspector for Modifier

Upvotes: 3

Related Questions