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