Merge "Fix documentation issues in wear-watchface" into androidx-main
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index aaf6abe..44e9b90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -374,7 +374,8 @@
) : DragGestureNode(
canDrag = AlwaysDrag,
enabled = enabled,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
+ orientationLock = orientation
) {
open suspend fun AnchoredDragScope.anchoredDrag(
@@ -389,9 +390,6 @@
state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
}
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = orientation.toPointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -429,7 +427,8 @@
update(
enabled = enabled,
interactionSource = interactionSource,
- isResetPointerInputHandling = resetPointerInputHandling,
+ shouldResetPointerInputHandling = resetPointerInputHandling,
+ orientationLock = orientation
)
}
@@ -742,7 +741,8 @@
@Deprecated(
message = "Use the progress function to query the progress between two specified " +
"anchors.",
- replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
+ replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)")
+ )
@get:FloatRange(from = 0.0, to = 1.0)
val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
val a = anchors.positionOf(settledValue)
@@ -1296,7 +1296,7 @@
}
}
-private fun<K> ObjectFloatMap<K>.minValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.minValueOrNaN(): Float {
if (size == 1) return Float.NaN
var minValue = Float.POSITIVE_INFINITY
forEachValue { value ->
@@ -1307,7 +1307,7 @@
return minValue
}
-private fun<K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
if (size == 1) return Float.NaN
var maxValue = Float.NEGATIVE_INFINITY
forEachValue { value ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4d7c7fc..86835e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -45,7 +45,7 @@
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
-import kotlin.math.abs
+import kotlin.math.absoluteValue
import kotlin.math.sign
import kotlinx.coroutines.CancellationException
@@ -79,7 +79,7 @@
pointerId,
PointerType.Touch,
onPointerSlopReached = onTouchSlopReached,
- pointerDirectionConfig = BidirectionalPointerDirectionConfig,
+ orientation = null,
)
}
@@ -170,33 +170,113 @@
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
+) = detectDragGestures(
+ onDragStart = { _, offset -> onDragStart(offset) },
+ onDragEnd = { onDragEnd.invoke() },
+ onDragCancel = onDragCancel,
+ shouldAwaitTouchSlop = { true },
+ orientationLock = null,
+ onDrag = onDrag
+)
+
+/**
+ * A Gesture detector that waits for pointer down and touch slop in the direction specified by
+ * [orientationLock] and then calls [onDrag] for each drag event.
+ * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the
+ * position change automatically once the touch slop has been crossed, the amount of drag over
+ * the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
+ * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
+ * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
+ * an [Offset.Zero].
+ *
+ * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
+ * the last known pointer position relative to the containing element as well as the initial
+ * down event that triggered this gesture detection cycle. The [Offset] can be outside
+ * the actual bounds of the element itself meaning the numbers can be negative or larger than the
+ * element bounds if the touch target is smaller than the
+ * [ViewConfiguration.minimumTouchTargetSize].
+ *
+ * [onDragEnd] is called after all pointers are up with the event change of the up event
+ * and [onDragCancel] is called if another gesture has consumed pointer input,
+ * canceling this gesture.
+ *
+ * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
+ * about the triggering [PointerInputChange] and post slop delta.
+ * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
+ * up [PointerInputChange] that finished the gesture.
+ * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
+ * when it was consumed.
+ * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
+ * @param orientationLock Optionally locks detection to this orientation, this means, when this is
+ * provided, touch slop detection and drag event detection will be conditioned to the given
+ * orientation axis. [onDrag] will still dispatch events on with information in both axis, but
+ * if orientation lock is provided, only events that happen on the given orientation will be
+ * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on
+ * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction
+ * and drag events will be dispatched if present in either direction.
+ * @param onDrag A lambda to be called for each delta event in the gesture. It contains information
+ * about the [PointerInputChange] and the movement offset.
+ *
+ * Example Usage:
+ * @sample androidx.compose.foundation.samples.DetectDragGesturesSample
+ *
+ * @see detectVerticalDragGestures
+ * @see detectHorizontalDragGestures
+ * @see detectDragGesturesAfterLongPress to detect gestures after long press
+ */
+internal suspend fun PointerInputScope.detectDragGestures(
+ onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+ onDragEnd: (change: PointerInputChange) -> Unit,
+ onDragCancel: () -> Unit,
+ shouldAwaitTouchSlop: () -> Boolean,
+ orientationLock: Orientation?,
+ onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
awaitEachGesture {
+ val initialDown =
+ awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+ val awaitTouchSlop = shouldAwaitTouchSlop()
+
+ if (!awaitTouchSlop) {
+ initialDown.consume()
+ }
val down = awaitFirstDown(requireUnconsumed = false)
var drag: PointerInputChange?
var overSlop = Offset.Zero
- do {
- drag = awaitPointerSlopOrCancellation(
- down.id,
- down.type,
- pointerDirectionConfig = BidirectionalPointerDirectionConfig
- ) { change, over ->
- change.consume()
- overSlop = over
- }
- } while (drag != null && !drag.isConsumed)
- if (drag != null) {
- onDragStart.invoke(drag.position)
- onDrag(drag, overSlop)
- if (
- !drag(drag.id) {
- onDrag(it, it.positionChange())
- it.consume()
+ var initialDelta = Offset.Zero
+
+ if (awaitTouchSlop) {
+ do {
+ drag = awaitPointerSlopOrCancellation(
+ down.id,
+ down.type,
+ orientation = orientationLock
+ ) { change, over ->
+ change.consume()
+ overSlop = over
}
- ) {
+ } while (drag != null && !drag.isConsumed)
+ initialDelta = drag?.position ?: Offset.Zero
+ } else {
+ drag = initialDown
+ }
+
+ if (drag != null) {
+ onDragStart.invoke(initialDown, initialDelta)
+ onDrag(drag, overSlop)
+ val upEvent = drag(
+ pointerId = drag.id,
+ onDrag = {
+ onDrag(it, it.positionChange())
+ },
+ orientation = orientationLock,
+ motionConsumed = {
+ it.isConsumed
+ })
+ if (upEvent == null) {
onDragCancel()
} else {
- onDragEnd()
+ onDragEnd(upEvent)
}
}
}
@@ -288,7 +368,7 @@
pointerId = pointerId,
pointerType = PointerType.Touch,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
- pointerDirectionConfig = VerticalPointerDirectionConfig
+ orientation = Orientation.Vertical
)
internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -299,7 +379,7 @@
pointerId = pointerId,
pointerType = pointerType,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
- pointerDirectionConfig = VerticalPointerDirectionConfig
+ orientation = Orientation.Vertical
)
/**
@@ -324,7 +404,7 @@
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
- hasDragged = { it.positionChangeIgnoreConsumed().y != 0f },
+ orientation = Orientation.Vertical,
motionConsumed = { it.isConsumed }
) != null
@@ -439,7 +519,7 @@
pointerId = pointerId,
pointerType = PointerType.Touch,
onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.x) },
- pointerDirectionConfig = HorizontalPointerDirectionConfig
+ orientation = Orientation.Horizontal
)
internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -450,7 +530,7 @@
pointerId = pointerId,
pointerType = pointerType,
onPointerSlopReached = { change, overSlop -> onPointerSlopReached(change, overSlop.x) },
- pointerDirectionConfig = HorizontalPointerDirectionConfig
+ orientation = Orientation.Horizontal
)
/**
@@ -472,7 +552,7 @@
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
- hasDragged = { it.positionChangeIgnoreConsumed().x != 0f },
+ orientation = Orientation.Horizontal,
motionConsumed = { it.isConsumed }
) != null
@@ -563,9 +643,12 @@
/**
* Continues to read drag events until all pointers are up or the drag event is canceled.
- * The initial pointer to use for driving the drag is [pointerId]. [hasDragged]
- * passes the result whether a change was detected from the drag function or not. [onDrag] is called
- * whenever the pointer moves and [hasDragged] returns non-zero.
+ * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called
+ * whenever the pointer moves. The up event is returned at the end of the drag gesture.
+ *
+ * @param pointerId The pointer where that is driving the gesture.
+ * @param onDrag Callback for every new drag event.
+ * @param motionConsumed If the PointerInputChange should be considered as consumed.
*
* @return The last pointer input event change when gesture ended with all pointers up
* and null when the gesture was canceled.
@@ -573,7 +656,7 @@
internal suspend inline fun AwaitPointerEventScope.drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit,
- hasDragged: (PointerInputChange) -> Boolean,
+ orientation: Orientation?,
motionConsumed: (PointerInputChange) -> Boolean
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
@@ -581,7 +664,15 @@
}
var pointer = pointerId
while (true) {
- val change = awaitDragOrUp(pointer, hasDragged) ?: return null
+ val change = awaitDragOrUp(pointer) {
+ val positionChange = it.positionChangeIgnoreConsumed()
+ val motionChange = if (orientation == null) {
+ positionChange.getDistance()
+ } else {
+ if (orientation == Orientation.Vertical) positionChange.y else positionChange.x
+ }
+ motionChange != 0.0f
+ } ?: return null
if (motionConsumed(change)) {
return null
@@ -629,16 +720,14 @@
}
/**
- * Waits for drag motion along one axis when [pointerDirectionConfig] is
- * [HorizontalPointerDirectionConfig] or [VerticalPointerDirectionConfig], and drag motion along
- * any axis when using [BidirectionalPointerDirectionConfig]. It passes [pointerId] as the pointer
- * to examine. If [pointerId] is raised, another pointer from those that are down will be chosen to
+ * Waits for drag motion and uses [orientation] to detect the direction of touch slop detection.
+ * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
+ * those that are down will be chosen to
* lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when
* [awaitPointerSlopOrCancellation] is called, then `null` is returned.
*
* When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [PointerDirectionConfig.calculateDeltaChange] should return the position
- * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * beyond the pointer slop. If [onPointerSlopReached] does not consume the
* position change, pointer slop will not have been considered detected and the detection will
* continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
*
@@ -650,10 +739,10 @@
* `null` if all pointers are raised or the position change was consumed by another gesture
* detector.
*/
-internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
pointerId: PointerId,
pointerType: PointerType,
- pointerDirectionConfig: PointerDirectionConfig,
+ orientation: Orientation?,
onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
): PointerInputChange? {
if (currentEvent.isPointerUp(pointerId)) {
@@ -661,8 +750,7 @@
}
val touchSlop = viewConfiguration.pointerSlop(pointerType)
var pointer: PointerId = pointerId
- var totalPositionChange = Offset.Zero
-
+ val touchSlopDetector = TouchSlopDetector(orientation)
while (true) {
val event = awaitPointerEvent()
val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
@@ -677,29 +765,8 @@
pointer = otherDown.id
}
} else {
- val currentPosition = dragEvent.position
- val previousPosition = dragEvent.previousPosition
-
- val positionChange = currentPosition - previousPosition
-
- totalPositionChange += positionChange
-
- val inDirection = pointerDirectionConfig.calculateDeltaChange(
- totalPositionChange
- )
-
- if (inDirection < touchSlop) {
- // verify that nothing else consumed the drag event
- awaitPointerEvent(PointerEventPass.Final)
- if (dragEvent.isConsumed) {
- return null
- }
- } else {
- val postSlopOffset = pointerDirectionConfig.calculatePostSlopOffset(
- totalPositionChange,
- touchSlop
- )
-
+ val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop)
+ if (postSlopOffset != null) {
onPointerSlopReached(
dragEvent,
postSlopOffset
@@ -707,7 +774,13 @@
if (dragEvent.isConsumed) {
return dragEvent
} else {
- totalPositionChange = Offset.Zero
+ touchSlopDetector.reset()
+ }
+ } else {
+ // verify that nothing else consumed the drag event
+ awaitPointerEvent(PointerEventPass.Final)
+ if (dragEvent.isConsumed) {
+ return null
}
}
}
@@ -715,70 +788,77 @@
}
/**
- * Configures the calculations to get the change amount depending on the dragging type.
- * [calculatePostSlopOffset] will return the post offset slop when the touchSlop is reached.
+ * Detects if touch slop has been crossed after adding a series of [PointerInputChange].
+ * For every new [PointerInputChange] one should add it to this detector using
+ * [addPointerInputChange]. If the position change causes the touch slop to be crossed,
+ * [addPointerInputChange] will return true.
*/
-internal interface PointerDirectionConfig {
- fun calculateDeltaChange(offset: Offset): Float
- fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset
-}
+private class TouchSlopDetector(val orientation: Orientation? = null) {
-/**
- * Used for monitoring changes on X axis.
- */
-internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = abs(offset.x)
+ fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y
+ fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
+ /**
+ * The accumulation of drag deltas in this detector.
+ */
+ private var totalPositionChange: Offset = Offset.Zero
+
+ /**
+ * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch
+ * slop provided by [touchSlop], this method will return the post slop offset, that is the
+ * total accumulated delta change minus the touch slop value, otherwise this should return null.
+ */
+ fun addPointerInputChange(
+ dragEvent: PointerInputChange,
touchSlop: Float
- ): Offset {
- val finalMainPositionChange = totalPositionChange.x -
- (sign(totalPositionChange.x) * touchSlop)
- return Offset(finalMainPositionChange, totalPositionChange.y)
+ ): Offset? {
+ val currentPosition = dragEvent.position
+ val previousPosition = dragEvent.previousPosition
+ val positionChange = currentPosition - previousPosition
+ totalPositionChange += positionChange
+
+ val inDirection = if (orientation == null) {
+ totalPositionChange.getDistance()
+ } else {
+ totalPositionChange.mainAxis().absoluteValue
+ }
+
+ val hasCrossedSlop = inDirection >= touchSlop
+
+ return if (hasCrossedSlop) {
+ calculatePostSlopOffset(touchSlop)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Resets the accumulator associated with this detector.
+ */
+ fun reset() {
+ totalPositionChange = Offset.Zero
+ }
+
+ private fun calculatePostSlopOffset(touchSlop: Float): Offset {
+ return if (orientation == null) {
+ val touchSlopOffset =
+ totalPositionChange / totalPositionChange.getDistance() * touchSlop
+ // update postSlopOffset
+ totalPositionChange - touchSlopOffset
+ } else {
+ val finalMainAxisChange = totalPositionChange.mainAxis() -
+ (sign(totalPositionChange.mainAxis()) * touchSlop)
+ val finalCrossAxisChange = totalPositionChange.crossAxis()
+ if (orientation == Orientation.Horizontal) {
+ Offset(finalMainAxisChange, finalCrossAxisChange)
+ } else {
+ Offset(finalCrossAxisChange, finalMainAxisChange)
+ }
+ }
}
}
/**
- * Used for monitoring changes on Y axis.
- */
-internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = abs(offset.y)
-
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset {
- val finalMainPositionChange = totalPositionChange.y -
- (sign(totalPositionChange.y) * touchSlop)
- return Offset(totalPositionChange.x, finalMainPositionChange)
- }
-}
-
-/**
- * Used for monitoring changes on both X and Y axes.
- */
-internal val BidirectionalPointerDirectionConfig = object : PointerDirectionConfig {
- override fun calculateDeltaChange(offset: Offset): Float = offset.getDistance()
-
- override fun calculatePostSlopOffset(
- totalPositionChange: Offset,
- touchSlop: Float
- ): Offset {
- val touchSlopOffset =
- totalPositionChange / calculateDeltaChange(totalPositionChange) * touchSlop
- return totalPositionChange - touchSlopOffset
- }
-}
-
-internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
- if (this == Orientation.Vertical) VerticalPointerDirectionConfig
- else HorizontalPointerDirectionConfig
-
-/**
* Waits for a long press by examining [pointerId].
*
* If that [pointerId] is raised (that is, the user lifts their finger), but another
@@ -839,7 +919,7 @@
// should technically never happen as we checked it above
finished = true
}
- // Pointer (id) stayed down.
+ // Pointer (id) stayed down.
} else {
longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 5db29b1..92f3e6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -31,16 +31,11 @@
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -56,7 +51,6 @@
import kotlin.math.sign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -301,9 +295,10 @@
private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
private var reverseDirection: Boolean
) : DragGestureNode(
- canDrag,
- enabled,
- interactionSource
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = orientation
) {
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
@@ -314,8 +309,6 @@
}
}
- override val pointerDirectionConfig = orientation.toPointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
this@DraggableNode.onDragStarted(this, startedPosition)
@@ -357,6 +350,7 @@
canDrag,
enabled,
interactionSource,
+ orientation,
resetPointerInputHandling
)
}
@@ -372,6 +366,7 @@
canDrag: (PointerInputChange) -> Boolean,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
+ private var orientationLock: Orientation?
) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
protected var canDrag = canDrag
@@ -397,13 +392,6 @@
abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
/**
- * Returns the pointerDirectionConfig which specifies the main and cross axis deltas. This is
- * important when observing the delta change for Draggable, as we want to observe the change
- * in the main axis only.
- */
- abstract val pointerDirectionConfig: PointerDirectionConfig
-
- /**
* Passes the action needed when a drag starts. This gives the ability to pass the desired
* behavior from other nodes implementing AbstractDraggableNode
*/
@@ -478,62 +466,63 @@
// re-create tracker when pointer input block restarts. This lazily creates the tracker
// only when it is need.
val velocityTracker = VelocityTracker()
+ val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
+ { startEvent, initialDelta ->
+ if (canDrag.invoke(startEvent)) {
+ if (!isListeningForEvents) {
+ if (channel == null) {
+ channel = Channel(capacity = Channel.UNLIMITED)
+ }
+ startListeningForEvents()
+ }
+ val overSlopOffset = initialDelta
+ val xSign = sign(startEvent.position.x)
+ val ySign = sign(startEvent.position.y)
+ val adjustedStart = startEvent.position -
+ Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
+
+ channel?.trySend(DragStarted(adjustedStart))
+ }
+ }
+
+ val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
+ velocityTracker.addPointerInputChange(upEvent)
+ val maximumVelocity = currentValueOf(LocalViewConfiguration)
+ .maximumFlingVelocity
+ val velocity = velocityTracker.calculateVelocity(
+ Velocity(maximumVelocity, maximumVelocity)
+ )
+ velocityTracker.resetTracking()
+ channel?.trySend(DragStopped(velocity))
+ }
+
+ val onDragCancel: () -> Unit = {
+ channel?.trySend(DragCancelled)
+ }
+
+ val shouldAwaitTouchSlop: () -> Boolean = {
+ !startDragImmediately()
+ }
+
+ val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
+ { change, delta ->
+ velocityTracker.addPointerInputChange(change)
+ channel?.trySend(DragDelta(delta))
+ }
+
coroutineScope {
try {
- awaitPointerEventScope {
- while (isActive) {
- awaitDownAndSlop(
- _canDrag,
- ::startDragImmediately,
- velocityTracker,
- pointerDirectionConfig
- )?.let {
- /**
- * The gesture crossed the touch slop, events are now relevant
- * and should be propagated
- */
- if (!isListeningForEvents) {
- if (channel == null) {
- channel = Channel(capacity = Channel.UNLIMITED)
- }
- startListeningForEvents()
- }
- var isDragSuccessful = false
- try {
- isDragSuccessful = awaitDrag(
- it.first,
- it.second,
- velocityTracker,
- channel
- ) { event ->
- pointerDirectionConfig.calculateDeltaChange(
- event.positionChangeIgnoreConsumed()
- ) != 0f
- }
- } catch (cancellation: CancellationException) {
- isDragSuccessful = false
- if (!isActive) throw cancellation
- } finally {
- val maximumVelocity = currentValueOf(LocalViewConfiguration)
- .maximumFlingVelocity
- val event = if (isDragSuccessful) {
- val velocity = velocityTracker.calculateVelocity(
- Velocity(maximumVelocity, maximumVelocity)
- )
- velocityTracker.resetTracking()
- DragStopped(velocity)
- } else {
- DragCancelled
- }
- channel?.trySend(event)
- }
- }
- }
- }
- } catch (exception: CancellationException) {
- if (!isActive) {
- throw exception
- }
+ detectDragGestures(
+ orientationLock = orientationLock,
+ onDragStart = onDragStart,
+ onDragEnd = onDragEnd,
+ onDragCancel = onDragCancel,
+ shouldAwaitTouchSlop = shouldAwaitTouchSlop,
+ onDrag = onDrag
+ )
+ } catch (cancellation: CancellationException) {
+ channel?.trySend(DragCancelled)
+ if (!isActive) throw cancellation
}
}
}
@@ -580,9 +569,10 @@
canDrag: (PointerInputChange) -> Boolean = this.canDrag,
enabled: Boolean = this.enabled,
interactionSource: MutableInteractionSource? = this.interactionSource,
- isResetPointerInputHandling: Boolean = false
+ orientationLock: Orientation? = this.orientationLock,
+ shouldResetPointerInputHandling: Boolean = false
) {
- var resetPointerInputHandling = isResetPointerInputHandling
+ var resetPointerInputHandling = shouldResetPointerInputHandling
this.canDrag = canDrag
if (this.enabled != enabled) {
@@ -599,91 +589,17 @@
this.interactionSource = interactionSource
}
+ if (this.orientationLock != orientationLock) {
+ this.orientationLock = orientationLock
+ resetPointerInputHandling = true
+ }
+
if (resetPointerInputHandling) {
pointerInputNode?.resetPointerInputHandler()
}
}
}
-private suspend fun AwaitPointerEventScope.awaitDownAndSlop(
- canDrag: (PointerInputChange) -> Boolean,
- startDragImmediately: () -> Boolean,
- velocityTracker: VelocityTracker,
- pointerDirectionConfig: PointerDirectionConfig
-): Pair<PointerInputChange, Offset>? {
- val initialDown =
- awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- return if (!canDrag(initialDown)) {
- null
- } else if (startDragImmediately()) {
- initialDown.consume()
- velocityTracker.addPointerInputChange(initialDown)
- // since we start immediately we don't wait for slop and the initial delta is 0
- initialDown to Offset.Zero
- } else {
- val down = awaitFirstDown(requireUnconsumed = false)
- velocityTracker.addPointerInputChange(down)
- var initialDelta = Offset.Zero
- val postPointerSlop = { event: PointerInputChange, offset: Offset ->
- velocityTracker.addPointerInputChange(event)
- event.consume()
- initialDelta = offset
- }
-
- val afterSlopResult = awaitPointerSlopOrCancellation(
- down.id,
- down.type,
- pointerDirectionConfig = pointerDirectionConfig,
- onPointerSlopReached = postPointerSlop
- )
-
- if (afterSlopResult != null) afterSlopResult to initialDelta else null
- }
-}
-
-private suspend fun AwaitPointerEventScope.awaitDrag(
- startEvent: PointerInputChange,
- initialDelta: Offset,
- velocityTracker: VelocityTracker,
- channel: SendChannel<DragEvent>?,
- hasDragged: (PointerInputChange) -> Boolean,
-): Boolean {
-
- val overSlopOffset = initialDelta
- val xSign = sign(startEvent.position.x)
- val ySign = sign(startEvent.position.y)
- val adjustedStart = startEvent.position -
- Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
- channel?.trySend(DragStarted(adjustedStart))
-
- channel?.trySend(DragDelta(initialDelta))
-
- return onDragOrUp(hasDragged, startEvent.id) { event ->
- // Velocity tracker takes all events, even UP
- velocityTracker.addPointerInputChange(event)
-
- // Dispatch only MOVE events
- if (!event.changedToUpIgnoreConsumed()) {
- val delta = event.positionChange()
- event.consume()
- channel?.trySend(DragDelta(delta))
- }
- }
-}
-
-private suspend fun AwaitPointerEventScope.onDragOrUp(
- hasDragged: (PointerInputChange) -> Boolean,
- pointerId: PointerId,
- onDrag: (PointerInputChange) -> Unit
-): Boolean {
- return drag(
- pointerId = pointerId,
- onDrag = onDrag,
- hasDragged = hasDragged,
- motionConsumed = { it.isConsumed }
- )?.let(onDrag) != null
-}
-
private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
private val dragScope: DragScope = object : DragScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index f9c9571..0c4b4c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -267,9 +267,10 @@
private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
private var reverseDirection: Boolean
) : DragGestureNode(
- canDrag,
- enabled,
- interactionSource
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = null
) {
override suspend fun drag(
@@ -282,8 +283,6 @@
}
}
- override val pointerDirectionConfig = BidirectionalPointerDirectionConfig
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
this@Draggable2DNode.onDragStarted(this, startedPosition)
@@ -317,10 +316,11 @@
this.startDragImmediately = startDragImmediately
update(
- canDrag,
- enabled,
- interactionSource,
- resetPointerInputHandling
+ canDrag = canDrag,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = null,
+ shouldResetPointerInputHandling = resetPointerInputHandling
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf62744a..bcfaf97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -273,7 +273,8 @@
) : DragGestureNode(
canDrag = CanDragCalculation,
enabled = enabled,
- interactionSource = interactionSource
+ interactionSource = interactionSource,
+ orientationLock = orientation
), ObserverModifierNode, CompositionLocalConsumerModifierNode,
FocusPropertiesModifierNode, KeyInputModifierNode {
@@ -333,9 +334,6 @@
scrollingLogic.dispatchDragEvents(forEachDelta)
}
- override val pointerDirectionConfig: PointerDirectionConfig
- get() = scrollingLogic.pointerDirectionConfig()
-
override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -386,7 +384,13 @@
this.flingBehavior = flingBehavior
// update DragGestureNode
- update(CanDragCalculation, enabled, interactionSource, resetPointerInputHandling)
+ update(
+ canDrag = CanDragCalculation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal,
+ shouldResetPointerInputHandling = resetPointerInputHandling
+ )
}
override fun onAttach() {
@@ -777,8 +781,6 @@
return resetPointerInputHandling
}
- fun pointerDirectionConfig(): PointerDirectionConfig = orientation.toPointerDirectionConfig()
-
fun isVertical(): Boolean = orientation == Vertical
}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
index 825a3ee..3ab85ce 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
@@ -44,6 +44,7 @@
* than the curved column, either at the [CurvedAlignment.Angular.Start] of the layout,
* at the [CurvedAlignment.Angular.End], or [CurvedAlignment.Angular.Center].
* If unspecified or null, they can choose for themselves.
+ * @param contentBuilder Scope used to provide the content for this column.
*/
public fun CurvedScope.curvedColumn(
modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
index f4fa3ca..4f5daea 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
@@ -42,6 +42,7 @@
* and if those needs to be reversed in a Rtl layout.
* If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
* See [CurvedDirection.Angular].
+ * @param contentBuilder Scope used to provide the content for this row.
*/
public fun CurvedScope.curvedRow(
modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index e344c9f..2087c5c3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -677,17 +677,19 @@
@RestrictTo(LIBRARY_GROUP)
public object SwipeableV2Defaults {
/**
- * The default animation used by [SwipeableV2State].
+ * The default animation that will be used to animate to a new state.
*/
val AnimationSpec = SpringSpec<Float>()
/**
- * The default velocity threshold (1.8 dp per millisecond) used by [rememberSwipeableV2State].
+ * The default velocity threshold (in dp per second) that the end velocity has to
+ * exceed in order to animate to the next state.
*/
val VelocityThreshold: Dp = 125.dp
/**
- * The default positional threshold (56 dp) used by [rememberSwipeableV2State]
+ * The default positional threshold used when calculating the target state while a swipe is in
+ * progress and when settling after the swipe ends.
*/
val PositionalThreshold: Density.(totalDistance: Float) -> Float =
fixedPositionalThreshold(56.dp)
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
index 1fdbc11..114fd30 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
@@ -310,10 +310,9 @@
// Canvas internally uses Spacer.drawBehind.
// Using Spacer.drawWithCache to optimize the stroke allocations.
Spacer(
+ // NB We must set the semantic role to Role.RadioButton in the parent Button,
+ // not here in the selection control - see b/330869742
modifier = modifier
- .semantics {
- this.role = Role.RadioButton
- }
.maybeSelectable(
onClick, enabled, selected, interactionSource, ripple, width, height
)
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
index 792bd00..3b0332a 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
@@ -211,6 +211,9 @@
indication = ripple,
interactionSource = interactionSource
)
+ // For a toggleable button, the role could be Checkbox or Switch,
+ // so we cannot set the semantics here. Instead,
+ // we set them in the toggle control
} else {
Modifier.selectable(
enabled = enabled,
@@ -218,7 +221,12 @@
onClick = { onCheckedChange(true) },
indication = ripple,
interactionSource = interactionSource
- )
+ ).semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ }
}
)
.padding(contentPadding),
@@ -375,6 +383,12 @@
indication = ripple,
interactionSource = checkedInteractionSource
)
+ .semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ }
}
Box(
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
index 2fde27b..c3d34d8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
@@ -136,6 +136,28 @@
}
@Test
+ fun selectable_chip_has_role_radiobutton() {
+ rule.setContentWithTheme {
+ SelectableChip(
+ selected = true,
+ onClick = {},
+ enabled = false,
+ label = { Text("Label") },
+ selectionControl = { TestImage() },
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .assert(
+ SemanticsMatcher.expectValue(
+ SemanticsProperties.Role,
+ Role.RadioButton
+ )
+ )
+ }
+
+ @Test
fun split_chip_has_clickaction_when_disabled() {
rule.setContentWithTheme {
SplitSelectableChip(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
index f5c3ef0..3997c14 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
@@ -82,6 +82,23 @@
}
@Test
+ fun radio_button_has_role_radiobutton() {
+ rule.setContentWithTheme {
+ RadioButtonWithDefaults(
+ modifier = Modifier.testTag(TEST_TAG)
+ )
+ }
+
+ rule.onNodeWithTag(TEST_TAG)
+ .assert(
+ SemanticsMatcher.expectValue(
+ SemanticsProperties.Role,
+ Role.RadioButton
+ )
+ )
+ }
+
+ @Test
fun radio_button_samples_build() {
rule.setContentWithTheme {
RadioButton()
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
index 600e737..a8ac0da 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
@@ -22,10 +22,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
@@ -67,25 +63,6 @@
}
@Test
- fun radio_control_has_role_radiobutton() {
- rule.setContentWithTheme {
- with(SelectionControlScope(isEnabled = true, isSelected = true)) {
- Radio(
- modifier = Modifier.testTag(TEST_TAG)
- )
- }
- }
-
- rule.onNodeWithTag(TEST_TAG)
- .assert(
- SemanticsMatcher.expectValue(
- SemanticsProperties.Role,
- Role.RadioButton
- )
- )
- }
-
- @Test
fun radio_control_is_correctly_enabled() {
rule.setContentWithTheme {
with(SelectionControlScope(isEnabled = true, isSelected = true)) {
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index 70dbab71..b4a6f53 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -144,7 +144,13 @@
indication = rippleOrFallbackImplementation(),
interactionSource = interactionSource
)
- .padding(contentPadding),
+ .padding(contentPadding)
+ .semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ },
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) {
@@ -329,7 +335,13 @@
.width(SPLIT_WIDTH)
.wrapContentHeight(align = Alignment.CenterVertically)
.wrapContentWidth(align = Alignment.End)
- .then(endPadding),
+ .then(endPadding)
+ .semantics {
+ // For a selectable button, the role is always RadioButton.
+ // See also b/330869742 for issue with setting the RadioButton role
+ // within the selection control.
+ role = Role.RadioButton
+ },
) {
val scope = remember(enabled, selected) { SelectionControlScope(enabled, selected) }
selectionControl(scope)
diff --git a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
index 7981cf2..5b295fc 100644
--- a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
+++ b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
@@ -34,7 +34,7 @@
* note, the above list is not exhaustive. It previews the composables on a small round Wear device.
*
* @sample androidx.wear.compose.material.samples.TitleCardWithImagePreview
- * @see [Preview.fontScale]
+ * @see Preview.fontScale
*/
@Preview(
device = WearDevices.SMALL_ROUND,
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 28d0392..d68e962 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -25,8 +25,8 @@
defaultConfig {
applicationId "androidx.wear.compose.integration.demos"
minSdk 25
- versionCode 23
- versionName "1.23"
+ versionCode 24
+ versionName "1.24"
}
buildTypes {
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -14,7 +14,7 @@
}
public static final class AnimationParameterBuilders.AnimationParameters.Builder {
- ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
}
public static final class AnimationParameterBuilders.AnimationSpec.Builder {
- ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
- method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
}
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
}
public static final class AnimationParameterBuilders.Repeatable.Builder {
- ctor public AnimationParameterBuilders.Repeatable.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -14,7 +14,7 @@
}
public static final class AnimationParameterBuilders.AnimationParameters.Builder {
- ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
}
public static final class AnimationParameterBuilders.AnimationSpec.Builder {
- ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
- method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+ method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
}
@androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
}
public static final class AnimationParameterBuilders.Repeatable.Builder {
- ctor public AnimationParameterBuilders.Repeatable.Builder();
+ ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
index 44eb9a7..a800b2a 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
@@ -140,6 +140,7 @@
AnimationParameterProto.AnimationSpec.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-2136602843);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets animation parameters including duration, easing and repeat delay. */
@@ -158,6 +159,7 @@
* Sets the repeatable mode to be used for specifying repetition parameters for the
* animation. If not set, animation won't be repeated.
*/
+ @RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setRepeatable(@NonNull Repeatable repeatable) {
mImpl.setRepeatable(repeatable.toProto());
@@ -261,6 +263,7 @@
AnimationParameterProto.AnimationParameters.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-1301308590);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -491,7 +494,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
AnimationParameterProto.CubicBezierEasing toProto() {
return mImpl;
@@ -525,6 +527,7 @@
AnimationParameterProto.CubicBezierEasing.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(856403705);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -704,6 +707,7 @@
AnimationParameterProto.Repeatable.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(2110475048);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
index 36e1b0b..5a088b9 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
@@ -48,12 +48,14 @@
}
/** Sets the value to use as the value when true in a conditional expression. */
- public @NonNull IfTrueScope<T, RawT> use(T valueWhenTrue) {
+ @NonNull
+ public IfTrueScope<T, RawT> use(T valueWhenTrue) {
return new IfTrueScope<>(valueWhenTrue, conditionBuilder, rawTypeMapper);
}
/** Sets the value to use as the value when true in a conditional expression. */
- public @NonNull IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
+ @NonNull
+ public IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
return use(rawTypeMapper.apply(valueWhenTrue));
}
}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 17d89ce..500985d 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -708,7 +708,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -1255,7 +1255,7 @@
/** Sets the value to start animating from. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
- public AnimatableFixedInt32.Builder setFromValue(int fromValue) {
+ public Builder setFromValue(int fromValue) {
mImpl.setFromValue(fromValue);
mFingerprint.recordPropertyUpdate(1, fromValue);
return this;
@@ -1264,7 +1264,7 @@
/** Sets the value to animate to. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
- public AnimatableFixedInt32.Builder setToValue(int toValue) {
+ public Builder setToValue(int toValue) {
mImpl.setToValue(toValue);
mFingerprint.recordPropertyUpdate(2, toValue);
return this;
@@ -1398,7 +1398,7 @@
/** Sets the value to watch, and animate when it changes. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
- public AnimatableDynamicInt32.Builder setInput(@NonNull DynamicInt32 input) {
+ public Builder setInput(@NonNull DynamicInt32 input) {
mImpl.setInput(input.toDynamicInt32Proto());
mFingerprint.recordPropertyUpdate(
1, checkNotNull(input.getFingerprint()).aggregateValueAsInt());
@@ -2413,7 +2413,7 @@
/** Returns whether digit grouping is used or not. */
public boolean isGroupingUsed() {
- return mInt32FormatOp.getGroupingUsed();
+ return mInt32FormatOp.isGroupingUsed();
}
/** Builder to create {@link IntFormatter} objects. */
@@ -2579,7 +2579,7 @@
* locale. If not defined, defaults to false. For example, for locale en_US, using grouping
* with 1234 would yield "1,234".
*/
- public boolean getGroupingUsed() {
+ public boolean isGroupingUsed() {
return mImpl.getGroupingUsed();
}
@@ -2638,13 +2638,13 @@
+ ", minIntegerDigits="
+ getMinIntegerDigits()
+ ", groupingUsed="
- + getGroupingUsed()
+ + isGroupingUsed()
+ "}";
}
/** Builder for {@link Int32FormatOp}. */
public static final class Builder implements DynamicString.Builder {
- final DynamicProto.Int32FormatOp.Builder mImpl =
+ private final DynamicProto.Int32FormatOp.Builder mImpl =
DynamicProto.Int32FormatOp.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(196209833);
@@ -2792,7 +2792,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -3143,7 +3143,7 @@
* locale. If not defined, defaults to false. For example, for locale en_US, using grouping
* with 1234.56 would yield "1,234.56".
*/
- public boolean getGroupingUsed() {
+ public boolean isGroupingUsed() {
return mImpl.getGroupingUsed();
}
@@ -3206,7 +3206,7 @@
+ ", minIntegerDigits="
+ getMinIntegerDigits()
+ ", groupingUsed="
- + getGroupingUsed()
+ + isGroupingUsed()
+ "}";
}
@@ -3736,7 +3736,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -5012,7 +5012,7 @@
/** Returns whether digit grouping is used or not. */
public boolean isGroupingUsed() {
- return mFloatFormatOp.getGroupingUsed();
+ return mFloatFormatOp.isGroupingUsed();
}
/** Builder to create {@link FloatFormatter} objects. */
@@ -5228,8 +5228,8 @@
@NonNull
public DynamicProto.DynamicBool toDynamicBoolProto(boolean withFingerprint) {
if (withFingerprint) {
- return DynamicProto.DynamicBool.newBuilder().
- setStateSource(mImpl)
+ return DynamicProto.DynamicBool.newBuilder()
+ .setStateSource(mImpl)
.setFingerprint(checkNotNull(mFingerprint).toProto())
.build();
}
@@ -5265,7 +5265,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -6144,7 +6144,7 @@
return this;
}
- /** Sets the name space for the state key. */
+ /** Sets the namespace for the state key. */
@RequiresSchemaVersion(major = 1, minor = 200)
@NonNull
public Builder setSourceNamespace(@NonNull String sourceNamespace) {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
index 69c88e3..c3b5c45 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
@@ -28,7 +28,6 @@
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicType;
import androidx.wear.protolayout.expression.FixedValueBuilders.FixedBool;
import androidx.wear.protolayout.expression.FixedValueBuilders.FixedColor;
import androidx.wear.protolayout.expression.FixedValueBuilders.FixedDuration;
@@ -51,7 +50,7 @@
/** Interface defining a dynamic data value. */
@RequiresSchemaVersion(major = 1, minor = 200)
- public interface DynamicDataValue<T extends DynamicType> {
+ public interface DynamicDataValue<T extends DynamicBuilders.DynamicType> {
/** Get the protocol buffer representation of this object. */
@RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
@@ -310,7 +309,7 @@
/** Builder to create {@link DynamicDataValue} objects. */
@RestrictTo(Scope.LIBRARY_GROUP)
- interface Builder<T extends DynamicType> {
+ interface Builder<T extends DynamicBuilders.DynamicType> {
/** Builds an instance with values accumulated in this Builder. */
@NonNull
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
index ced6685..e16e059 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
@@ -78,7 +78,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedInt32 toProto() {
return mImpl;
@@ -144,6 +143,7 @@
private final FixedProto.FixedInt32.Builder mImpl = FixedProto.FixedInt32.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(974881783);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the value. */
@@ -203,7 +203,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedString toProto() {
return mImpl;
@@ -271,6 +270,7 @@
FixedProto.FixedString.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(1963352072);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the value. */
@@ -333,7 +333,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedFloat toProto() {
return mImpl;
@@ -399,6 +398,7 @@
private final FixedProto.FixedFloat.Builder mImpl = FixedProto.FixedFloat.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-144724541);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -461,7 +461,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedBool toProto() {
return mImpl;
@@ -527,6 +526,7 @@
private final FixedProto.FixedBool.Builder mImpl = FixedProto.FixedBool.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-665116398);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the value. */
@@ -587,7 +587,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedColor toProto() {
return mImpl;
@@ -653,6 +652,7 @@
private final FixedProto.FixedColor.Builder mImpl = FixedProto.FixedColor.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-1895809356);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets the color value, in ARGB format. */
@@ -733,7 +733,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedInstant toProto() {
return mImpl;
@@ -778,6 +777,7 @@
FixedProto.FixedInstant.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(-1986552556);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/**
@@ -860,7 +860,6 @@
}
/** Returns the internal proto instance. */
- @RestrictTo(Scope.LIBRARY_GROUP)
@NonNull
FixedProto.FixedDuration toProto() {
return mImpl;
@@ -905,6 +904,7 @@
FixedProto.FixedDuration.newBuilder();
private final Fingerprint mFingerprint = new Fingerprint(9029504);
+ @RequiresSchemaVersion(major = 1, minor = 200)
public Builder() {}
/** Sets duration in seconds. */
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
new file mode 100644
index 0000000..d6a206c
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License = 0 Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing = 0 software
+ * distributed under the License is distributed on an "AS IS" BASIS = 0
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND = 0 either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Shared constants. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Constants {
+
+ private Constants() {}
+
+ /** The reason why an update was requested. */
+ @IntDef({
+ UPDATE_REQUEST_REASON_UNKNOWN,
+ UPDATE_REQUEST_REASON_SYSUI_CAROUSEL,
+ UPDATE_REQUEST_REASON_FRESHNESS,
+ UPDATE_REQUEST_REASON_USER_INTERACTION,
+ UPDATE_REQUEST_REASON_UPDATE_REQUESTER,
+ UPDATE_REQUEST_REASON_CACHE_INVALIDATION,
+ UPDATE_REQUEST_REASON_RETRY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UpdateRequestReason {}
+
+ /** Unknown reason. */
+ public static final int UPDATE_REQUEST_REASON_UNKNOWN = 0;
+
+ /** Update triggered by SysUI Carousel. */
+ public static final int UPDATE_REQUEST_REASON_SYSUI_CAROUSEL = 1;
+
+ /** Update triggered by freshness. */
+ public static final int UPDATE_REQUEST_REASON_FRESHNESS = 2;
+
+ /** Update triggered by user interaction (e.g. clicking on the tile). */
+ public static final int UPDATE_REQUEST_REASON_USER_INTERACTION = 3;
+
+ /** Update triggered using update requester. */
+ public static final int UPDATE_REQUEST_REASON_UPDATE_REQUESTER = 4;
+
+ /** Update triggered due to clearing the cache. */
+ public static final int UPDATE_REQUEST_REASON_CACHE_INVALIDATION = 5;
+
+ /** Update triggered by retry policy. */
+ public static final int UPDATE_REQUEST_REASON_RETRY = 6;
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
new file mode 100644
index 0000000..f292c98
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+/** A No-Op implementation of {@link ProviderStatsLogger}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class NoOpProviderStatsLogger implements ProviderStatsLogger {
+ private static final String TAG = "NoOpProviderStatsLogger";
+
+ /** Creates an instance of {@link NoOpProviderStatsLogger}. */
+ public NoOpProviderStatsLogger(@NonNull String reason) {
+ Log.i(TAG, "Instance used because " + reason);
+ }
+
+ /** No-op method. */
+ @Override
+ public void logLayoutSchemaVersion(int major, int minor) {}
+
+ /** No-op method. */
+ @Override
+ public void logStateStructure(@NonNull State state, boolean isInitialState) {}
+
+ /** No-op method. */
+ @Override
+ public void logIgnoredFailure(int failure) {}
+
+ /** No-op method. */
+ @Override
+ public void logInflationFailed(@InflationFailureReason int failureReason) {}
+
+ /** No-op method. */
+ @Override
+ @NonNull
+ public InflaterStatsLogger createInflaterStatsLogger() {
+ return new NoOpInflaterStatsLogger();
+ }
+
+ /** No-op method. */
+ @Override
+ public void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger) {}
+
+ /** No-op method. */
+ @Override
+ public void logTileRequestReason(@UpdateRequestReason int updateRequestReason) {}
+
+ /** A No-Op implementation of {@link InflaterStatsLogger}. */
+ public static class NoOpInflaterStatsLogger implements InflaterStatsLogger {
+
+ private NoOpInflaterStatsLogger() {}
+
+ @Override
+ public void logMutationChangedNodes(int changedNodesCount) {}
+
+ @Override
+ public void logTotalNodeCount(int totalNodesCount) {}
+
+ /** No-op method. */
+ @Override
+ public void logDrawableUsage(@NonNull Drawable drawable) {}
+
+ /** No-op method. */
+ @Override
+ public void logIgnoredFailure(@IgnoredFailure int failure) {}
+
+ /** No-op method. */
+ @Override
+ public void logInflationFailed(@InflationFailureReason int failureReason) {}
+ }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
new file mode 100644
index 0000000..96a6fb0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Logger used for collecting metrics. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ProviderStatsLogger {
+
+ /** Failures that doesn't cause the inflation to fail. */
+ @IntDef({
+ IGNORED_FAILURE_UNKNOWN,
+ IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION,
+ IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED,
+ IGNORED_FAILURE_DIFFING_FAILURE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface IgnoredFailure {}
+
+ /** Unknown failure. */
+ int IGNORED_FAILURE_UNKNOWN = 0;
+
+ /** Failure applying the diff mutation. */
+ int IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION = 1;
+
+ /** Failure caused by exceeding animation quota. */
+ int IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED = 2;
+
+ /** Failure diffing the layout. */
+ int IGNORED_FAILURE_DIFFING_FAILURE = 3;
+
+ /** Failures that causes the inflation to fail. */
+ @IntDef({
+ INFLATION_FAILURE_REASON_UNKNOWN,
+ INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED,
+ INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface InflationFailureReason {}
+
+ /** Unknown failure. */
+ int INFLATION_FAILURE_REASON_UNKNOWN = 0;
+
+ /** Failure caused by exceeding maximum layout depth. */
+ int INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED = 1;
+
+ /** Failure caused by exceeding maximum expression node count. */
+ int INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED = 2;
+
+ /** Log the schema version of the received layout. */
+ void logLayoutSchemaVersion(int major, int minor);
+
+ /** Log Protolayout state structure. */
+ void logStateStructure(@NonNull State state, boolean isInitialState);
+
+ /** Log the occurrence of an ignored failure. */
+ @UiThread
+ void logIgnoredFailure(@IgnoredFailure int failure);
+
+ /** Log the reason for inflation failure. */
+ @UiThread
+ void logInflationFailed(@InflationFailureReason int failureReason);
+
+ /**
+ * Creates an {@link InflaterStatsLogger} and marks the start of inflation. The atoms will be
+ * logged to statsd only when {@link #logInflationFinished} is called.
+ */
+ @UiThread
+ @NonNull
+ InflaterStatsLogger createInflaterStatsLogger();
+
+ /** Makes the end of inflation and log the inflation results. */
+ @UiThread
+ void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger);
+
+ /** Log tile request reason. */
+ void logTileRequestReason(@UpdateRequestReason int updateRequestReason);
+
+ /** Logger used for logging inflation stats. */
+ interface InflaterStatsLogger {
+ /** log the mutation changed nodes count for the ongoing inflation. */
+ @UiThread
+ void logMutationChangedNodes(int changedNodesCount);
+
+ /** Log the total nodes count for the ongoing inflation. */
+ @UiThread
+ void logTotalNodeCount(int totalNodesCount);
+
+ /**
+ * Log the usage of a drawable. This method should be called between {@link
+ * #createInflaterStatsLogger()} and {@link #logInflationFinished(InflaterStatsLogger)}.
+ */
+ @UiThread
+ void logDrawableUsage(@NonNull Drawable drawable);
+
+ /**
+ * Log the occurrence of an ignored failure. The usage of this method is not restricted to
+ * inflation start or end.
+ */
+ @UiThread
+ void logIgnoredFailure(@IgnoredFailure int failure);
+
+ /**
+ * Log the reason for inflation failure. This will make any future call {@link
+ * #logInflationFinished(InflaterStatsLogger)} a Noop.
+ */
+ @UiThread
+ void logInflationFailed(@InflationFailureReason int failureReason);
+ }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
new file mode 100644
index 0000000..8424728
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+
+/** Artifacts resulted from the layout rendering. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface RenderingArtifact {
+
+ /** Creates a {@link RenderingArtifact} instance. */
+ @NonNull
+ static RenderingArtifact create(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+ return new SuccessfulRenderingArtifact(inflaterStatsLogger);
+ }
+
+ /** Creates a {@link RenderingArtifact} instance for a skipped inflation. */
+ @NonNull
+ static RenderingArtifact skipped() {
+ return new SkippedRenderingArtifact();
+ }
+
+ /** Creates a {@link RenderingArtifact} instance for a failed inflation. */
+ @NonNull
+ static RenderingArtifact failed() {
+ return new FailedRenderingArtifact();
+ }
+
+ /** Artifacts resulted from a successful layout rendering. */
+ class SuccessfulRenderingArtifact implements RenderingArtifact {
+ @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
+
+ private SuccessfulRenderingArtifact(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+ mInflaterStatsLogger = inflaterStatsLogger;
+ }
+
+ /**
+ * Returns the {@link ProviderStatsLogger.InflaterStatsLogger} used log inflation stats.
+ * This will return {@code null} if the inflation was skipped or failed.
+ */
+ @NonNull
+ public InflaterStatsLogger getInflaterStatsLogger() {
+ return mInflaterStatsLogger;
+ }
+ }
+
+ /** Artifacts resulted from a skipped layout rendering. */
+ class SkippedRenderingArtifact implements RenderingArtifact {}
+
+ /** Artifacts resulted from a failed layout rendering. */
+ class FailedRenderingArtifact implements RenderingArtifact {}
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 08f0228..8f18ca8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -20,6 +20,13 @@
import static android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY;
import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED;
+
+import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
import android.content.Context;
import android.content.res.Resources;
@@ -41,6 +48,7 @@
import androidx.wear.protolayout.expression.PlatformDataKey;
import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider;
+import androidx.wear.protolayout.expression.pipeline.QuotaManager;
import androidx.wear.protolayout.expression.pipeline.StateStore;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase;
@@ -52,7 +60,11 @@
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -114,6 +126,7 @@
@NonNull private final ListeningExecutorService mUiExecutorService;
@NonNull private final ListeningExecutorService mBgExecutorService;
@NonNull private final String mClickableIdExtra;
+ @NonNull private final ProviderStatsLogger mProviderStatsLogger;
@Nullable private final LoggingUtils mLoggingUtils;
@Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@@ -219,10 +232,11 @@
*/
@UiThread
@NonNull
- ListenableFuture<Void> postInflate(
+ ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching);
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger);
}
/** Result of a {@link #renderOrComputeMutations} call when no changes are required. */
@@ -234,11 +248,12 @@
@NonNull
@Override
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
- return Futures.immediateVoidFuture();
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
+ return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
}
}
@@ -251,11 +266,12 @@
@NonNull
@Override
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
- return Futures.immediateVoidFuture();
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
+ return immediateFuture(RenderingArtifact.failed());
}
}
@@ -278,10 +294,11 @@
@NonNull
@Override
@UiThread
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
InflateResult inflateResult =
checkNotNull(
mNewInflateParentData.mInflateResult,
@@ -292,7 +309,7 @@
attachParent.addView(
inflateResult.inflateParent, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
inflateResult.updateDynamicDataPipeline(isReattaching);
- return Futures.immediateVoidFuture();
+ return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
}
}
@@ -318,10 +335,11 @@
@NonNull
@Override
@UiThread
- public ListenableFuture<Void> postInflate(
+ public ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
- boolean isReattaching) {
+ boolean isReattaching,
+ InflaterStatsLogger inflaterStatsLogger) {
return mInflater.applyMutation(checkNotNull(prevInflateParent), mMutation);
}
}
@@ -345,6 +363,7 @@
@NonNull private final String mClickableIdExtra;
@Nullable private final LoggingUtils mLoggingUtils;
+ @NonNull private final ProviderStatsLogger mProviderStatsLogger;
private final boolean mAnimationEnabled;
private final int mRunningAnimationsLimit;
@@ -366,6 +385,7 @@
@Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
@NonNull String clickableIdExtra,
@Nullable LoggingUtils loggingUtils,
+ @NonNull ProviderStatsLogger providerStatsLogger,
boolean animationEnabled,
int runningAnimationsLimit,
boolean updatesEnabled,
@@ -384,6 +404,7 @@
this.mExtensionViewProvider = extensionViewProvider;
this.mClickableIdExtra = clickableIdExtra;
this.mLoggingUtils = loggingUtils;
+ this.mProviderStatsLogger = providerStatsLogger;
this.mAnimationEnabled = animationEnabled;
this.mRunningAnimationsLimit = runningAnimationsLimit;
this.mUpdatesEnabled = updatesEnabled;
@@ -468,6 +489,13 @@
return mLoggingUtils;
}
+ /** Returns the provider stats logger used for telemetry. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public ProviderStatsLogger getProviderStatsLogger() {
+ return mProviderStatsLogger;
+ }
+
/** Returns whether animations are enabled. */
@RestrictTo(Scope.LIBRARY)
public boolean getAnimationEnabled() {
@@ -529,6 +557,7 @@
@Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@NonNull private final String mClickableIdExtra;
@Nullable private LoggingUtils mLoggingUtils;
+ @Nullable private ProviderStatsLogger mProviderStatsLogger;
private boolean mAnimationEnabled = true;
private int mRunningAnimationsLimit = DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS;
@@ -632,6 +661,15 @@
return this;
}
+ /** Sets the provider stats logger used for telemetry. */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setProviderStatsLogger(
+ @NonNull ProviderStatsLogger providerStatsLogger) {
+ this.mProviderStatsLogger = providerStatsLogger;
+ return this;
+ }
+
/**
* Sets whether animation are enabled. If disabled, none of the animation will be
* played.
@@ -715,6 +753,12 @@
if (mRendererResources == null) {
this.mRendererResources = mUiContext.getResources();
}
+
+ if (mProviderStatsLogger == null) {
+ mProviderStatsLogger =
+ new NoOpProviderStatsLogger(
+ "ProviderStatsLogger not provided to " + TAG);
+ }
return new Config(
mUiContext,
mRendererResources,
@@ -728,6 +772,7 @@
mExtensionViewProvider,
mClickableIdExtra,
mLoggingUtils,
+ mProviderStatsLogger,
mAnimationEnabled,
mRunningAnimationsLimit,
mUpdatesEnabled,
@@ -754,24 +799,51 @@
this.mWasFullyVisibleBefore = false;
this.mAllowLayoutChangingBindsWithoutDefault =
config.getAllowLayoutChangingBindsWithoutDefault();
+ this.mProviderStatsLogger = config.getProviderStatsLogger();
StateStore stateStore = config.getStateStore();
- if (stateStore != null) {
- mDataPipeline =
- config.getAnimationEnabled()
- ? new ProtoLayoutDynamicDataPipeline(
- config.getPlatformDataProviders(),
- stateStore,
- new FixedQuotaManagerImpl(
- config.getRunningAnimationsLimit(), "animations"),
- new FixedQuotaManagerImpl(
- DYNAMIC_NODES_MAX_COUNT, "dynamic nodes"))
- : new ProtoLayoutDynamicDataPipeline(
- config.getPlatformDataProviders(), stateStore);
- mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
- } else {
+ if (stateStore == null) {
mDataPipeline = null;
+ return;
}
+
+ if (config.getAnimationEnabled()) {
+ QuotaManager nodeQuotaManager =
+ new FixedQuotaManagerImpl(DYNAMIC_NODES_MAX_COUNT, "dynamic nodes") {
+ @Override
+ public boolean tryAcquireQuota(int quota) {
+ boolean success = super.tryAcquireQuota(quota);
+ if (!success) {
+ mProviderStatsLogger.logInflationFailed(
+ INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED);
+ }
+ return success;
+ }
+ };
+ mDataPipeline =
+ new ProtoLayoutDynamicDataPipeline(
+ config.getPlatformDataProviders(),
+ stateStore,
+ new FixedQuotaManagerImpl(
+ config.getRunningAnimationsLimit(), "animations") {
+ @Override
+ public boolean tryAcquireQuota(int quota) {
+ boolean success = super.tryAcquireQuota(quota);
+ if (!success) {
+ mProviderStatsLogger.logIgnoredFailure(
+ IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED);
+ }
+ return success;
+ }
+ },
+ nodeQuotaManager);
+ } else {
+ mDataPipeline =
+ new ProtoLayoutDynamicDataPipeline(
+ config.getPlatformDataProviders(), stateStore);
+ }
+
+ mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
}
@WorkerThread
@@ -780,7 +852,8 @@
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@Nullable RenderedMetadata prevRenderedMetadata,
- @NonNull ViewProperties parentViewProp) {
+ @NonNull ViewProperties parentViewProp,
+ @NonNull InflaterStatsLogger inflaterStatsLogger) {
ResourceResolvers resolvers =
mResourceResolversProvider.getResourceResolvers(
mUiContext, resources, mUiExecutorService, mAnimationEnabled);
@@ -797,10 +870,10 @@
if (sameFingerprint) {
if (mPrevLayoutAlreadyFailingDepthCheck) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
} else {
- checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH);
+ checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH, inflaterStatsLogger);
}
mPrevLayoutAlreadyFailingDepthCheck = false;
@@ -815,6 +888,7 @@
.setClickableIdExtra(mClickableIdExtra)
.setAllowLayoutChangingBindsWithoutDefault(
mAllowLayoutChangingBindsWithoutDefault)
+ .setInflaterStatsLogger(inflaterStatsLogger)
.setApplyFontVariantBodyAsDefault(true);
if (mDataPipeline != null) {
inflaterConfigBuilder.setDynamicDataPipeline(mDataPipeline);
@@ -886,6 +960,18 @@
return new InflateParentData(result);
}
+ @UiThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public ListenableFuture<RenderingArtifact> renderLayoutAndAttach(
+ @NonNull Layout layout,
+ @NonNull ResourceProto.Resources resources,
+ @NonNull ViewGroup attachParent) {
+
+ return renderAndAttach(
+ layout, resources, attachParent, mProviderStatsLogger.createInflaterStatsLogger());
+ }
+
/**
* Render the layout for this layout and attach this layout instance to a {@code attachParent}
* container. Note that this method may clear all of {@code attachParent}'s children before
@@ -911,6 +997,47 @@
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@NonNull ViewGroup attachParent) {
+ SettableFuture<Void> result = SettableFuture.create();
+ ListenableFuture<RenderingArtifact> future =
+ renderLayoutAndAttach(layout, resources, attachParent);
+ if (future.isDone()) {
+ if (future.isCancelled()) {
+ return immediateCancelledFuture();
+ }
+ return immediateFuture(null);
+ } else {
+ future.addListener(
+ () -> {
+ if (future.isCancelled()) {
+ result.cancel(/* mayInterruptIfRunning= */ false);
+ } else {
+ try {
+ RenderingArtifact ignored = future.get();
+ result.set(null);
+ } catch (ExecutionException
+ | InterruptedException
+ | CancellationException e) {
+ Log.e(TAG, "Failed to render layout", e);
+ result.setException(e);
+ }
+ }
+ },
+ mUiExecutorService);
+ }
+ return result;
+ }
+
+ @UiThread
+ @SuppressWarnings({
+ "ReferenceEquality",
+ "ExecutorTaskName",
+ }) // layout == prevLayout is intentional (and enough in this case)
+ @NonNull
+ private ListenableFuture<RenderingArtifact> renderAndAttach(
+ @NonNull Layout layout,
+ @NonNull ResourceProto.Resources resources,
+ @NonNull ViewGroup attachParent,
+ @NonNull InflaterStatsLogger inflaterStatsLogger) {
if (mLoggingUtils != null && mLoggingUtils.canLogD(TAG)) {
mLoggingUtils.logD(TAG, "Layout received in #renderAndAttach:\n %s", layout.toString());
mLoggingUtils.logD(
@@ -930,7 +1057,7 @@
if (layout == mPrevLayout && mInflateParent != null) {
// Nothing to do.
- return Futures.immediateVoidFuture();
+ return Futures.immediateFuture(RenderingArtifact.skipped());
}
boolean isReattaching = false;
@@ -1000,10 +1127,11 @@
layout,
resources,
prevRenderedMetadata,
- parentViewProp));
+ parentViewProp,
+ inflaterStatsLogger));
mCanReattachWithoutRendering = false;
}
- SettableFuture<Void> result = SettableFuture.create();
+ SettableFuture<RenderingArtifact> result = SettableFuture.create();
if (!checkNotNull(mRenderFuture).isDone()) {
ListenableFuture<RenderResult> rendererFuture = mRenderFuture;
mRenderFuture.addListener(
@@ -1023,7 +1151,8 @@
checkNotNull(rendererFuture).get(),
/* isReattaching= */ false,
layout,
- resources));
+ resources,
+ inflaterStatsLogger));
} catch (ExecutionException
| InterruptedException
| CancellationException e) {
@@ -1048,7 +1177,8 @@
mRenderFuture.get(),
isReattaching,
layout,
- resources));
+ resources,
+ inflaterStatsLogger));
} catch (ExecutionException | InterruptedException | CancellationException e) {
Log.e(TAG, "Failed to render layout", e);
result.setException(e);
@@ -1064,6 +1194,12 @@
*/
public void invalidateCache() {
mPrevResourcesVersion = null;
+ // Cancel any ongoing rendering which might have a reference to older app resources.
+ if (mRenderFuture != null && !mRenderFuture.isDone()) {
+ mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mRenderFuture = null;
+ Log.w(TAG, "Cancelled ongoing rendering due to cache invalidation.");
+ }
}
@Nullable
@@ -1080,13 +1216,14 @@
@UiThread
@SuppressWarnings("ExecutorTaskName")
@NonNull
- private ListenableFuture<Void> postInflate(
+ private ListenableFuture<RenderingArtifact> postInflate(
@NonNull ViewGroup attachParent,
@Nullable ViewGroup prevInflateParent,
@NonNull RenderResult renderResult,
boolean isReattaching,
@NonNull Layout layout,
- @NonNull ResourceProto.Resources resources) {
+ @NonNull ResourceProto.Resources resources,
+ InflaterStatsLogger inflaterStatsLogger) {
mCanReattachWithoutRendering = renderResult.canReattachWithoutRendering();
if (renderResult instanceof InflatedIntoNewParentRenderResult) {
@@ -1101,9 +1238,10 @@
.inflateParent;
}
- ListenableFuture<Void> postInflateFuture =
- renderResult.postInflate(attachParent, prevInflateParent, isReattaching);
- SettableFuture<Void> result = SettableFuture.create();
+ ListenableFuture<RenderingArtifact> postInflateFuture =
+ renderResult.postInflate(
+ attachParent, prevInflateParent, isReattaching, inflaterStatsLogger);
+ SettableFuture<RenderingArtifact> result = SettableFuture.create();
if (!postInflateFuture.isDone()) {
postInflateFuture.addListener(
() -> {
@@ -1114,20 +1252,24 @@
| CancellationException e) {
result.setFuture(
handlePostInflateFailure(
- e, layout, resources, prevInflateParent, attachParent));
+ e,
+ layout,
+ resources,
+ prevInflateParent,
+ attachParent,
+ inflaterStatsLogger));
}
},
mUiExecutorService);
} else {
try {
- postInflateFuture.get();
- return Futures.immediateVoidFuture();
+ return immediateFuture(postInflateFuture.get());
} catch (ExecutionException
| InterruptedException
| CancellationException
| ViewMutationException e) {
return handlePostInflateFailure(
- e, layout, resources, prevInflateParent, attachParent);
+ e, layout, resources, prevInflateParent, attachParent, inflaterStatsLogger);
}
}
return result;
@@ -1136,22 +1278,24 @@
@UiThread
@SuppressWarnings("ReferenceEquality") // layout == prevLayout is intentional
@NonNull
- private ListenableFuture<Void> handlePostInflateFailure(
+ private ListenableFuture<RenderingArtifact> handlePostInflateFailure(
@NonNull Throwable error,
@NonNull Layout layout,
@NonNull ResourceProto.Resources resources,
@Nullable ViewGroup prevInflateParent,
- @NonNull ViewGroup parent) {
+ @NonNull ViewGroup parent,
+ InflaterStatsLogger inflaterStatsLogger) {
// If a RuntimeError is thrown, it'll be wrapped in an UncheckedExecutionException
Throwable e = error.getCause();
if (e instanceof ViewMutationException) {
+ inflaterStatsLogger.logIgnoredFailure(IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION);
Log.w(TAG, "applyMutation failed." + e.getMessage());
if (mPrevLayout == layout && parent == mAttachParent) {
Log.w(TAG, "Retrying full inflation.");
// Clear rendering metadata and prevLayout to force a full reinflation.
ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent));
mPrevLayout = null;
- return renderAndAttach(layout, resources, parent);
+ return renderAndAttach(layout, resources, parent, inflaterStatsLogger);
}
} else {
Log.e(TAG, "postInflate failed.", error);
@@ -1176,6 +1320,7 @@
private void detachInternal() {
if (mRenderFuture != null && !mRenderFuture.isDone()) {
mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mRenderFuture = null;
}
setLayoutVisibility(ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE);
@@ -1227,9 +1372,12 @@
}
/** Returns true if the layout element depth doesn't exceed the given {@code allowedDepth}. */
- private void checkLayoutDepth(LayoutElement layoutElement, int allowedDepth) {
+ private void checkLayoutDepth(
+ LayoutElement layoutElement,
+ int allowedDepth,
+ InflaterStatsLogger inflaterStatsLogger) {
if (allowedDepth <= 0) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
List<LayoutElement> children = ImmutableList.of();
switch (layoutElement.getInnerCase()) {
@@ -1245,28 +1393,32 @@
case ARC:
List<ArcLayoutElement> arcElements = layoutElement.getArc().getContentsList();
if (!arcElements.isEmpty() && allowedDepth == 1) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
for (ArcLayoutElement element : arcElements) {
if (element.getInnerCase() == InnerCase.ADAPTER) {
- checkLayoutDepth(element.getAdapter().getContent(), allowedDepth - 1);
+ checkLayoutDepth(
+ element.getAdapter().getContent(),
+ allowedDepth - 1,
+ inflaterStatsLogger);
}
}
break;
case SPANNABLE:
if (layoutElement.getSpannable().getSpansCount() > 0 && allowedDepth == 1) {
- throwExceptionForLayoutDepthCheckFailure();
+ handleLayoutDepthCheckFailure(inflaterStatsLogger);
}
break;
default:
// Other LayoutElements have depth of one.
}
for (LayoutElement child : children) {
- checkLayoutDepth(child, allowedDepth - 1);
+ checkLayoutDepth(child, allowedDepth - 1, inflaterStatsLogger);
}
}
- private void throwExceptionForLayoutDepthCheckFailure() {
+ private void handleLayoutDepthCheckFailure(InflaterStatsLogger inflaterStatsLogger) {
+ inflaterStatsLogger.logInflationFailed(INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED);
mPrevLayoutAlreadyFailingDepthCheck = true;
throw new IllegalStateException(
"Layout depth exceeds maximum allowed depth: " + MAX_LAYOUT_ELEMENT_DEPTH);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 1ca9a23..4997dde 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -28,7 +28,7 @@
import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.getParentNodePosId;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
-import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
import static java.lang.Math.max;
import static java.lang.Math.min;
@@ -180,9 +180,12 @@
import androidx.wear.protolayout.renderer.ProtoLayoutTheme.FontSet;
import androidx.wear.protolayout.renderer.R;
import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.LayoutDiff;
import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.TreeNodeWithChange;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.LayoutInfo;
@@ -301,6 +304,7 @@
final String mClickableIdExtra;
@Nullable private final LoggingUtils mLoggingUtils;
+ @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
@Nullable final Executor mLoadActionExecutor;
final LoadActionListener mLoadActionListener;
@@ -528,6 +532,7 @@
@NonNull private final String mClickableIdExtra;
@Nullable private final LoggingUtils mLoggingUtils;
+ @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
@Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
private final boolean mAnimationEnabled;
@@ -547,6 +552,7 @@
@Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
@NonNull String clickableIdExtra,
@Nullable LoggingUtils loggingUtils,
+ @NonNull InflaterStatsLogger inflaterStatsLogger,
boolean animationEnabled,
boolean allowLayoutChangingBindsWithoutDefault,
boolean applyFontVariantBodyAsDefault) {
@@ -562,6 +568,7 @@
this.mAllowLayoutChangingBindsWithoutDefault = allowLayoutChangingBindsWithoutDefault;
this.mClickableIdExtra = clickableIdExtra;
this.mLoggingUtils = loggingUtils;
+ this.mInflaterStatsLogger = inflaterStatsLogger;
this.mExtensionViewProvider = extensionViewProvider;
this.mApplyFontVariantBodyAsDefault = applyFontVariantBodyAsDefault;
}
@@ -638,6 +645,12 @@
return mLoggingUtils;
}
+ /** Stats logger used for telemetry. */
+ @NonNull
+ public InflaterStatsLogger getInflaterStatsLogger() {
+ return mInflaterStatsLogger;
+ }
+
/** View provider for the renderer extension. */
@Nullable
public ProtoLayoutExtensionViewProvider getExtensionViewProvider() {
@@ -678,6 +691,7 @@
@Nullable private String mClickableIdExtra;
@Nullable private LoggingUtils mLoggingUtils;
+ @Nullable private InflaterStatsLogger mInflaterStatsLogger;
@Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider = null;
@@ -788,6 +802,14 @@
return this;
}
+ /** Sets the stats logger used for telemetry. */
+ @NonNull
+ public Builder setInflaterStatsLogger(
+ @NonNull InflaterStatsLogger inflaterStatsLogger) {
+ this.mInflaterStatsLogger = inflaterStatsLogger;
+ return this;
+ }
+
/**
* Sets whether a "layout changing" data bind can be applied without the
* "value_for_layout" field being filled in, or being set to zero / empty. Defaults to
@@ -834,7 +856,11 @@
if (mClickableIdExtra == null) {
mClickableIdExtra = DEFAULT_CLICKABLE_ID_EXTRA;
}
-
+ if (mInflaterStatsLogger == null) {
+ mInflaterStatsLogger =
+ new NoOpProviderStatsLogger("No implementation was provided")
+ .createInflaterStatsLogger();
+ }
return new Config(
mUiContext,
mLayout,
@@ -847,6 +873,7 @@
mExtensionViewProvider,
checkNotNull(mClickableIdExtra),
mLoggingUtils,
+ mInflaterStatsLogger,
mAnimationEnabled,
mAllowLayoutChangingBindsWithoutDefault,
mApplyFontVariantBodyAsDefault);
@@ -873,6 +900,7 @@
config.getAllowLayoutChangingBindsWithoutDefault();
this.mClickableIdExtra = config.getClickableIdExtra();
this.mLoggingUtils = config.getLoggingUtils();
+ this.mInflaterStatsLogger = config.getInflaterStatsLogger();
this.mExtensionViewProvider = config.getExtensionViewProvider();
this.mApplyFontVariantBodyAsDefault = config.getApplyFontVariantBodyAsDefault();
}
@@ -1731,7 +1759,9 @@
if (modifiers.hasTransformation()) {
applyTransformation(
- wrapper == null ? view : wrapper, modifiers.getTransformation(), posId,
+ wrapper == null ? view : wrapper,
+ modifiers.getTransformation(),
+ posId,
pipelineMaker);
}
@@ -2565,8 +2595,7 @@
}
@Override
- public void onViewDetachedFromWindow(@NonNull View v) {
- }
+ public void onViewDetachedFromWindow(@NonNull View v) {}
});
}
@@ -3152,7 +3181,7 @@
* to the image view; otherwise returns null to indicate the failure of setting drawable.
*/
@Nullable
- private static Drawable setImageDrawable(
+ private Drawable setImageDrawable(
ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
try {
return setImageDrawable(imageView, drawableFuture.get(), protoResId);
@@ -3169,8 +3198,10 @@
* null to indicate the failure of setting drawable.
*/
@Nullable
- private static Drawable setImageDrawable(
- ImageView imageView, Drawable drawable, String protoResId) {
+ private Drawable setImageDrawable(ImageView imageView, Drawable drawable, String protoResId) {
+ if (drawable != null) {
+ mInflaterStatsLogger.logDrawableUsage(drawable);
+ }
if (drawable instanceof BitmapDrawable
&& ((BitmapDrawable) drawable).getBitmap().getByteCount()
> DEFAULT_MAX_BITMAP_RAW_SIZE) {
@@ -3300,7 +3331,7 @@
Log.w(
TAG,
"ArcLine length's value_for_layout is not a positive value. Element"
- + " won't be visible.");
+ + " won't be visible.");
}
sizeWrapper.setSweepAngleDegrees(sizeForLayout);
sizedLp.setAngularAlignment(
@@ -4079,8 +4110,9 @@
Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
if (dpProp.hasDynamicValue() && pipelineMaker.isPresent()) {
try {
- pipelineMaker.get().addPipelineFor(dpProp, dpProp.getValue(), posId,
- dynamicValueConsumer);
+ pipelineMaker
+ .get()
+ .addPipelineFor(dpProp, dpProp.getValue(), posId, dynamicValueConsumer);
} catch (RuntimeException ex) {
Log.e(TAG, "Error building pipeline", ex);
staticValueConsumer.accept(dpProp.getValue());
@@ -4143,7 +4175,9 @@
pipelineMaker
.get()
.addPipelineFor(
- floatProp.getDynamicValue(), floatProp.getValue(), posId,
+ floatProp.getDynamicValue(),
+ floatProp.getValue(),
+ posId,
dynamicValueconsumer);
} catch (RuntimeException ex) {
Log.e(TAG, "Error building pipeline", ex);
@@ -4548,7 +4582,7 @@
/** Apply the mutation that was previously computed with {@link #computeMutation}. */
@UiThread
@NonNull
- public ListenableFuture<Void> applyMutation(
+ public ListenableFuture<RenderingArtifact> applyMutation(
@NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
RenderedMetadata prevRenderedMetadata = getRenderedMetadata(prevInflatedParent);
if (prevRenderedMetadata != null
@@ -4561,11 +4595,11 @@
}
if (groupMutation.isNoOp()) {
// Nothing to do.
- return immediateVoidFuture();
+ return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
}
if (groupMutation.mPipelineMaker.isPresent()) {
- SettableFuture<Void> result = SettableFuture.create();
+ SettableFuture<RenderingArtifact> result = SettableFuture.create();
groupMutation
.mPipelineMaker
.get()
@@ -4575,7 +4609,7 @@
() -> {
try {
applyMutationInternal(prevInflatedParent, groupMutation);
- result.set(null);
+ result.set(RenderingArtifact.create(mInflaterStatsLogger));
} catch (ViewMutationException ex) {
result.setException(ex);
}
@@ -4584,7 +4618,7 @@
} else {
try {
applyMutationInternal(prevInflatedParent, groupMutation);
- return immediateVoidFuture();
+ return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
} catch (ViewMutationException ex) {
return immediateFailedFuture(ex);
}
@@ -4593,6 +4627,7 @@
private void applyMutationInternal(
@NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
+ mInflaterStatsLogger.logMutationChangedNodes(groupMutation.mInflatedViews.size());
for (InflatedView inflatedView : groupMutation.mInflatedViews) {
String posId = inflatedView.getTag();
if (posId == null) {
@@ -4620,15 +4655,21 @@
}
// Remove the touch delegate to the view to be updated
if (immediateParent.getTouchDelegate() != null) {
- ((TouchDelegateComposite) immediateParent.getTouchDelegate())
- .removeDelegate(viewToUpdate);
+ TouchDelegateComposite delegateComposite =
+ (TouchDelegateComposite) immediateParent.getTouchDelegate();
+ delegateComposite.removeDelegate(viewToUpdate);
// Make sure to remove the touch delegate when the actual clickable view is wrapped,
// for example ImageView inside the RatioViewWrapper
if (viewToUpdate instanceof ViewGroup
&& ((ViewGroup) viewToUpdate).getChildCount() > 0) {
- ((TouchDelegateComposite) immediateParent.getTouchDelegate())
- .removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+ delegateComposite.removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+ }
+
+ // If no more touch delegate left in the composite, remove it completely from the
+ // parent
+ if (delegateComposite.isEmpty()) {
+ immediateParent.setTouchDelegate(null);
}
}
immediateParent.removeViewAt(childIndex);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
index e6bf97c..6184ac8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
@@ -83,6 +83,10 @@
mDelegates.remove(delegateView);
}
+ boolean isEmpty() {
+ return mDelegates.isEmpty();
+ }
+
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
boolean eventForwarded = false;
@@ -125,7 +129,7 @@
@Override
@NonNull
public AccessibilityNodeInfo.TouchDelegateInfo getTouchDelegateInfo() {
- if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+ if (VERSION.SDK_INT >= VERSION_CODES.Q && !mDelegates.isEmpty()) {
Map<Region, View> targetMap = new ArrayMap<>(mDelegates.size());
for (Map.Entry<View, DelegateInfo> entry : mDelegates.entrySet()) {
AccessibilityNodeInfo.TouchDelegateInfo info =
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index 6db2dfb..d095d58 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -53,6 +53,7 @@
import androidx.wear.protolayout.expression.pipeline.StateStore;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.ResourceProto.Resources;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
import androidx.wear.protolayout.renderer.helper.TestDsl.LayoutNode;
import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config;
@@ -97,8 +98,8 @@
@Test
public void adaptiveUpdateRatesDisabled_attach_reinflatesCompletely() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -108,7 +109,7 @@
assertThat(layout1).hasSize(1);
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -120,8 +121,8 @@
@Test
public void adaptiveUpdateRatesEnabled_attach_appliesDiffOnly() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -131,7 +132,7 @@
assertThat(layout1).hasSize(1);
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -144,8 +145,8 @@
@Test
public void reattach_usesCachedLayoutForDiffUpdate() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -156,7 +157,7 @@
mInstanceUnderTest.detach(mRootContainer);
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -172,8 +173,8 @@
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
// First one that does the full layout update.
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
@@ -184,7 +185,7 @@
// Second one that applies mutation only.
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
// Detach so it can't apply update.
mInstanceUnderTest.detach(mRootContainer);
@@ -200,9 +201,8 @@
// Render the first layout.
Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
- layout1, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -227,9 +227,7 @@
// not changed part of the layout was also changed in inflated View.
Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
- result =
- mInstanceUnderTest.renderAndAttach(
- layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
// Make sure future is computing result.
assertThat(result.isDone()).isFalse();
@@ -252,8 +250,8 @@
// Render the first layout.
Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -263,7 +261,7 @@
assertThat(findViewsWithText(mRootContainer, TEXT2)).hasSize(1);
Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
- result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
// Make sure future is computing result.
assertThat(result.isDone()).isFalse();
shadowOf(Looper.getMainLooper()).idle();
@@ -279,13 +277,13 @@
public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsPreviousLayout() {
FrameLayout container = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result1 =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result1 =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container);
assertThat(result1.isDone()).isFalse();
- ListenableFuture<Void> result2 =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result2 =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
shadowOf(Looper.getMainLooper()).idle();
@@ -301,14 +299,14 @@
FrameLayout container1 = new FrameLayout(mApplicationContext);
FrameLayout container2 = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container1);
assertThrows(
IllegalStateException.class,
() ->
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container2));
shadowOf(Looper.getMainLooper()).idle();
@@ -321,12 +319,14 @@
FrameLayout container1 = new FrameLayout(mApplicationContext);
FrameLayout container2 = new FrameLayout(mApplicationContext);
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result1 =
- mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container1);
+ ListenableFuture<RenderingArtifact> result1 =
+ mInstanceUnderTest.renderLayoutAndAttach(
+ layout(text(TEXT1)), RESOURCES, container1);
mInstanceUnderTest.detach(container1);
- ListenableFuture<Void> result2 =
- mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container2);
+ ListenableFuture<RenderingArtifact> result2 =
+ mInstanceUnderTest.renderLayoutAndAttach(
+ layout(text(TEXT1)), RESOURCES, container2);
shadowOf(Looper.getMainLooper()).idle();
assertThat(result1.isCancelled()).isTrue();
@@ -341,13 +341,13 @@
throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
- result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -360,14 +360,14 @@
Layout layout1 = layout(text(TEXT1));
Layout layout2 = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
// Make sure we have an UnchangedRenderResult
- result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -377,7 +377,7 @@
assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
shadowOf(Looper.getMainLooper()).idle();
- result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
assertThat(result.isDone()).isTrue();
assertNoException(result);
@@ -388,8 +388,8 @@
public void fullInflationResultCanBeReused() throws Exception {
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
Layout layout = layout(text(TEXT1));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -397,7 +397,7 @@
ListenableFuture<?> renderFuture = mInstanceUnderTest.mRenderFuture;
mInstanceUnderTest.detach(mRootContainer);
- result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -409,15 +409,15 @@
throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
List<View> textViews1 = findViewsWithText(mRootContainer, TEXT1);
assertThat(textViews1).hasSize(1);
mInstanceUnderTest.close();
- result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
shadowOf(Looper.getMainLooper()).idle();
@@ -431,8 +431,8 @@
public void detach_clearsHostView() throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -449,14 +449,14 @@
Layout layout2 = layout(text(TEXT1));
Resources resources2 = Resources.newBuilder().setVersion("2").build();
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
- result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -472,15 +472,15 @@
Layout layout2 = layout(text(TEXT1));
Resources resources2 = Resources.newBuilder().setVersion("1").build();
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
mInstanceUnderTest.invalidateCache();
- result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+ result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
@@ -490,13 +490,28 @@
}
@Test
+ public void invalidateCache_ongoingInflation_oldInflationGetsCancelled() throws Exception {
+ Layout layout1 = layout(text(TEXT1));
+ Resources resources1 = Resources.newBuilder().setVersion("1").build();
+ setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
+
+ mInstanceUnderTest.invalidateCache();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(result.isCancelled()).isTrue();
+ assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
+ }
+
+ @Test
public void adaptiveUpdateRatesEnabled_rootElementdiff_keepsElementCentered() throws Exception {
int dimension = 50;
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
// Full inflation.
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(
column(
props -> {
@@ -518,7 +533,7 @@
// Diff update only for the root element.
result =
- mInstanceUnderTest.renderAndAttach(
+ mInstanceUnderTest.renderLayoutAndAttach(
layout(
column(
props -> {
@@ -546,8 +561,8 @@
public void close_clearsHostView() throws Exception {
Layout layout = layout(text(TEXT1));
setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -562,7 +577,9 @@
setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
assertThrows(
ExecutionException.class,
- () -> renderAndAttachLayout(layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
+ () ->
+ renderLayoutAndAttachLayout(
+ layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
}
@Test
@@ -573,8 +590,8 @@
for (int i = 0; i < children.length; i++) {
children[i] = recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1);
}
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
// MAX_LAYOUT_ELEMENT_DEPTH branches of depth MAX_LAYOUT_ELEMENT_DEPTH - 1.
// Total depth is MAX_LAYOUT_ELEMENT_DEPTH (if we count the head).
layout(box(children)), RESOURCES, mRootContainer);
@@ -591,7 +608,7 @@
assertThrows(
ExecutionException.class,
() ->
- renderAndAttachLayout(
+ renderLayoutAndAttachLayout(
// Total number of views is = MAX_LAYOUT_ELEMENT_DEPTH + 1 (span
// text)
layout(
@@ -599,8 +616,8 @@
MAX_LAYOUT_ELEMENT_DEPTH,
spannable(spanText("Hello"))))));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
// Total number of views is = (MAX_LAYOUT_ELEMENT_DEPTH -1) + 1 (span text)
layout(
recursiveBox(
@@ -620,12 +637,12 @@
assertThrows(
ExecutionException.class,
() ->
- renderAndAttachLayout(
+ renderLayoutAndAttachLayout(
// Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH)
layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH))))));
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(
// Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH - 1)
// = MAX_LAYOUT_ELEMENT_DEPTH
layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1)))),
@@ -637,9 +654,9 @@
assertThat(mRootContainer.getChildCount()).isEqualTo(1);
}
- private void renderAndAttachLayout(Layout layout) throws Exception {
- ListenableFuture<Void> result =
- mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+ private void renderLayoutAndAttachLayout(Layout layout) throws Exception {
+ ListenableFuture<RenderingArtifact> result =
+ mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
shadowOf(Looper.getMainLooper()).idle();
assertNoException(result);
}
@@ -690,7 +707,8 @@
return views;
}
- private static void assertNoException(ListenableFuture<Void> result) throws Exception {
+ private static void assertNoException(ListenableFuture<RenderingArtifact> result)
+ throws Exception {
// Assert that result hasn't thrown exception.
result.get();
}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index da07985..1a46140 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -83,7 +83,6 @@
import androidx.core.content.ContextCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.expression.AppDataKey;
import androidx.wear.protolayout.expression.DynamicBuilders;
import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
@@ -203,6 +202,8 @@
import androidx.wear.protolayout.proto.TypesProto.StringProp;
import androidx.wear.protolayout.protobuf.ByteString;
import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
import androidx.wear.protolayout.renderer.helper.TestFingerprinter;
import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -222,6 +223,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowChoreographer;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPackageManager;
@@ -543,37 +545,41 @@
int width = 10;
int height = 12;
byte[] payload = "Hello World".getBytes(UTF_8);
+ LayoutElement extension =
+ LayoutElement.newBuilder()
+ .setExtension(
+ ExtensionLayoutElement.newBuilder()
+ .setExtensionId("foo")
+ .setPayload(ByteString.copyFrom(payload))
+ .setWidth(
+ ExtensionDimension.newBuilder()
+ .setLinearDimension(dp(width))
+ .build())
+ .setHeight(
+ ExtensionDimension.newBuilder()
+ .setLinearDimension(dp(height))
+ .build()))
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
- .setBox(
- Box.newBuilder()
- // Outer box's width and height left at default value of "wrap"
- .addContents(
- LayoutElement.newBuilder()
- .setExtension(
- ExtensionLayoutElement.newBuilder()
- .setExtensionId("foo")
- .setPayload(ByteString.copyFrom(payload))
- .setWidth(
- ExtensionDimension.newBuilder()
- .setLinearDimension(dp(width))
- .build())
- .setHeight(
- ExtensionDimension.newBuilder()
- .setLinearDimension(dp(height))
- .build()))))
+ .setBox(
+ Box.newBuilder()
+ // Outer box's width and height left at default value of
+ // "wrap"
+ .addContents(extension))
.build();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root))
- .setExtensionViewProvider(
- (extensionPayload, id) -> {
- TextView returnedView = new TextView(getApplicationContext());
- returnedView.setText("testing");
+ newRendererConfigBuilder(fingerprintedLayout(root))
+ .setExtensionViewProvider(
+ (extensionPayload, id) -> {
+ TextView returnedView =
+ new TextView(getApplicationContext());
+ returnedView.setText("testing");
- return returnedView;
- }))
+ return returnedView;
+ }))
.inflate();
// Check that the outer box is displayed and it has a child.
@@ -976,6 +982,23 @@
// A column with a row (Spacer + Spacer) and Spacer, everything has weighted expand
// dimension.
+
+ Row rowWithSpacers =
+ Row.newBuilder()
+ .setWidth(expand())
+ .setHeight(
+ ContainerDimension.newBuilder()
+ .setExpandedDimension(expandWithWeight(heightWeight1))
+ .build())
+ .addContents(
+ LayoutElement.newBuilder()
+ .setSpacer(
+ buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
+ .addContents(
+ LayoutElement.newBuilder()
+ .setSpacer(
+ buildExpandedSpacer(widthWeight2, DEFAULT_WEIGHT)))
+ .build();
LayoutElement root =
LayoutElement.newBuilder()
.setColumn(
@@ -983,26 +1006,13 @@
.setWidth(expand())
.setHeight(expand())
.addContents(
- LayoutElement.newBuilder()
- .setRow(
- Row.newBuilder()
- .setWidth(expand())
- .setHeight(
- ContainerDimension.newBuilder()
- .setExpandedDimension(expandWithWeight(heightWeight1))
- .build())
- .addContents(
- LayoutElement.newBuilder()
- .setSpacer(
- buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
- .addContents(
- LayoutElement.newBuilder()
- .setSpacer(
- buildExpandedSpacer(
- widthWeight2, DEFAULT_WEIGHT)))))
+ LayoutElement.newBuilder().setRow(rowWithSpacers))
.addContents(
LayoutElement.newBuilder()
- .setSpacer(buildExpandedSpacer(DEFAULT_WEIGHT, heightWeight2)))
+ .setSpacer(
+ buildExpandedSpacer(
+ DEFAULT_WEIGHT,
+ heightWeight2)))
.build())
.build();
@@ -1034,9 +1044,9 @@
.setExpandedDimension(
ExpandedDimensionProp
.getDefaultInstance()))
- .setWidth(SpacerDimension
- .newBuilder()
- .setLinearDimension(dp(width))))
+ .setWidth(
+ SpacerDimension.newBuilder()
+ .setLinearDimension(dp(width))))
.build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
@@ -1543,17 +1553,17 @@
ContainerDimension.newBuilder().setLinearDimension(dp(childSize)).build();
LayoutElement childBox =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(childBoxSize)
- .setHeight(childBoxSize)
- .setModifiers(
- Modifiers.newBuilder()
- .setClickable(
- Clickable.newBuilder()
- .setId("foo")
- .setOnClick(
- action))))
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(childBoxSize)
+ .setHeight(childBoxSize)
+ .setModifiers(
+ Modifiers.newBuilder()
+ .setClickable(
+ Clickable.newBuilder()
+ .setId("foo")
+ .setOnClick(action))))
.build();
LayoutElement root =
@@ -1563,13 +1573,14 @@
.setWidth(parentBoxSize)
.setHeight(parentBoxSize)
.addContents(childBox))
- .build();
+ .build();
State.Builder receivedState = State.newBuilder();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
- .setLoadActionListener(receivedState::mergeFrom))
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
.inflate();
shadowOf(Looper.getMainLooper()).idle();
@@ -1627,8 +1638,7 @@
.setWidth(
SpacerDimension.newBuilder()
.setLinearDimension(dp(spacerSize))
- .build()
- ));
+ .build()));
// |--clickable area child box 1 (5 - 35)--|
// |---clickable area child box 2 (30-60)--|
@@ -1647,8 +1657,7 @@
dp(clickTargetSize))
.setMinimumClickableHeight(
dp(clickTargetSize))
- .setOnClick(
- action)
+ .setOnClick(action)
.setId("foo1"))))
.build();
@@ -1666,8 +1675,7 @@
dp(clickTargetSize))
.setMinimumClickableHeight(
dp(clickTargetSize))
- .setOnClick(
- action)
+ .setOnClick(action)
.setId("foo2"))))
.build();
@@ -1692,8 +1700,9 @@
State.Builder receivedState = State.newBuilder();
FrameLayout rootLayout =
renderer(
- newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
- .setLoadActionListener(receivedState::mergeFrom))
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
.inflate();
ShadowLooper.runUiThreadTasks();
@@ -1812,8 +1821,8 @@
// Compute the mutation
ViewGroupMutation mutation =
- renderer.computeMutation(getRenderedMetadata(rootLayout),
- fingerprintedLayout(root2));
+ renderer.computeMutation(
+ getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
assertThat(mutation).isNotNull();
assertThat(mutation.isNoOp()).isFalse();
@@ -1848,8 +1857,9 @@
.setSpacer(
Spacer.newBuilder()
.setWidth(
- SpacerDimension.newBuilder().setLinearDimension(
- dp(spacerSize)).build()));
+ SpacerDimension.newBuilder()
+ .setLinearDimension(dp(spacerSize))
+ .build()));
int parentHeight = 45;
int parentWidth = 125;
@@ -1916,13 +1926,13 @@
// Compute the mutation
ViewGroupMutation mutation =
- renderer.computeMutation(getRenderedMetadata(rootLayout),
- fingerprintedLayout(root2));
+ renderer.computeMutation(
+ getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
assertThat(mutation).isNotNull();
assertThat(mutation.isNoOp()).isFalse();
// Apply the mutation
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(rootLayout, mutation);
shadowOf(getMainLooper()).idle();
try {
@@ -1958,6 +1968,108 @@
}
@Test
+ @Config(minSdk = VERSION_CODES.Q)
+ public void inflateThenMutate_withClickableSizeChange_clickableModifier_extendClickTargetSize()
+ {
+ Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+ int parentSize = 50;
+ ContainerDimension parentBoxSize =
+ ContainerDimension.newBuilder().setLinearDimension(dp(parentSize)).build();
+ ContainerDimension childBoxSize =
+ ContainerDimension.newBuilder().setLinearDimension(dp(parentSize / 2f)).build();
+
+ Modifiers testModifiers1 =
+ Modifiers.newBuilder()
+ .setClickable(Clickable.newBuilder().setOnClick(action).setId("foo1"))
+ .build();
+
+ // Child box has a size smaller than the minimum clickable size, touch delegation is
+ // required.
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(parentBoxSize)
+ .setHeight(parentBoxSize)
+ .addContents(
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(childBoxSize)
+ .setHeight(childBoxSize)
+ .setModifiers(
+ testModifiers1))))
+ .build();
+
+ State.Builder receivedState = State.newBuilder();
+ Renderer renderer =
+ renderer(
+ newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom));
+ FrameLayout rootLayout = renderer.inflate();
+ ViewGroup parent = (ViewGroup) rootLayout.getChildAt(0);
+ // Confirm the touch delegation has happened.
+ assertThat(parent.getTouchDelegate()).isNotNull();
+ // Dispatch a click event to the parent View within the expanded clickable area;
+ // it should trigger the LoadAction...
+ receivedState.clearLastClickableId();
+ dispatchTouchEvent(parent, 5, 5);
+ expect.that(receivedState.getLastClickableId()).isEqualTo("foo1");
+
+ // Produce a new layout with child box specifies its minimum clickable size, NO touch
+ // delegation is required.
+ Modifiers testModifiers2 =
+ Modifiers.newBuilder()
+ .setClickable(
+ Clickable.newBuilder()
+ .setOnClick(action)
+ .setId("foo2")
+ .setMinimumClickableWidth(dp(parentSize / 2f))
+ .setMinimumClickableHeight(dp(parentSize / 2f)))
+ .build();
+ LayoutElement root2 =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(parentBoxSize)
+ .setHeight(parentBoxSize)
+ .addContents(
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(childBoxSize)
+ .setHeight(childBoxSize)
+ .setModifiers(
+ testModifiers2))))
+ .build();
+
+ // Compute the mutation
+ ViewGroupMutation mutation =
+ renderer.computeMutation(getRenderedMetadata(rootLayout),
+ fingerprintedLayout(root2));
+ assertThat(mutation).isNotNull();
+ assertThat(mutation.isNoOp()).isFalse();
+
+ // Apply the mutation
+ boolean mutationResult = renderer.applyMutation(rootLayout, mutation);
+ assertThat(mutationResult).isTrue();
+
+ // Verify that the parent removed the touch delegation.
+ // Keep an empty touch delegate composite will lead to failure when calling
+ // {@link TouchDelegateComposite#getTouchDelegateInfo}
+ assertThat(parent.getTouchDelegate()).isNull();
+
+ // Dispatch a click event to the parent View within the expanded clickable area;
+ // it should no longer trigger the LoadAction.
+ receivedState.clearLastClickableId();
+ dispatchTouchEvent(parent, 5, 5);
+ expect.that(receivedState.getLastClickableId()).isEmpty();
+ View box = parent.getChildAt(0);
+ dispatchTouchEvent(box, 1, 1);
+ expect.that(receivedState.getLastClickableId()).isEqualTo("foo2");
+ }
+
+ @Test
public void inflate_clickable_withoutRippleEffect_rippleDrawableNotAdded() throws IOException {
final String textContentsWithRipple = "clickable with ripple";
final String textContentsWithoutRipple = "clickable without ripple";
@@ -1993,15 +2105,15 @@
FrameLayout rootLayout =
renderer(
- fingerprintedLayout(
- LayoutElement.newBuilder()
- .setColumn(
- Column.newBuilder()
- .addContents(textElementWithRipple)
- .addContents(
- textElementWithoutRipple)
- .build())
- .build()))
+ fingerprintedLayout(
+ LayoutElement.newBuilder()
+ .setColumn(
+ Column.newBuilder()
+ .addContents(textElementWithRipple)
+ .addContents(
+ textElementWithoutRipple)
+ .build())
+ .build()))
.inflate();
// Column
@@ -2025,6 +2137,49 @@
}
@Test
+ public void inflate_hiddenModifier_inhibitsClicks() {
+ final String textContents = "I am a clickable";
+
+ Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+
+ LayoutElement root =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .addContents(
+ LayoutElement.newBuilder()
+ .setText(
+ createTextWithVisibility(
+ textContents,
+ "back",
+ action,
+ true)))
+ .addContents(
+ LayoutElement.newBuilder()
+ .setText(
+ createTextWithVisibility(
+ textContents,
+ "front",
+ action,
+ false))))
+ .build();
+
+ State.Builder receivedState = State.newBuilder();
+ FrameLayout rootLayout =
+ renderer(
+ newRendererConfigBuilder(
+ fingerprintedLayout(root), resourceResolvers())
+ .setLoadActionListener(receivedState::mergeFrom))
+ .inflate();
+
+ // Try to tap the stacked clickables.
+ dispatchTouchEvent(rootLayout, 5f, 5f);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ expect.that(receivedState.getLastClickableId()).isEqualTo("back");
+ }
+
+ @Test
public void inflate_arc_withLineDrawnWithArcTo() {
LayoutElement root =
LayoutElement.newBuilder()
@@ -2151,8 +2306,8 @@
@Test
public void inflate_arc_withText_autoSize_notSet() {
int lastSize = 12;
- FontStyle.Builder style = FontStyle.newBuilder()
- .addAllSize(buildSizesList(new int[]{10, 20, lastSize}));
+ FontStyle.Builder style =
+ FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {10, 20, lastSize}));
LayoutElement root =
LayoutElement.newBuilder()
.setArc(
@@ -2889,12 +3044,13 @@
.setFontStyle(FontStyle.newBuilder().addSize(sp(16)))
.setMaxLines(Int32Prop.newBuilder().setValue(6))
.setOverflow(
- TextOverflowProp.newBuilder().setValue(
- TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+ TextOverflowProp.newBuilder()
+ .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
Layout layout1 =
fingerprintedLayout(
LayoutElement.newBuilder()
- .setBox(buildFixedSizeBoxWIthText(text1)).build());
+ .setBox(buildFixedSizeBoxWIthText(text1))
+ .build());
Text.Builder text2 =
Text.newBuilder()
@@ -2904,18 +3060,19 @@
.setFontStyle(FontStyle.newBuilder().addSize(sp(4)))
.setMaxLines(Int32Prop.newBuilder().setValue(6))
.setOverflow(
- TextOverflowProp.newBuilder().setValue(
- TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+ TextOverflowProp.newBuilder()
+ .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
Layout layout2 =
fingerprintedLayout(
LayoutElement.newBuilder()
- .setBox(buildFixedSizeBoxWIthText(text2)).build());
+ .setBox(buildFixedSizeBoxWIthText(text2))
+ .build());
// Initial layout.
Renderer renderer = renderer(layout1);
ViewGroup inflatedViewParent = renderer.inflate();
- TextView textView1 = (TextView) ((ViewGroup) inflatedViewParent
- .getChildAt(0)).getChildAt(0);
+ TextView textView1 =
+ (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
// Apply the mutation.
ViewGroupMutation mutation =
@@ -2926,8 +3083,8 @@
assertThat(mutationResult).isTrue();
// This contains layout after the mutation.
- TextView textView2 = (TextView) ((ViewGroup) inflatedViewParent
- .getChildAt(0)).getChildAt(0);
+ TextView textView2 =
+ (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
expect.that(textView1.getEllipsize()).isEqualTo(TruncateAt.END);
expect.that(textView1.getMaxLines()).isEqualTo(2);
@@ -3028,7 +3185,7 @@
@Test
public void inflate_textView_autosize_set() {
String text = "Test text";
- int[] presetSizes = new int[]{12, 20, 10};
+ int[] presetSizes = new int[] {12, 20, 10};
List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
LayoutElement textElement =
@@ -3036,16 +3193,16 @@
.setText(
Text.newBuilder()
.setText(string(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3076,16 +3233,16 @@
Text.newBuilder()
.setText(string(text))
.setMaxLines(Int32Prop.newBuilder().setValue(4))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3099,23 +3256,23 @@
public void inflate_textView_autosize_notSet() {
String text = "Test text";
int size = 24;
- List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size});
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {size});
LayoutElement textElement =
LayoutElement.newBuilder()
.setText(
Text.newBuilder()
.setText(string(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3129,23 +3286,23 @@
public void inflate_textView_autosize_setDynamic_noop() {
String text = "Test text";
int lastSize = 24;
- List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize});
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {10, 30, lastSize});
LayoutElement textElement =
LayoutElement.newBuilder()
.setText(
Text.newBuilder()
.setText(dynamicString(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ArrayList<View> textChildren = new ArrayList<>();
@@ -3159,23 +3316,23 @@
@Test
public void inflate_textView_autosize_wrongSizes_noop() {
String text = "Test text";
- List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{0, -2, 0});
+ List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {0, -2, 0});
LayoutElement textElement =
LayoutElement.newBuilder()
.setText(
Text.newBuilder()
.setText(string(text))
- .setFontStyle(
- FontStyle.newBuilder()
- .addAllSize(sizes)))
+ .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
.build();
LayoutElement root =
- LayoutElement.newBuilder().setBox(
- Box.newBuilder()
- .setWidth(expand())
- .setHeight(expand())
- .addContents(textElement)).build();
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(expand())
+ .addContents(textElement))
+ .build();
FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
ArrayList<View> textChildren = new ArrayList<>();
@@ -3219,8 +3376,8 @@
public void inflate_spantext_ignoresMultipleSizes() {
String text = "Test text";
int firstSize = 12;
- FontStyle.Builder style = FontStyle.newBuilder()
- .addAllSize(buildSizesList(new int[]{firstSize, 10, 20}));
+ FontStyle.Builder style =
+ FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {firstSize, 10, 20}));
LayoutElement root =
LayoutElement.newBuilder()
.setSpannable(
@@ -4780,7 +4937,7 @@
boolean applyMutation(ViewGroup parent, ViewGroupMutation mutation) {
try {
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
mRenderer.applyMutation(parent, mutation);
shadowOf(Looper.getMainLooper()).idle();
applyMutationFuture.get();
@@ -5000,33 +5157,25 @@
LayoutElement image = buildImage(protoResId, 30, 30);
-
- BoolProp.Builder stateBoolPropBuilder = BoolProp
- .newBuilder()
- .setValue(
- true)
- .setDynamicValue(
- DynamicBool
- .newBuilder()
- .setStateSource(
- StateBoolSource
- .newBuilder()
- .setSourceKey(
- boolKey)));
- LayoutElement.Builder boxBuilder = LayoutElement.newBuilder()
- .setBox(
- Box.newBuilder()
- .addContents(image)
- .setModifiers(
- Modifiers
- .newBuilder()
- .setHidden(stateBoolPropBuilder)));
+ BoolProp.Builder stateBoolPropBuilder =
+ BoolProp.newBuilder()
+ .setValue(true)
+ .setDynamicValue(
+ DynamicBool.newBuilder()
+ .setStateSource(
+ StateBoolSource.newBuilder()
+ .setSourceKey(boolKey)));
+ LayoutElement.Builder boxBuilder =
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .addContents(image)
+ .setModifiers(
+ Modifiers.newBuilder()
+ .setHidden(stateBoolPropBuilder)));
LayoutElement root =
LayoutElement.newBuilder()
- .setRow(
- Row.newBuilder()
- .addContents(boxBuilder)
- .addContents(image))
+ .setRow(Row.newBuilder().addContents(boxBuilder).addContents(image))
.build();
FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
@@ -5059,7 +5208,8 @@
assertThat(secondImage.getLeft()).isEqualTo(secondImageLeft);
}
- @Test public void inflate_box_withVisibleModifier() {
+ @Test
+ public void inflate_box_withVisibleModifier() {
final String protoResId = "android";
final String boolKey = "bool-key";
@@ -5226,20 +5376,18 @@
ContainerDimension.newBuilder().setLinearDimension(dp(100.f).build()).build();
ContainerDimension innerBoxSize =
ContainerDimension.newBuilder().setLinearDimension(dp(60.f).build()).build();
- Box.Builder boxBuilder = Box.newBuilder()
- .setWidth(expand())
- .setHeight(wrap())
- .setModifiers(
- Modifiers.newBuilder()
- .setTransformation(
- transformation)
- .build())
- .addContents(
- LayoutElement.newBuilder()
- .setBox(
- Box.newBuilder()
- .setWidth(innerBoxSize)
- .setHeight(innerBoxSize)));
+ Box.Builder boxBuilder =
+ Box.newBuilder()
+ .setWidth(expand())
+ .setHeight(wrap())
+ .setModifiers(
+ Modifiers.newBuilder().setTransformation(transformation).build())
+ .addContents(
+ LayoutElement.newBuilder()
+ .setBox(
+ Box.newBuilder()
+ .setWidth(innerBoxSize)
+ .setHeight(innerBoxSize)));
LayoutElement root =
LayoutElement.newBuilder()
.setBox(
@@ -5657,7 +5805,7 @@
renderer.computeMutation(
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(textFadeIn("World")));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
// Idle for running code for starting animations.
@@ -5689,7 +5837,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 0)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5715,7 +5863,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 1)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5738,7 +5886,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 1)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5764,7 +5912,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5792,7 +5940,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idle();
@@ -5821,7 +5969,7 @@
fingerprintedLayout(
getMultipleTextElementWithExitAnimation(
Arrays.asList("Hello"), /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5850,7 +5998,7 @@
getRenderedMetadata(inflatedViewParent),
fingerprintedLayout(
getTextElementWithExitAnimation("World", /* iterations= */ 10)));
- ListenableFuture<Void> applyMutationFuture =
+ ListenableFuture<RenderingArtifact> applyMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5863,7 +6011,7 @@
getTextElementWithExitAnimation(
"Second mutation", /* iterations= */ 10)));
- ListenableFuture<Void> applySecondMutationFuture =
+ ListenableFuture<RenderingArtifact> applySecondMutationFuture =
renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation);
// the previous mutation should be finished
@@ -6222,9 +6370,12 @@
private static Spacer.Builder buildExpandedSpacer(int widthWeight, int heightWeight) {
return Spacer.newBuilder()
- .setWidth(SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(widthWeight)))
+ .setWidth(
+ SpacerDimension.newBuilder()
+ .setExpandedDimension(expandWithWeight(widthWeight)))
.setHeight(
- SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(heightWeight)));
+ SpacerDimension.newBuilder()
+ .setExpandedDimension(expandWithWeight(heightWeight)));
}
private static ExpandedDimensionProp expandWithWeight(int weight) {
@@ -6255,4 +6406,15 @@
.addContents(LayoutElement.newBuilder().setSpacer(spacer)))
.build());
}
+
+ private static Text createTextWithVisibility(
+ String text, String id, Action action, boolean visibility) {
+ return Text.newBuilder()
+ .setText(string(text))
+ .setModifiers(
+ Modifiers.newBuilder()
+ .setVisible(BoolProp.newBuilder().setValue(visibility))
+ .setClickable(Clickable.newBuilder().setId(id).setOnClick(action)))
+ .build();
+ }
}