@@ -4,10 +4,13 @@ package org.wordpress.aztec.placeholders
44
55import android.graphics.Rect
66import android.graphics.drawable.Drawable
7+ import android.os.Looper
78import android.text.Editable
89import android.text.Layout
910import android.text.Spanned
1011import android.view.View
12+ import android.view.View.MeasureSpec
13+ import android.view.ViewGroup
1114import android.view.ViewTreeObserver
1215import androidx.compose.foundation.layout.Box
1316import androidx.compose.foundation.layout.height
@@ -17,6 +20,7 @@ import androidx.compose.runtime.Composable
1720import androidx.compose.runtime.collectAsState
1821import androidx.compose.runtime.key
1922import androidx.compose.ui.Modifier
23+ import androidx.compose.ui.platform.ComposeView
2024import androidx.compose.ui.platform.LocalDensity
2125import androidx.compose.ui.zIndex
2226import androidx.core.content.ContextCompat
@@ -339,12 +343,23 @@ 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+
347+ if (adapter.sizingPolicy(attrs) != ComposePlaceholderAdapter .SizingPolicy .Unknown ) {
348+ // New behavior with enhanced measuring
349+ val widthPx = adapter.calculateWidth(attrs, editorWidth)
350+ val heightPx = computeHeightPx(adapter, attrs, editorWidth, widthPx)
351+ // Reserve additional flow space after the placeholder to visually separate following blocks
352+ val flowHeight = heightPx + (adapter.bottomSpacingPx(attrs))
353+ drawable.setBounds(0 , 0 , widthPx, flowHeight)
354+ } else {
355+ // Legacy behavior
356+ drawable.setBounds(
357+ 0 ,
358+ 0 ,
359+ adapter.calculateWidth(attrs, editorWidth),
360+ adapter.calculateHeight(attrs, editorWidth)
361+ )
362+ }
348363 return drawable
349364 }
350365
@@ -409,40 +424,160 @@ class ComposePlaceholderManager(
409424
410425 val adapter = adapters[type]!!
411426 val windowWidth = parentTextViewRect.right - parentTextViewRect.left - EDITOR_INNER_PADDING
427+
428+ // Check if using new sizing policy or legacy behavior
429+ val newComposeView = if (adapter.sizingPolicy(attrs) != ComposePlaceholderAdapter .SizingPolicy .Unknown ) {
430+ createComposeViewWithSizingPolicy(
431+ adapter, attrs, uuid, windowWidth, parentTextViewRect, parentTextViewTopAndBottomOffset
432+ )
433+ } else {
434+ createComposeViewWithLegacy(
435+ adapter, attrs, uuid, windowWidth, parentTextViewRect, parentTextViewTopAndBottomOffset
436+ )
437+ }
438+
439+ // Check if view needs updating
440+ val existingView = _composeViewState .value[uuid]
441+ if (existingView != null &&
442+ existingView.width == newComposeView.width &&
443+ existingView.height == newComposeView.height &&
444+ existingView.topMargin == newComposeView.topMargin &&
445+ existingView.leftMargin == newComposeView.leftMargin &&
446+ existingView.attrs == attrs
447+ ) {
448+ return
449+ }
450+
451+ // Update compose view state
452+ _composeViewState .value = _composeViewState .value.toMutableMap().apply {
453+ this [uuid] = newComposeView
454+ }
455+ }
456+
457+ private suspend fun createComposeViewWithSizingPolicy (
458+ adapter : ComposePlaceholderAdapter ,
459+ attrs : AztecAttributes ,
460+ uuid : String ,
461+ windowWidth : Int ,
462+ parentTextViewRect : Rect ,
463+ parentTextViewTopAndBottomOffset : Int
464+ ): ComposeView {
465+ val targetWidth = adapter.calculateWidth(attrs, windowWidth)
466+ val measuredHeight = computeHeightPx(adapter, attrs, windowWidth, targetWidth)
467+ val extraBottom = adapter.bottomSpacingPx(attrs)
468+ val height = measuredHeight + extraBottom
469+ parentTextViewRect.top + = parentTextViewTopAndBottomOffset
470+ parentTextViewRect.bottom = parentTextViewRect.top + height
471+
472+ val overlayPad = adapter.overlayPaddingPx(attrs)
473+ val newLeftPadding = parentTextViewRect.left + overlayPad.left + aztecText.paddingStart
474+ val newTopPadding = parentTextViewRect.top + overlayPad.top
475+ val adjustedHeight = measuredHeight + adapter.contentHeightAdjustmentPx(attrs)
476+
477+ return ComposeView (
478+ uuid = uuid,
479+ width = targetWidth,
480+ height = adjustedHeight,
481+ topMargin = newTopPadding,
482+ leftMargin = newLeftPadding,
483+ visible = true ,
484+ adapterKey = adapter.type,
485+ attrs = attrs
486+ )
487+ }
488+
489+ private suspend fun createComposeViewWithLegacy (
490+ adapter : ComposePlaceholderAdapter ,
491+ attrs : AztecAttributes ,
492+ uuid : String ,
493+ windowWidth : Int ,
494+ parentTextViewRect : Rect ,
495+ parentTextViewTopAndBottomOffset : Int
496+ ): ComposeView {
412497 val height = adapter.calculateHeight(attrs, windowWidth)
413498 parentTextViewRect.top + = parentTextViewTopAndBottomOffset
414499 parentTextViewRect.bottom = parentTextViewRect.top + height
415500
416- val box = _composeViewState .value[uuid]
417501 val newWidth = adapter.calculateWidth(attrs, windowWidth) - EDITOR_INNER_PADDING
418502 val newHeight = height - EDITOR_INNER_PADDING
419503 val padding = 10
420504 val newLeftPadding = parentTextViewRect.left + padding + aztecText.paddingStart
421505 val newTopPadding = parentTextViewRect.top + padding
422- box?.let { existingView ->
423- val widthSame = existingView.width == newWidth
424- val heightSame = existingView.height == newHeight
425- val topMarginSame = existingView.topMargin == newTopPadding
426- val leftMarginSame = existingView.leftMargin == newLeftPadding
427- val attrsSame = existingView.attrs == attrs
428- if (widthSame && heightSame && topMarginSame && leftMarginSame && attrsSame) {
429- return
506+
507+ return ComposeView (
508+ uuid = uuid,
509+ width = newWidth,
510+ height = newHeight,
511+ topMargin = newTopPadding,
512+ leftMargin = newLeftPadding,
513+ visible = true ,
514+ adapterKey = adapter.type,
515+ attrs = attrs
516+ )
517+ }
518+
519+ private suspend fun computeHeightPx (
520+ adapter : ComposePlaceholderAdapter ,
521+ attrs : AztecAttributes ,
522+ windowWidth : Int ,
523+ contentWidthPx : Int
524+ ): Int =
525+ when (val policy = adapter.sizingPolicy(attrs)) {
526+ is ComposePlaceholderAdapter .SizingPolicy .FixedHeightPx -> policy.heightPx
527+
528+ is ComposePlaceholderAdapter .SizingPolicy .AspectRatio -> (policy.ratio * contentWidthPx).toInt()
529+
530+ ComposePlaceholderAdapter .SizingPolicy .MatchWidthWrapContentHeight ->
531+ preMeasureHeight(adapter, attrs, contentWidthPx) ? : adapter.calculateHeight(attrs, windowWidth)
532+
533+ ComposePlaceholderAdapter .SizingPolicy .Unknown -> adapter.calculateHeight(attrs, windowWidth)
534+ }
535+
536+ private suspend fun preMeasureHeight (
537+ adapter : ComposePlaceholderAdapter ,
538+ attrs : AztecAttributes ,
539+ widthPx : Int
540+ ): Int? {
541+ // Pre-measure only on main thread. If not on main, fall back to legacy path
542+ if (Looper .myLooper() != Looper .getMainLooper()) return null
543+ val measurer = object : ComposePlaceholderAdapter .PlaceholderMeasurer {
544+ override suspend fun measure (content : @Composable () -> Unit , widthPx : Int ): Int {
545+ if (! aztecText.isAttachedToWindow) return - 1
546+ val parent = aztecText.parent as ? ViewGroup ? : return - 1
547+ val composeView = ComposeView (aztecText.context)
548+ composeView.visibility = View .GONE
549+ composeView.layoutParams = ViewGroup .LayoutParams (0 , 0 )
550+ try {
551+ parent.addView(composeView)
552+ composeView.setContent {
553+ Box (
554+ Modifier
555+ .width(with (LocalDensity .current) { widthPx.toDp() })
556+ ) {
557+ content()
558+ }
559+ }
560+ val wSpec = MeasureSpec .makeMeasureSpec(widthPx, MeasureSpec .EXACTLY )
561+ val hSpec = MeasureSpec .makeMeasureSpec(0 , MeasureSpec .UNSPECIFIED )
562+ composeView.measure(wSpec, hSpec)
563+ return composeView.measuredHeight
564+ } catch (_: IllegalStateException ) {
565+ return - 1
566+ } finally {
567+ parent.removeView(composeView)
568+ }
430569 }
431570 }
432- _composeViewState .value = _composeViewState .value.let { state ->
433- val mutableState = state.toMutableMap()
434- mutableState[uuid] = ComposeView (
435- uuid = uuid,
436- width = newWidth,
437- height = newHeight,
438- topMargin = newTopPadding,
439- leftMargin = newLeftPadding,
440- visible = true ,
441- adapterKey = adapter.type,
442- attrs = attrs
443- )
444- mutableState
571+ // Let adapter compute/measure if it wants to
572+ val fromAdapter = adapter.preComposeMeasureHeight(attrs, widthPx, measurer)
573+ if (fromAdapter != null && fromAdapter >= 0 ) return fromAdapter
574+ // If adapter did not implement it but hinted wrap content policy, measure the actual content once
575+ if (adapter.sizingPolicy(attrs) == ComposePlaceholderAdapter .SizingPolicy .MatchWidthWrapContentHeight ) {
576+ val uuid = attrs.getValue(UUID_ATTRIBUTE )
577+ val h = measurer.measure(content = { adapter.Placeholder (uuid, attrs) }, widthPx = widthPx)
578+ return if (h >= 0 ) h else null
445579 }
580+ return null
446581 }
447582
448583 private fun validateAttributes (attributes : AztecAttributes ): Boolean {
0 commit comments