1 /*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.compose.animation.scene
18
19 import androidx.compose.runtime.Stable
20 import androidx.compose.ui.Modifier
21 import androidx.compose.ui.geometry.Offset
22 import androidx.compose.ui.geometry.Size
23 import androidx.compose.ui.graphics.BlendMode
24 import androidx.compose.ui.graphics.Color
25 import androidx.compose.ui.graphics.CompositingStrategy
26 import androidx.compose.ui.graphics.Outline
27 import androidx.compose.ui.graphics.RectangleShape
28 import androidx.compose.ui.graphics.Shape
29 import androidx.compose.ui.graphics.drawOutline
30 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
31 import androidx.compose.ui.graphics.drawscope.DrawScope
32 import androidx.compose.ui.graphics.drawscope.translate
33 import androidx.compose.ui.layout.LayoutCoordinates
34 import androidx.compose.ui.layout.Measurable
35 import androidx.compose.ui.layout.MeasureResult
36 import androidx.compose.ui.layout.MeasureScope
37 import androidx.compose.ui.node.DelegatingNode
38 import androidx.compose.ui.node.DrawModifierNode
39 import androidx.compose.ui.node.GlobalPositionAwareModifierNode
40 import androidx.compose.ui.node.LayoutModifierNode
41 import androidx.compose.ui.node.ModifierNodeElement
42 import androidx.compose.ui.unit.Constraints
43 import androidx.compose.ui.unit.LayoutDirection
44 import androidx.compose.ui.unit.toSize
45
46 /**
47 * Punch a hole in this node with the given [size], [offset] and [shape].
48 *
49 * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
50 * This can be used to make content drawn below an opaque element visible. For example, if we have
51 * [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
52 * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big clock
53 * time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be the
54 * result.
55 */
56 @Stable
Modifiernull57 fun Modifier.punchHole(
58 size: () -> Size,
59 offset: () -> Offset,
60 shape: Shape = RectangleShape,
61 ): Modifier = this.then(PunchHoleElement(size, offset, shape))
62
63 /**
64 * Punch a hole in this node using the bounds of [coords] and the given [shape].
65 *
66 * You can use [androidx.compose.ui.layout.onGloballyPositioned] to get the last coordinates of a
67 * node.
68 */
69 @Stable
70 fun Modifier.punchHole(
71 coords: () -> LayoutCoordinates?,
72 shape: Shape = RectangleShape,
73 ): Modifier = this.then(PunchHoleWithBoundsElement(coords, shape))
74
75 private data class PunchHoleElement(
76 private val size: () -> Size,
77 private val offset: () -> Offset,
78 private val shape: Shape,
79 ) : ModifierNodeElement<PunchHoleNode>() {
80 override fun create(): PunchHoleNode = PunchHoleNode(size, offset, { shape })
81
82 override fun update(node: PunchHoleNode) {
83 node.size = size
84 node.offset = offset
85 node.shape = { shape }
86 }
87 }
88
89 private class PunchHoleNode(
90 var size: () -> Size,
91 var offset: () -> Offset,
92 var shape: () -> Shape,
93 ) : Modifier.Node(), DrawModifierNode, LayoutModifierNode {
94 private var lastSize: Size = Size.Unspecified
95 private var lastLayoutDirection: LayoutDirection = LayoutDirection.Ltr
96 private var lastOutline: Outline? = null
97
measurenull98 override fun MeasureScope.measure(
99 measurable: Measurable,
100 constraints: Constraints
101 ): MeasureResult {
102 return measurable.measure(constraints).run {
103 layout(width, height) {
104 placeWithLayer(0, 0) { compositingStrategy = CompositingStrategy.Offscreen }
105 }
106 }
107 }
108
drawnull109 override fun ContentDrawScope.draw() {
110 drawContent()
111
112 val holeSize = size()
113 if (holeSize != Size.Zero) {
114 val offset = offset()
115 translate(offset.x, offset.y) { drawHole(holeSize) }
116 }
117 }
118
DrawScopenull119 private fun DrawScope.drawHole(size: Size) {
120 if (shape == RectangleShape) {
121 drawRect(Color.Black, size = size, blendMode = BlendMode.DstOut)
122 return
123 }
124
125 val outline =
126 if (size == lastSize && layoutDirection == lastLayoutDirection) {
127 lastOutline!!
128 } else {
129 val newOutline = shape().createOutline(size, layoutDirection, this)
130 lastSize = size
131 lastLayoutDirection = layoutDirection
132 lastOutline = newOutline
133 newOutline
134 }
135
136 drawOutline(
137 outline,
138 Color.Black,
139 blendMode = BlendMode.DstOut,
140 )
141 }
142 }
143
144 private data class PunchHoleWithBoundsElement(
145 private val coords: () -> LayoutCoordinates?,
146 private val shape: Shape,
147 ) : ModifierNodeElement<PunchHoleWithBoundsNode>() {
createnull148 override fun create(): PunchHoleWithBoundsNode = PunchHoleWithBoundsNode(coords, shape)
149
150 override fun update(node: PunchHoleWithBoundsNode) {
151 node.holeCoords = coords
152 node.shape = shape
153 }
154 }
155
156 private class PunchHoleWithBoundsNode(
157 var holeCoords: () -> LayoutCoordinates?,
158 var shape: Shape,
159 ) : DelegatingNode(), DrawModifierNode, GlobalPositionAwareModifierNode {
160 private val delegate = delegate(PunchHoleNode(::holeSize, ::holeOffset, ::shape))
161 private var lastCoords: LayoutCoordinates? = null
162
onGloballyPositionednull163 override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
164 this.lastCoords = coordinates
165 }
166
<lambda>null167 override fun ContentDrawScope.draw() = with(delegate) { draw() }
168
holeSizenull169 private fun holeSize(): Size {
170 return holeCoords()?.size?.toSize() ?: Size.Zero
171 }
172
holeOffsetnull173 private fun holeOffset(): Offset {
174 val holeCoords = holeCoords() ?: return Offset.Zero
175 val lastCoords = lastCoords ?: error("draw() was called before onGloballyPositioned()")
176 return lastCoords.localPositionOf(holeCoords, relativeToSource = Offset.Zero)
177 }
178 }
179