onVisibilityChanged Modifier

Ever tried logging impressions of components or screens in your app when they’re displayed to a user? If you’ve been down that rabbit hole, you know it’s like trying to catch the wind. Given how common this functionality it, having an out-of-the-box solution would’ve been a huge time-saver.

Enter Rebecca Franks, a Dev Rel Engineer at Google, who’s swooping in to save the day with a proposal for a new “Visibility” Modifier. This nifty Modifier aims to track visibility changes of a Composable easily. It’s still a proposal because, like any good superhero, it has its kryptonite: performance implications when used in lists. But hey, it’s currently the best way to track visibility, so I encourage you to check it out. And if you’ve got opinions, suggestions, or just want to say hi, give Rebecca your feedback directly. Let’s help shape this API

/**
* `onVisibleChange` fires events when the visibility of an item changes.
*
* The event includes information such as the bounds of the visible item, and the fraction of the
* item that's visible. It'll only fire once for when the item enters the screen, and on exit, using
* the bounds within the current window to determine if an item is visible or not.
*
* _Limitations_:
* - This modifier has no way to detect if there is something on top of it - obscuring (ie z-index checking).
* - Keyboard visibility, if an item is below the keyboard, this modifier will still track
* the item as visible, it is recommended that you ensure you are using
* android:windowSoftInputMode="adjustResize" to ensure the whole window is resized and then
* items will not report themselves as being visible.
*
* See VisibilityTrackingSample.kt for example usage.
*
* @param visibleEvent function that will run when the item becomes visible or invisible.
*/
fun Modifier.onVisibilityChanged(visibleEvent: (VisibleEvent) -> Unit) =
this.then(VisibilityAwareModifierElement(visibleEvent))
/**
* `onVisiblePositionChanged` fires events when the visibility of an item changes. It include the same information as `onVisibleChange`, with additional information such as
* a trigger on each position change.
*
* The event includes information such as the bounds of the visible item, and the fraction of the
* item that's visible. It'll trigger when the item enters the screen, and on exit, using
* the bounds within the current window to determine if an item is visible or not, as well as for each position change of the item on screen.
*
* WARNING: This function is invoked often if items are changing frequently (e.g. in a scrolling list), it should be use sparingly. Prefer `onVisibleChange` over this function.
*
* _Limitations_:
* - This modifier has no way to detect if there is something on top of it - obscuring (ie z-index checking).
* - Keyboard visibility, if an item is below the keyboard, this modifier will still track
* the item as visible, it is recommended that you ensure you are using
* android:windowSoftInputMode="adjustResize" to ensure the whole window is resized and then
* items will not report themselves as being visible.
*
* See VisibilityTrackingSample.kt for example usage.
*
* @param visibleEvent function that will run when the item becomes visible or invisible.
*/
fun Modifier.onVisiblePositionChanged(visibleEvent: (VisiblePositionChangedEvent) -> Unit) =
this.then(VisiblePositionChangedModifierElement(visibleEvent))
sealed class VisibleEvent {
data class Visible(
val visibleRect: Rect,
val size: IntSize,
val fractionVisibleWidth: Float,
val fractionVisibleHeight: Float
) : VisibleEvent()
data object Invisible : VisibleEvent()
}
sealed class VisiblePositionChangedEvent {
data class Visible(
val visibleRect: Rect,
val size: IntSize,
val fractionVisibleWidth: Float,
val fractionVisibleHeight: Float
) : VisiblePositionChangedEvent()
data object Invisible : VisiblePositionChangedEvent()
data class OnPositionChanged(
val visibleRect: Rect,
val size: IntSize,
val fractionVisibleWidth: Float,
val fractionVisibleHeight: Float
): VisiblePositionChangedEvent()
}
private class VisiblePositionChangedModifierNode(var visibleEventCallback: (VisiblePositionChangedEvent) -> Unit) : GlobalPositionAwareModifierNode, Modifier.Node() {
private var currentlyVisible = false
private var visibleBounds = Rect.Zero
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
val bounds = coordinates.boundsInWindow()
val visible = isAttached && bounds.width > 0 && bounds.height > 0
if (currentlyVisible != visible || bounds != visibleBounds) {
val size = coordinates.size
val fractionVisibleWidth = bounds.width / size.width.toFloat()
val fractionVisibleHeight = bounds.height / size.height.toFloat()
if (currentlyVisible == visible) {
visibleEventCallback(VisiblePositionChangedEvent.OnPositionChanged(bounds,
size,
fractionVisibleWidth,
fractionVisibleHeight))
} else {
if (visible) {
visibleEventCallback(
VisiblePositionChangedEvent.Visible(
bounds,
size,
fractionVisibleWidth,
fractionVisibleHeight
)
)
} else {
visibleEventCallback(VisiblePositionChangedEvent.Invisible)
}
}
currentlyVisible = visible
visibleBounds = bounds
}
}
override fun onDetach() {
super.onDetach()
visibleEventCallback(VisiblePositionChangedEvent.Invisible)
currentlyVisible = false
}
}
private data class VisiblePositionChangedModifierElement(val visibleEventCallback: (VisiblePositionChangedEvent) -> Unit) :
ModifierNodeElement<VisiblePositionChangedModifierNode>() {
override fun create() = VisiblePositionChangedModifierNode(visibleEventCallback)
override fun update(node: VisiblePositionChangedModifierNode) {
node.visibleEventCallback = visibleEventCallback
}
override fun InspectorInfo.inspectableProperties() {
name = "visibility aware"
properties["visibleEventCallback"] = visibleEventCallback
}
}
private class VisibilityAwareModifierNode(var visibleEventCallback: (VisibleEvent) -> Unit) :
GlobalPositionAwareModifierNode, Modifier.Node() {
private var currentlyVisible = false
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
val bounds = coordinates.boundsInWindow()
val visible = isAttached && bounds.width > 0 && bounds.height > 0
if (currentlyVisible != visible) {
val size = coordinates.size
val fractionVisibleWidth = bounds.width / size.width.toFloat()
val fractionVisibleHeight = bounds.height / size.height.toFloat()
if (visible) {
visibleEventCallback(
VisibleEvent.Visible(
bounds,
size,
fractionVisibleWidth,
fractionVisibleHeight
)
)
} else {
visibleEventCallback(VisibleEvent.Invisible)
}
currentlyVisible = visible
}
}
override fun onDetach() {
super.onDetach()
visibleEventCallback(VisibleEvent.Invisible)
currentlyVisible = false
}
}
private data class VisibilityAwareModifierElement(val visibleEventCallback: (VisibleEvent) -> Unit) :
ModifierNodeElement<VisibilityAwareModifierNode>() {
override fun create() = VisibilityAwareModifierNode(visibleEventCallback)
override fun update(node: VisibilityAwareModifierNode) {
node.visibleEventCallback = visibleEventCallback
}
override fun InspectorInfo.inspectableProperties() {
name = "visibility aware"
properties["visibleEventCallback"] = visibleEventCallback
}
}
view raw Visibility.kt hosted with ❤ by GitHub

Have a project you'd like to submit? Fill this form, will ya!

If you like this snippet, you might also like:

Maker OS is an all-in-one productivity system for developers

I built Maker OS to track, manage & organize my life. Now you can do it too!