Я хочу разместить несколько составных элементов в определенных позициях поверх изображения. Однако изображение будет адаптивным (размер его изменится), поэтому я не смогу использовать 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)
поместит желтую точку в первую точку сетки.
@AdnanHabib, это могла быть фотография котенка - это не имеет значения. Решение должно работать с любым изображением, графическим или компонуемым в целом. Сетка — это XML-ресурс с элементом <vector>
. Это просто то, что я быстро создаю в Adobe Illustrator -> .svg -> Android Studio > Создать> Векторный ресурс» > .xml.
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)
)
Вполне рабочий пример, спасибо. Мне нужна дюжина или около того таких относительно расположенных объектов, поэтому я оберну этот внутренний блок в небольшой специальный компонуемый объект.
Если хотите, вы можете превратить это в модификатор, например, в смещение. Я не проверял RtL в качестве модификатора и не проверял, чтобы процент находился в диапазоне 0..1f, но их можно легко добавить.
Поскольку я новичок в Kotlin — вы только что познакомили меня с функциями расширения :) И эту расширенную функцию-модификатор — именно то, что я собираюсь использовать, хотя и с отдельным процентом для x и y.
Продолжая ответ Трециана , я в конечном итоге создал собственный составной элемент в дополнение к пользовательскому модификатору. Причина в том, что модификатор полагается на то, что родительский элемент имеет тот же размер и положение, что и изображение, поверх которого я хочу разместить составные элементы. Поскольку трудно заставить родительский элемент обернуть контент (он имеет тенденцию заполнять все доступное пространство), мне действительно нужно было, чтобы наложение основывалось на положении и размере изображения, а не на родительском элементе. Следовательно, мы используем собственный составной элемент, в котором мы используем внутренний размер изображения для расчета того, где оно будет эффективно нарисовано внутри составного элемента изображения, а именно в его центре:
@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.
)
}
}
Я уверен, что никто не собирается использовать этот код, чтобы украсить фотографии котят розовыми звездочками и тому подобным...
Пожалуйста, отредактируйте свой вопрос и добавьте
testgrid10x10
изображение.