@@ -9,6 +9,10 @@ import android.text.Layout
99import android.text.Spanned
1010import android.view.View
1111import android.view.ViewTreeObserver
12+ import android.os.Looper
13+ import android.view.ViewGroup
14+ import android.view.View.MeasureSpec
15+ import androidx.compose.ui.platform.ComposeView
1216import androidx.compose.foundation.layout.Box
1317import androidx.compose.foundation.layout.height
1418import androidx.compose.foundation.layout.padding
@@ -339,12 +343,11 @@ class ComposePlaceholderManager(
339343 val editorWidth = if (aztecText.width > 0 ) {
340344 aztecText.width - aztecText.paddingStart - aztecText.paddingEnd
341345 } else aztecText.maxImagesWidth
342- drawable.setBounds(
343- 0 ,
344- 0 ,
345- adapter.calculateWidth(attrs, editorWidth),
346- adapter.calculateHeight(attrs, editorWidth)
347- )
346+ val widthPx = adapter.calculateWidth(attrs, editorWidth)
347+ val heightPx = computeHeightPx(adapter, attrs, editorWidth, widthPx)
348+ // Reserve additional flow space after the placeholder to visually separate following blocks
349+ val flowHeight = heightPx + (adapter.bottomSpacingPx(attrs))
350+ drawable.setBounds(0 , 0 , widthPx, flowHeight)
348351 return drawable
349352 }
350353
@@ -409,16 +412,19 @@ class ComposePlaceholderManager(
409412
410413 val adapter = adapters[type]!!
411414 val windowWidth = parentTextViewRect.right - parentTextViewRect.left - EDITOR_INNER_PADDING
412- val height = adapter.calculateHeight(attrs, windowWidth)
415+ val targetWidth = adapter.calculateWidth(attrs, windowWidth)
416+ val measuredHeight = computeHeightPx(adapter, attrs, windowWidth, targetWidth)
417+ val extraBottom = adapter.bottomSpacingPx(attrs)
418+ val height = measuredHeight + extraBottom
413419 parentTextViewRect.top + = parentTextViewTopAndBottomOffset
414420 parentTextViewRect.bottom = parentTextViewRect.top + height
415421
416422 val box = _composeViewState .value[uuid]
417- val newWidth = adapter.calculateWidth(attrs, windowWidth) - EDITOR_INNER_PADDING
418- val newHeight = height - EDITOR_INNER_PADDING
419- val padding = 10
420- val newLeftPadding = parentTextViewRect.left + padding + aztecText.paddingStart
421- val newTopPadding = parentTextViewRect.top + padding
423+ val newWidth = targetWidth
424+ val newHeight = measuredHeight
425+ val overlayPad = adapter.overlayPaddingPx(attrs)
426+ val newLeftPadding = parentTextViewRect.left + overlayPad.left + aztecText.paddingStart
427+ val newTopPadding = parentTextViewRect.top + overlayPad.top
422428 box?.let { existingView ->
423429 val widthSame = existingView.width == newWidth
424430 val heightSame = existingView.height == newHeight
@@ -431,10 +437,11 @@ class ComposePlaceholderManager(
431437 }
432438 _composeViewState .value = _composeViewState .value.let { state ->
433439 val mutableState = state.toMutableMap()
440+ val adjustedHeight = newHeight + (adapter.contentHeightAdjustmentPx(attrs))
434441 mutableState[uuid] = ComposeView (
435442 uuid = uuid,
436443 width = newWidth,
437- height = newHeight ,
444+ height = adjustedHeight ,
438445 topMargin = newTopPadding,
439446 leftMargin = newLeftPadding,
440447 visible = true ,
@@ -445,6 +452,70 @@ class ComposePlaceholderManager(
445452 }
446453 }
447454
455+ private suspend fun computeHeightPx (
456+ adapter : ComposePlaceholderAdapter ,
457+ attrs : AztecAttributes ,
458+ windowWidth : Int ,
459+ contentWidthPx : Int
460+ ): Int =
461+ when (val policy = adapter.sizingPolicy(attrs)) {
462+ is ComposePlaceholderAdapter .SizingPolicy .FixedHeightPx -> policy.heightPx
463+
464+ is ComposePlaceholderAdapter .SizingPolicy .AspectRatio -> (policy.ratio * contentWidthPx).toInt()
465+
466+ ComposePlaceholderAdapter .SizingPolicy .MatchWidthWrapContentHeight ->
467+ preMeasureHeight(adapter, attrs, contentWidthPx) ? : adapter.calculateHeight(attrs, windowWidth)
468+
469+ ComposePlaceholderAdapter .SizingPolicy .Unknown -> adapter.calculateHeight(attrs, windowWidth)
470+ }
471+
472+ private suspend fun preMeasureHeight (
473+ adapter : ComposePlaceholderAdapter ,
474+ attrs : AztecAttributes ,
475+ widthPx : Int
476+ ): Int? {
477+ // Pre-measure only on main thread. If not on main, fall back to legacy path
478+ if (Looper .myLooper() != Looper .getMainLooper()) return null
479+ val measurer = object : ComposePlaceholderAdapter .PlaceholderMeasurer {
480+ override suspend fun measure (content : @Composable () -> Unit , widthPx : Int ): Int {
481+ if (! aztecText.isAttachedToWindow) return - 1
482+ val parent = aztecText.parent as ? ViewGroup ? : return - 1
483+ val composeView = ComposeView (aztecText.context)
484+ composeView.visibility = View .GONE
485+ composeView.layoutParams = ViewGroup .LayoutParams (0 , 0 )
486+ try {
487+ parent.addView(composeView)
488+ composeView.setContent {
489+ Box (
490+ Modifier
491+ .width(with (LocalDensity .current) { widthPx.toDp() })
492+ ) {
493+ content()
494+ }
495+ }
496+ val wSpec = MeasureSpec .makeMeasureSpec(widthPx, MeasureSpec .EXACTLY )
497+ val hSpec = MeasureSpec .makeMeasureSpec(0 , MeasureSpec .UNSPECIFIED )
498+ composeView.measure(wSpec, hSpec)
499+ return composeView.measuredHeight
500+ } catch (_: IllegalStateException ) {
501+ return - 1
502+ } finally {
503+ parent.removeView(composeView)
504+ }
505+ }
506+ }
507+ // Let adapter compute/measure if it wants to
508+ val fromAdapter = adapter.preComposeMeasureHeight(attrs, widthPx, measurer)
509+ if (fromAdapter != null && fromAdapter >= 0 ) return fromAdapter
510+ // If adapter did not implement it but hinted wrap content policy, measure the actual content once
511+ if (adapter.sizingPolicy(attrs) == ComposePlaceholderAdapter .SizingPolicy .MatchWidthWrapContentHeight ) {
512+ val uuid = attrs.getValue(UUID_ATTRIBUTE )
513+ val h = measurer.measure(content = { adapter.Placeholder (uuid, attrs) }, widthPx = widthPx)
514+ return if (h >= 0 ) h else null
515+ }
516+ return null
517+ }
518+
448519 private fun validateAttributes (attributes : AztecAttributes ): Boolean {
449520 return attributes.hasAttribute(UUID_ATTRIBUTE ) &&
450521 attributes.hasAttribute(TYPE_ATTRIBUTE ) &&
0 commit comments