Составление Android: смещение на долю родительского размера

Я хочу разместить несколько составных элементов в определенных позициях поверх изображения. Однако изображение будет адаптивным (размер его изменится), поэтому я не смогу использовать Modifier.offset(some_fixed_offset.dp).

Modifier.offset(10.percent) было бы идеально, но это не работает (или работает?). Как я могу это реализовать? Вот возможные пути, которые я кратко рассмотрел:

  • BoxWithConstraints имеет минимальный/максимальный размер, но мне понадобится точный размер, чтобы самому рассчитать относительные позиции.
  • Пользовательский составной элемент может подойти, но на первый взгляд неочевидно получить фактический родительский размер. Он скорее сообщает родителю о желаемом внутреннем размере.

Прежде чем я углублюсь в индивидуальное решение, есть ли более простой способ, похожий на .offset(10.percent)?

Вот тестовый код, с которого я начал:

@Composable
fun GeneralPlaceHolder(modifier: Modifier = Modifier) {
    Box (
        modifier
            .background(Color.Red)
            .size(300.dp) //<-- if this size changes, 
                          //    the yellow dot should hold it's relative position
            .padding(10.dp)
    ) {
        Box(modifier = Modifier
            .background(Color.Blue)
            .fillMaxSize()) {
            DaThing()
        }
    }
}

@Composable
fun DaThing() {
    Box {
        Image(painter = painterResource(id = R.drawable.testgrid10x10), 
           contentDescription = "", 
           modifier = Modifier.fillMaxSize())
        Box(modifier = Modifier
            .size(5.dp)
            .offset(x = 10.dp, y=20.dp) //<-- percentage no can do?
            .background(Color.Yellow)
        )
    }
}

И скриншот с сеткой в ​​качестве "изображения". Это сетка 10x10, поэтому offset(10.percent, 10.percent) поместит желтую точку в первую точку сетки.

Пожалуйста, отредактируйте свой вопрос и добавьте testgrid10x10 изображение.

Adnan Habib 13.07.2024 17:18

@AdnanHabib, это могла быть фотография котенка - это не имеет значения. Решение должно работать с любым изображением, графическим или компонуемым в целом. Сетка — это XML-ресурс с элементом <vector>. Это просто то, что я быстро создаю в Adobe Illustrator -> .svg -> Android Studio > Создать> Векторный ресурс» > .xml.

Erik Bongers 13.07.2024 17:37
2
2
106
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Modifier.offset() изменяет размещаемое размещение на основе фиксированных значений с помощью параметров, как

private class OffsetNode(
    var x: Dp,
    var y: Dp,
    var rtlAware: Boolean
) : LayoutModifierNode, Modifier.Node() {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val placeable = measurable.measure(constraints)
        return layout(placeable.width, placeable.height) {
            if (rtlAware) {
                placeable.placeRelative(x.roundToPx(), y.roundToPx())
            } else {
                placeable.place(x.roundToPx(), y.roundToPx())
            }
        }
    }
}

Изменив placeable.placeRelative(x.roundToPx(), y.roundToPx()) на процент родительского элемента Constraints с помощью Modifier.layout{}, вы можете разместить свой Composable для значения в процентах на основе родительского элемента.

Однако вам нужно обратить внимание на то, что Constraints должен исходить от родителя, а это значит, что то, куда вы поместите Modifier.layout{}, имеет значение. Если вы установите его после любого размера, смещения или заполнения Modifier, Constraints может измениться, поскольку модификаторы размера и заполнения делают одно и то же, основываясь на своей внутренней логике. Если вы хотите узнать о том, как работают ограничения, модификаторы макета и размера, вы можете обратиться к этому ответу.

@Preview
@Composable
fun GeneralPlaceHolderTest() {
    GeneralPlaceHolder()
}

@Composable
fun GeneralPlaceHolder(modifier: Modifier = Modifier) {
    Column {

        var percentage by remember {
            mutableStateOf(0f)
        }

        Slider(
            value = percentage,
            onValueChange = {
                percentage = it
            }
        )

        Text("Percentage: $percentage")

        Box(
            modifier
                .background(Color.Red)
                .size(300.dp) //<-- if this size changes,
                //    the yellow dot should hold it's relative position
                .padding(10.dp)
        ) {
            Box(
                modifier = Modifier
                    .background(Color.Blue)
                    .fillMaxSize()
            ) {
                DaThing(percentage)
            }
        }
    }
}

@Composable
fun DaThing(percentage: Float) {
    Box {
        Box(
            modifier = Modifier.fillMaxSize().drawBehind {
                drawRect(color = Color.Blue)
            }
        )

        Box(
            modifier = Modifier
                .layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    layout(placeable.width, placeable.height) {
                        placeable.placeRelative(
                            x = (constraints.maxWidth * percentage).toInt(),
                            y = (constraints.maxHeight * percentage).toInt()
                        )
                    }
                }
                .size(5.dp)
                .background(Color.Yellow)
        )
    }
}

Этот макет также можно реорганизовать как модификатор.

fun Modifier.offsetByPercent(percentage: Float) = this.then(
    Modifier.layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        layout(placeable.width, placeable.height) {
            placeable.placeRelative(
                x = (constraints.maxWidth * percentage).toInt(),
                y = (constraints.maxHeight * percentage).toInt()
            )
        }
    }
)

И может использоваться как

Box(
    modifier = Modifier
        .offsetByPercent(percentage)
        .size(5.dp)
        .background(Color.Yellow)
)

Вполне рабочий пример, спасибо. Мне нужна дюжина или около того таких относительно расположенных объектов, поэтому я оберну этот внутренний блок в небольшой специальный компонуемый объект.

Erik Bongers 13.07.2024 18:22

Если хотите, вы можете превратить это в модификатор, например, в смещение. Я не проверял RtL в качестве модификатора и не проверял, чтобы процент находился в диапазоне 0..1f, но их можно легко добавить.

Thracian 13.07.2024 18:22

Поскольку я новичок в Kotlin — вы только что познакомили меня с функциями расширения :) И эту расширенную функцию-модификатор — именно то, что я собираюсь использовать, хотя и с отдельным процентом для x и y.

Erik Bongers 13.07.2024 19:01

Продолжая ответ Трециана , я в конечном итоге создал собственный составной элемент в дополнение к пользовательскому модификатору. Причина в том, что модификатор полагается на то, что родительский элемент имеет тот же размер и положение, что и изображение, поверх которого я хочу разместить составные элементы. Поскольку трудно заставить родительский элемент обернуть контент (он имеет тенденцию заполнять все доступное пространство), мне действительно нужно было, чтобы наложение основывалось на положении и размере изображения, а не на родительском элементе. Следовательно, мы используем собственный составной элемент, в котором мы используем внутренний размер изображения для расчета того, где оно будет эффективно нарисовано внутри составного элемента изображения, а именно в его центре:

@Composable
fun PercentageImageOverlay(painter: Painter,
                           overlay: @Composable () -> Unit,
                           modifier: Modifier = Modifier,
                           ) {
    val image = @Composable {
        Image(
            painter = painter,
            contentDescription = "",
            modifier = Modifier.wrapContentSize()
        )
    }
    Layout(contents = listOf(image) + overlay, modifier) {
            (imageMeasureables, overlayMeasureables), constraints ->


        val imagePlaceable = imageMeasureables.first().measure(constraints)
        //newConstraints = size of Image() composable.
        val imgW = imagePlaceable.width.toFloat()
        val imgH = imagePlaceable.height.toFloat()
        //intrinsic size of the image to be painted gives us the aspect ratio.
        val intrW = painter.intrinsicSize.width.toFloat()
        val intrH = painter.intrinsicSize.height.toFloat()
        //1. get both aspect ratios. If ratio > 1 : a high shape (portrait)
        val imgRatio= imgH/imgW
        val paintRatio = intrH/intrW

        // figure out where image will be painted.
        val paintInImageW: Float
        val paintInImageH: Float

        if ( paintRatio > imgRatio) { // painting higher than image. Resize painting to fit the image height.
            paintInImageH = imgH // = intrH * (imgH / intrH)
            paintInImageW = intrW * (imgH / intrH)
        } else {
            paintInImageW = imgW // = intrW * (imgW / intrW)
            paintInImageH = intrH * (imgW / intrW)
        }

        val newConstraints = Constraints(0, paintInImageW.toInt(), 
                                         0, paintInImageH.toInt())

        val overlayPlaceables = overlayMeasureables.map { it.measure(newConstraints) }

        val offsetX = (imgW - paintInImageW) / 2
        val offsetY = (imgH - paintInImageH) / 2

        layout(imagePlaceable.width, imagePlaceable.height) {
            imagePlaceable.place(0, 0)
            overlayPlaceables.map { it.place(offsetX.toInt(), offsetY.toInt()) }
        }

    }
}

В сочетании со слегка измененным модификатором, который также устанавливает процентный размер составного объекта:

fun Modifier.placeByPercent(percentageX: Float, percentageY: Float, percentageW: Float, percentageH: Float) = this.then(
    Modifier.layout { measurable, constraints ->
        val width = (constraints.maxWidth * percentageW).toInt()
        val height = (constraints.maxHeight * percentageH).toInt()
        val placeable = measurable.measure(Constraints(width, width, height, height))
        layout(placeable.width, placeable.height) {
            placeable.place(
                x = (constraints.maxWidth * percentageX).toInt(),
                y = (constraints.maxHeight * percentageY).toInt()
            )
        }
    }
)

Ниже (почти) предварительный пример. Он использует 2 изображения, которые можно легко удалить или заменить. Это также проверяет обоснованную озабоченность, возникшую у Thracian: модификатор сам по себе не работает при расположении справа налево. Тем не менее, пользовательский составной компонент, похоже, исправляет это:

@Composable
fun GeneralPlaceHolder(modifier: Modifier = Modifier) {
    Surface(
        modifier
            .background(Color.Red)
            .padding(10.dp)
            .size(bigSize)
    ) {
            val painter = painterResource(id = R.drawable.testgrid10x10)
            PercentageImageOverlay(painter,
                overlay =  @Composable {
                    SomeOverlay(percentageX = .1f, percentageY = .2f,
                                percentageWidth = .1f, percentageHeight = .1f)
                    SomeOverlay(percentageX = .5f, percentageY = .4f,
                                percentageWidth = .2f, percentageHeight = .2f)
                },
                modifier = Modifier
                    .background(Color.Blue)
                    .fillMaxSize()
            )
    }
}

@Composable
fun SomeOverlay(percentageX: Float, percentageY: Float, 
                percentageWidth: Float, percentageHeight: Float) {
    Box(
        modifier = Modifier
            .placeByPercent(percentageX, percentageY, 
                            percentageWidth, percentageHeight)
            .background(Color.Green)
    ) {
        Image(painterResource(id = R.drawable.someCircle), contentDescription = null,
            modifier = Modifier.fillMaxSize()
        )
    }
}

val bigSize = 200.dp

@Preview(name = "Left-To-Right", locale = "en")
@Preview(name = "Right-To-Left", locale = "ar")
annotation class LayoutDirectionPreviews

@LayoutDirectionPreviews
@Preview()
@Composable
fun ThePreview(
) {
    Ears2Theme {
        GeneralPlaceHolder(
            modifier = Modifier
                .size(bigSize) //<-- if this size changes, everything should still work.
        )
    }
}

Я уверен, что никто не собирается использовать этот код, чтобы украсить фотографии котят розовыми звездочками и тому подобным...

Другие вопросы по теме