Skip to content

Commit 731ae5a

Browse files
authored
[SR] Detect dominant color for TextViews with Spans (#3682)
1 parent 8586d1f commit 731ae5a

File tree

4 files changed

+143
-3
lines changed

4 files changed

+143
-3
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- Avoid stopping appStartProfiler after application creation ([#3630](https://github.com/getsentry/sentry-java/pull/3630))
8+
- Session Replay: Correctly detect dominant color for `TextView`s with Spans ([#3682](https://github.com/getsentry/sentry-java/pull/3682))
89

910
*Breaking changes*:
1011

sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.sentry.SentryLevel.WARNING
2424
import io.sentry.SentryOptions
2525
import io.sentry.SentryReplayOptions
2626
import io.sentry.android.replay.util.MainLooperHandler
27+
import io.sentry.android.replay.util.dominantTextColor
2728
import io.sentry.android.replay.util.getVisibleRects
2829
import io.sentry.android.replay.util.gracefullyShutdown
2930
import io.sentry.android.replay.util.submitSafely
@@ -142,13 +143,14 @@ internal class ScreenshotRecorder(
142143
}
143144

144145
is TextViewHierarchyNode -> {
145-
// TODO: find a way to get the correct text color for RN
146-
// TODO: now it always returns black
146+
val textColor = node.layout.dominantTextColor
147+
?: node.dominantColor
148+
?: Color.BLACK
147149
node.layout.getVisibleRects(
148150
node.visibleRect,
149151
node.paddingLeft,
150152
node.paddingTop
151-
) to (node.dominantColor ?: Color.BLACK)
153+
) to textColor
152154
}
153155

154156
else -> {

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt

+33
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import android.graphics.drawable.VectorDrawable
1313
import android.os.Build.VERSION
1414
import android.os.Build.VERSION_CODES
1515
import android.text.Layout
16+
import android.text.Spanned
17+
import android.text.style.ForegroundColorSpan
1618
import android.view.View
1719
import android.widget.TextView
1820
import java.lang.NullPointerException
@@ -101,3 +103,34 @@ internal val TextView.totalPaddingTopSafe: Int
101103
} catch (e: NullPointerException) {
102104
extendedPaddingTop
103105
}
106+
107+
/**
108+
* Returns the dominant text color of the layout by looking at the [ForegroundColorSpan] spans if
109+
* this text is a [Spanned] text. If the text is not a [Spanned] text or there are no spans, it
110+
* returns null.
111+
*/
112+
internal val Layout?.dominantTextColor: Int? get() {
113+
this ?: return null
114+
115+
if (text !is Spanned) return null
116+
117+
val spans = (text as Spanned).getSpans(0, text.length, ForegroundColorSpan::class.java)
118+
119+
// determine the dominant color by the span with the longest range
120+
var longestSpan = Int.MIN_VALUE
121+
var dominantColor: Int? = null
122+
for (span in spans) {
123+
val spanStart = (text as Spanned).getSpanStart(span)
124+
val spanEnd = (text as Spanned).getSpanEnd(span)
125+
if (spanStart == -1 || spanEnd == -1) {
126+
// the span is not attached
127+
continue
128+
}
129+
val spanLength = spanEnd - spanStart
130+
if (spanLength > longestSpan) {
131+
longestSpan = spanLength
132+
dominantColor = span.foregroundColor
133+
}
134+
}
135+
return dominantColor
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package io.sentry.android.replay.util
2+
3+
import android.app.Activity
4+
import android.graphics.Color
5+
import android.os.Bundle
6+
import android.os.Looper
7+
import android.text.SpannableString
8+
import android.text.Spanned
9+
import android.text.style.ForegroundColorSpan
10+
import android.widget.LinearLayout
11+
import android.widget.LinearLayout.LayoutParams
12+
import android.widget.TextView
13+
import androidx.test.ext.junit.runners.AndroidJUnit4
14+
import io.sentry.SentryOptions
15+
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
16+
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
17+
import org.junit.runner.RunWith
18+
import org.robolectric.Robolectric.buildActivity
19+
import org.robolectric.Shadows.shadowOf
20+
import org.robolectric.annotation.Config
21+
import kotlin.test.Test
22+
import kotlin.test.assertEquals
23+
import kotlin.test.assertNull
24+
import kotlin.test.assertTrue
25+
26+
@RunWith(AndroidJUnit4::class)
27+
@Config(sdk = [30])
28+
class TextViewDominantColorTest {
29+
30+
@Test
31+
fun `when no spans, returns currentTextColor`() {
32+
val controller = buildActivity(TextViewActivity::class.java, null).setup()
33+
controller.create().start().resume()
34+
35+
TextViewActivity.textView?.setTextColor(Color.WHITE)
36+
37+
val node = ViewHierarchyNode.fromView(TextViewActivity.textView!!, null, 0, SentryOptions())
38+
assertTrue(node is TextViewHierarchyNode)
39+
assertNull(node.layout.dominantTextColor)
40+
}
41+
42+
@Test
43+
fun `when has a foreground color span, returns its color`() {
44+
val controller = buildActivity(TextViewActivity::class.java, null).setup()
45+
controller.create().start().resume()
46+
47+
val text = "Hello, World!"
48+
TextViewActivity.textView?.text = SpannableString(text).apply {
49+
setSpan(ForegroundColorSpan(Color.RED), 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
50+
}
51+
TextViewActivity.textView?.setTextColor(Color.WHITE)
52+
TextViewActivity.textView?.requestLayout()
53+
54+
shadowOf(Looper.getMainLooper()).idle()
55+
56+
val node = ViewHierarchyNode.fromView(TextViewActivity.textView!!, null, 0, SentryOptions())
57+
assertTrue(node is TextViewHierarchyNode)
58+
assertEquals(Color.RED, node.layout.dominantTextColor)
59+
}
60+
61+
@Test
62+
fun `when has multiple foreground color spans, returns color of the longest span`() {
63+
val controller = buildActivity(TextViewActivity::class.java, null).setup()
64+
controller.create().start().resume()
65+
66+
val text = "Hello, World!"
67+
TextViewActivity.textView?.text = SpannableString(text).apply {
68+
setSpan(ForegroundColorSpan(Color.RED), 0, 5, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
69+
setSpan(ForegroundColorSpan(Color.BLACK), 6, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
70+
}
71+
TextViewActivity.textView?.setTextColor(Color.WHITE)
72+
TextViewActivity.textView?.requestLayout()
73+
74+
shadowOf(Looper.getMainLooper()).idle()
75+
76+
val node = ViewHierarchyNode.fromView(TextViewActivity.textView!!, null, 0, SentryOptions())
77+
assertTrue(node is TextViewHierarchyNode)
78+
assertEquals(Color.BLACK, node.layout.dominantTextColor)
79+
}
80+
}
81+
82+
private class TextViewActivity : Activity() {
83+
84+
companion object {
85+
var textView: TextView? = null
86+
}
87+
88+
override fun onCreate(savedInstanceState: Bundle?) {
89+
super.onCreate(savedInstanceState)
90+
val linearLayout = LinearLayout(this).apply {
91+
setBackgroundColor(android.R.color.white)
92+
orientation = LinearLayout.VERTICAL
93+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
94+
}
95+
96+
textView = TextView(this).apply {
97+
text = "Hello, World!"
98+
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
99+
}
100+
linearLayout.addView(textView)
101+
102+
setContentView(linearLayout)
103+
}
104+
}

0 commit comments

Comments
 (0)