Skip to content

Commit 8d2f84a

Browse files
fix(android): center inline images on the cap height like iOS
An inline image's bottom sat on the baseline, so an icon taller than the capitals stood above the line of text. iOS centers inline attachments on the font's cap height; Android now draws the image centered on the cap height of the paint it is drawn with. Both ImageSpan copies change the same way.
1 parent a9aa97a commit 8d2f84a

3 files changed

Lines changed: 79 additions & 2 deletions

File tree

  • packages
    • enriched-markdown-android/ui/src
    • react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans

‎packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.graphics.Canvas
88
import android.graphics.Color
99
import android.graphics.Paint
1010
import android.graphics.Path
11+
import android.graphics.Rect
1112
import android.graphics.drawable.Drawable
1213
import android.net.Uri
1314
import android.os.Build
@@ -52,6 +53,7 @@ class ImageSpan(
5253
private val requestKey: String = ImageCache.requestKey(imageUrl, requestHeaders)
5354

5455
private var cachedWidth: Int = 0
56+
private val capBounds = Rect()
5557
private var viewRef: WeakReference<TextView>? = null
5658
private var sourceDrawable: Drawable? = null
5759

@@ -221,8 +223,10 @@ class ImageSpan(
221223
val drawable = getDrawable()
222224
canvas.withSave {
223225
if (isInline) {
226+
// Centered on the cap height of the surrounding text, as iOS places inline attachments.
224227
val imageHeight = drawable.bounds.height()
225-
translate(x, (y - imageHeight + (imageHeight * 0.1f)))
228+
paint.getTextBounds("H", 0, 1, capBounds)
229+
translate(x, y - (capBounds.height() + imageHeight) / 2f)
226230
} else {
227231
translate(x, top.toFloat())
228232
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.swmansion.enriched.markdown
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import android.graphics.Canvas
6+
import android.graphics.Color
7+
import android.graphics.Rect
8+
import android.util.TypedValue
9+
import android.view.View
10+
import android.widget.TextView
11+
import androidx.test.core.app.ApplicationProvider
12+
import androidx.test.ext.junit.runners.AndroidJUnit4
13+
import com.swmansion.enriched.markdown.spans.registerWithSpans
14+
import com.swmansion.enriched.markdown.test.MarkdownRenderTestSupport
15+
import com.swmansion.enriched.markdown.test.MarkdownRenderTestSupport.render
16+
import com.swmansion.enriched.markdown.test.TestAstFactory.document
17+
import com.swmansion.enriched.markdown.test.TestAstFactory.image
18+
import com.swmansion.enriched.markdown.test.TestAstFactory.paragraph
19+
import com.swmansion.enriched.markdown.test.TestAstFactory.text
20+
import com.swmansion.enriched.markdown.utils.text.ImageCache
21+
import org.junit.Assert.assertEquals
22+
import org.junit.Test
23+
import org.junit.runner.RunWith
24+
import org.robolectric.annotation.Config
25+
import org.robolectric.annotation.GraphicsMode
26+
27+
@RunWith(AndroidJUnit4::class)
28+
@Config(sdk = [28])
29+
// Legacy graphics report zero font metrics, so the cap height this checks against would be zero.
30+
@GraphicsMode(GraphicsMode.Mode.NATIVE)
31+
class InlineImagePlacementTest {
32+
private val context: Context = ApplicationProvider.getApplicationContext()
33+
34+
// iOS centers an inline attachment on the font's cap height; Android used to sit the image on
35+
// the baseline, so an icon taller than the capitals stood above the text.
36+
@Test
37+
fun anInlineImageIsCenteredOnTheCapHeight() {
38+
val url = "test://inline-image-placement"
39+
ImageCache.putOriginal(url, Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.RED) })
40+
val style = MarkdownRenderTestSupport.defaultStyle
41+
val rendered = render(document(paragraph(text("Inline "), image(url), text(" image"))), style)
42+
val textView = TextView(context)
43+
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, style.paragraphStyle.fontSize)
44+
textView.text = rendered
45+
textView.registerWithSpans(rendered)
46+
textView.measure(
47+
View.MeasureSpec.makeMeasureSpec(400, View.MeasureSpec.EXACTLY),
48+
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
49+
)
50+
textView.layout(0, 0, 400, textView.measuredHeight)
51+
val layout = requireNotNull(textView.layout)
52+
53+
val bitmap = Bitmap.createBitmap(400, textView.height, Bitmap.Config.ARGB_8888)
54+
layout.draw(Canvas(bitmap))
55+
val redRows =
56+
(0 until bitmap.height).filter { y ->
57+
(0 until bitmap.width).any { x ->
58+
val pixel = bitmap.getPixel(x, y)
59+
Color.alpha(pixel) > 0 && Color.red(pixel) > 128 && Color.green(pixel) < 64
60+
}
61+
}
62+
val imageCenter = (redRows.first() + redRows.last() + 1) / 2f
63+
64+
val capBounds = Rect()
65+
textView.paint.getTextBounds("H", 0, 1, capBounds)
66+
val capCenter = layout.getLineBaseline(0) - capBounds.height() / 2f
67+
assertEquals(capCenter, imageCenter, 1f)
68+
}
69+
}

‎packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.graphics.Canvas
66
import android.graphics.Color
77
import android.graphics.Paint
88
import android.graphics.Path
9+
import android.graphics.Rect
910
import android.graphics.drawable.Drawable
1011
import android.net.Uri
1112
import android.os.Build
@@ -75,6 +76,7 @@ class ImageSpan(
7576
private val requestKey: String = ImageCache.requestKey(requestUrl, requestHeaders)
7677

7778
private var cachedWidth: Int = 0
79+
private val capBounds = Rect()
7880
private var viewRef: WeakReference<TextView>? = null
7981
private var sourceDrawable: Drawable? = null
8082
private var onBoxHeightChanged: (() -> Unit)? = null
@@ -313,8 +315,10 @@ class ImageSpan(
313315
val drawable = getDrawable()
314316
canvas.withSave {
315317
if (isInline) {
318+
// Centered on the cap height of the surrounding text, as iOS places inline attachments.
316319
val imageHeight = drawable.bounds.height()
317-
translate(x, (y - imageHeight + (imageHeight * 0.1f)))
320+
paint.getTextBounds("H", 0, 1, capBounds)
321+
translate(x, y - (capBounds.height() + imageHeight) / 2f)
318322
} else {
319323
translate(x, top.toFloat())
320324
}

0 commit comments

Comments
 (0)