Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Faster boundsInWindow for compose
  • Loading branch information
romtsn committed Oct 9, 2024
commit c546d6ab8cec18007ae1156c6f121556d1070342
8 changes: 8 additions & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ public class io/sentry/android/replay/util/FixedWindowCallback : android/view/Wi
public fun onWindowStartingActionMode (Landroid/view/ActionMode$Callback;I)Landroid/view/ActionMode;
}

public final class io/sentry/android/replay/util/NodesKt {
public static final fun fastCoerceAtLeast (FF)F
public static final fun fastCoerceAtMost (FF)F
public static final fun fastCoerceIn (FFF)F
public static final fun fastMaxOf (FFFF)F
public static final fun fastMinOf (FFFF)F
}

public abstract interface class io/sentry/android/replay/util/TextLayout {
public abstract fun getDominantTextColor ()Ljava/lang/Integer;
public abstract fun getEllipsisCount (I)I
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
package io.sentry.android.replay.util

import android.graphics.Rect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.text.TextLayoutResult
import kotlin.math.roundToInt
Expand Down Expand Up @@ -78,13 +80,6 @@ internal fun Painter.isRedactable(): Boolean {
!className.contains("Brush")
}

/**
* Converts from [androidx.compose.ui.geometry.Rect] to [android.graphics.Rect].
*/
internal fun androidx.compose.ui.geometry.Rect.toRect(): Rect {
return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
}

internal data class TextAttributes(val color: Color?, val hasFillModifier: Boolean)

/**
Expand Down Expand Up @@ -125,3 +120,87 @@ internal fun LayoutNode.findTextAttributes(): TextAttributes {
}
return TextAttributes(color, hasFillModifier)
}

/**
* Returns the smaller of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.minOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private inline fun fastMinOf(a: Float, b: Float, c: Float, d: Float): Float {
return minOf(a, minOf(b, minOf(c, d)))
}

/**
* Returns the largest of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.maxOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private inline fun fastMaxOf(a: Float, b: Float, c: Float, d: Float): Float {
return maxOf(a, maxOf(b, maxOf(c, d)))
}

/**
* Returns this float value clamped in the inclusive range defined by [minimumValue] and
* [maximumValue]. Unlike [Float.coerceIn], the range is not validated: the caller must ensure that
* [minimumValue] is less than [maximumValue].
*/
private inline fun Float.fastCoerceIn(minimumValue: Float, maximumValue: Float) =
this.fastCoerceAtLeast(minimumValue).fastCoerceAtMost(maximumValue)

/** Ensures that this value is not less than the specified [minimumValue]. */
private inline fun Float.fastCoerceAtLeast(minimumValue: Float): Float {
return if (this < minimumValue) minimumValue else this
}

/** Ensures that this value is not greater than the specified [maximumValue]. */
private inline fun Float.fastCoerceAtMost(maximumValue: Float): Float {
return if (this > maximumValue) maximumValue else this
}

/**
* A faster copy of https://github.com/androidx/androidx/blob/fc7df0dd68466ac3bb16b1c79b7a73dd0bfdd4c1/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt#L187
*
* Since we traverse the tree from the root, we don't need to find it again from the leaf node and
* just pass it as an argument.
*
* @return boundaries of this layout relative to the window's origin.
*/
internal fun LayoutCoordinates.boundsInWindow(root: LayoutCoordinates?): Rect {
root ?: return Rect()

val rootWidth = root.size.width.toFloat()
val rootHeight = root.size.height.toFloat()

val bounds = root.localBoundingBoxOf(this)
val boundsLeft = bounds.left.fastCoerceIn(0f, rootWidth)
val boundsTop = bounds.top.fastCoerceIn(0f, rootHeight)
val boundsRight = bounds.right.fastCoerceIn(0f, rootWidth)
val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)

if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
return Rect()
}

val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))

val topLeftX = topLeft.x
val topRightX = topRight.x
val bottomLeftX = bottomLeft.x
val bottomRightX = bottomRight.x

val left = fastMinOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
val right = fastMaxOf(topLeftX, topRightX, bottomLeftX, bottomRightX)

val topLeftY = topLeft.y
val topRightY = topRight.y
val bottomLeftY = bottomLeft.y
val bottomRightY = bottomRight.y

val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)

return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import android.annotation.TargetApi
import android.view.View
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.Owner
Expand All @@ -19,11 +20,11 @@ import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.SentryReplayModifiers
import io.sentry.android.replay.util.ComposeTextLayout
import io.sentry.android.replay.util.boundsInWindow
import io.sentry.android.replay.util.findPainter
import io.sentry.android.replay.util.findTextAttributes
import io.sentry.android.replay.util.isRedactable
import io.sentry.android.replay.util.toOpaque
import io.sentry.android.replay.util.toRect
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
Expand Down Expand Up @@ -62,19 +63,26 @@ internal object ComposeViewHierarchyNode {
return options.experimental.sessionReplay.redactViewClasses.contains(className)
}

private var _rootCoordinates: LayoutCoordinates? = null

private fun fromComposeNode(
node: LayoutNode,
parent: ViewHierarchyNode?,
distance: Int,
isComposeRoot: Boolean,
options: SentryOptions
): ViewHierarchyNode? {
val isInTree = node.isPlaced && node.isAttached
if (!isInTree) {
return null
}

if (isComposeRoot) {
_rootCoordinates = node.coordinates.findRootCoordinates()
}

val semantics = node.collapsedSemantics
val visibleRect = node.coordinates.boundsInWindow().toRect()
val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates)
val isVisible = !node.outerCoordinator.isTransparent() &&
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
visibleRect.height() > 0 && visibleRect.width() > 0
Expand Down Expand Up @@ -167,7 +175,7 @@ internal object ComposeViewHierarchyNode {

try {
val rootNode = (view as? Owner)?.root ?: return false
rootNode.traverse(parent, options)
rootNode.traverse(parent, isComposeRoot = true, options)
} catch (e: Throwable) {
options.logger.log(
SentryLevel.ERROR,
Expand All @@ -185,7 +193,7 @@ internal object ComposeViewHierarchyNode {
return true
}

private fun LayoutNode.traverse(parentNode: ViewHierarchyNode, options: SentryOptions) {
private fun LayoutNode.traverse(parentNode: ViewHierarchyNode, isComposeRoot: Boolean, options: SentryOptions) {
val children = this.children
if (children.isEmpty()) {
return
Expand All @@ -194,10 +202,10 @@ internal object ComposeViewHierarchyNode {
val childNodes = ArrayList<ViewHierarchyNode>(children.size)
for (index in children.indices) {
val child = children[index]
val childNode = fromComposeNode(child, parentNode, index, options)
val childNode = fromComposeNode(child, parentNode, index, isComposeRoot, options)
if (childNode != null) {
childNodes.add(childNode)
child.traverse(childNode, options)
child.traverse(childNode, isComposeRoot = false, options)
}
}
parentNode.children = childNodes
Expand Down