1 /*
<lambda>null2  * Copyright 2020 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 androidx.compose.foundation
18 
19 import androidx.compose.foundation.interaction.FocusInteraction
20 import androidx.compose.foundation.interaction.HoverInteraction
21 import androidx.compose.foundation.interaction.Interaction
22 import androidx.compose.foundation.interaction.InteractionSource
23 import androidx.compose.foundation.interaction.PressInteraction
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.Stable
26 import androidx.compose.runtime.compositionLocalOf
27 import androidx.compose.runtime.remember
28 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.composed
30 import androidx.compose.ui.draw.DrawModifier
31 import androidx.compose.ui.graphics.Color
32 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
33 import androidx.compose.ui.node.DelegatableNode
34 import androidx.compose.ui.node.DelegatingNode
35 import androidx.compose.ui.node.DrawModifierNode
36 import androidx.compose.ui.node.ModifierNodeElement
37 import androidx.compose.ui.node.invalidateDraw
38 import androidx.compose.ui.platform.InspectorInfo
39 import androidx.compose.ui.platform.debugInspectorInfo
40 import kotlinx.coroutines.launch
41 
42 /**
43  * Indication represents visual effects that occur when certain interactions happens. For example:
44  * showing a ripple effect when a component is pressed, or a highlight when a component is focused.
45  *
46  * To implement your own Indication, see [IndicationNodeFactory] - an optimized [Indication] that
47  * allows for more efficient implementations than the deprecated [rememberUpdatedInstance].
48  *
49  * Indication is typically provided throughout the hierarchy through [LocalIndication] - you can
50  * provide a custom Indication to [LocalIndication] to change the default [Indication] used for
51  * components such as [clickable].
52  */
53 @Stable
54 interface Indication {
55 
56     /**
57      * [remember]s a new [IndicationInstance], and updates its state based on [Interaction]s emitted
58      * via [interactionSource] . Typically this will be called by [indication], so one
59      * [IndicationInstance] will be used for one component that draws [Indication], such as a
60      * button.
61      *
62      * Implementations of this function should observe [Interaction]s using [interactionSource],
63      * using them to launch animations / state changes inside [IndicationInstance] that will then be
64      * reflected inside [IndicationInstance.drawIndication].
65      *
66      * @param interactionSource the [InteractionSource] representing the stream of [Interaction]s
67      *   the returned [IndicationInstance] should represent
68      * @return an [IndicationInstance] that represents the stream of [Interaction]s emitted by
69      *   [interactionSource]
70      */
71     @Suppress("DEPRECATION_ERROR")
72     @Deprecated(RememberUpdatedInstanceDeprecationMessage, level = DeprecationLevel.ERROR)
73     @Composable
74     fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance =
75         NoIndicationInstance
76 }
77 
78 /**
79  * IndicationNodeFactory is an Indication that creates [Modifier.Node] instances to render visual
80  * effects that occur when certain interactions happens. For example: showing a ripple effect when a
81  * component is pressed, or a highlight when a component is focused.
82  *
83  * An instance of IndicationNodeFactory is responsible for creating individual nodes on demand for
84  * each component that needs to render indication. IndicationNodeFactory instances should be very
85  * simple - they just hold the relevant configuration properties needed to create the node instances
86  * that are responsible for drawing visual effects.
87  *
88  * IndicationNodeFactory is conceptually similar to [ModifierNodeElement] - it is designed to be
89  * able to be created outside of composition, and re-used in multiple places.
90  *
91  * Indication is typically provided throughout the hierarchy through [LocalIndication] - you can
92  * provide a custom Indication to [LocalIndication] to change the default [Indication] used for
93  * components such as [clickable].
94  */
95 @Stable
96 interface IndicationNodeFactory : Indication {
97     /**
98      * Creates a node that will be applied to a specific component and render indication for the
99      * provided [interactionSource]. This method will be re-invoked for a given layout node if a new
100      * [interactionSource] is provided or if [hashCode] or [equals] change for this
101      * IndicationNodeFactory over time, allowing a new node to be created using the new properties
102      * in this IndicationNodeFactory. If you instead want to gracefully update the existing node
103      * over time, consider replacing those properties with [androidx.compose.runtime.State]
104      * properties, so when the value of the State changes, [equals] and [hashCode] remain the same,
105      * and the same node instance can just query the updated state value.
106      *
107      * The returned [DelegatableNode] should implement [DrawModifierNode], or delegate to a node
108      * that implements [DrawModifierNode], so that it can draw visual effects. Inside
109      * [DrawModifierNode.draw], make sure to call [ContentDrawScope.drawContent] to render the
110      * component in addition to any visual effects.
111      *
112      * @param interactionSource the [InteractionSource] representing the stream of [Interaction]s
113      *   the returned node should render visual effects for
114      * @return a [DelegatableNode] that renders visual effects for the provided [interactionSource]
115      *   by also implementing / delegating to a [DrawModifierNode]
116      */
createnull117     fun create(interactionSource: InteractionSource): DelegatableNode
118 
119     /**
120      * Require hashCode() to be implemented. Using a data class is sufficient. Singletons and
121      * instances with no properties may implement this function by returning an arbitrary constant.
122      */
123     override fun hashCode(): Int
124 
125     /**
126      * Require equals() to be implemented. Using a data class is sufficient. Singletons may
127      * implement this function with referential equality (`this === other`). Instances with no
128      * properties may implement this function by checking the type of the other object.
129      */
130     override fun equals(other: Any?): Boolean
131 }
132 
133 /**
134  * IndicationInstance is a specific instance of an [Indication] that draws visual effects on certain
135  * interactions, such as press or focus.
136  *
137  * IndicationInstances can be stateful or stateless, and are created by
138  * [Indication.rememberUpdatedInstance] - they should be used in-place and not re-used between
139  * different [indication] modifiers.
140  */
141 @Deprecated(IndicationInstanceDeprecationMessage, level = DeprecationLevel.ERROR)
142 interface IndicationInstance {
143 
144     /**
145      * Draws visual effects for the current interactions present on this component.
146      *
147      * Typically this function will read state within this instance that is mutated by
148      * [Indication.rememberUpdatedInstance]. This allows [IndicationInstance] to just read state and
149      * draw visual effects, and not actually change any state itself.
150      *
151      * This method MUST call [ContentDrawScope.drawContent] at some point in order to draw the
152      * component itself underneath any indication. Typically this is called at the beginning, so
153      * that indication can be drawn as an overlay on top.
154      */
155     fun ContentDrawScope.drawIndication()
156 }
157 
158 /**
159  * Draws visual effects for this component when interactions occur.
160  *
161  * @sample androidx.compose.foundation.samples.IndicationSample
162  * @param interactionSource [InteractionSource] that will be used by [indication] to draw visual
163  *   effects - this [InteractionSource] represents the stream of [Interaction]s for this component.
164  * @param indication [Indication] used to draw visual effects. If `null`, no visual effects will be
165  *   shown for this component.
166  */
indicationnull167 fun Modifier.indication(interactionSource: InteractionSource, indication: Indication?): Modifier {
168     if (indication == null) return this
169     // Fast path - ideally we should never break into the composed path below.
170     if (indication is IndicationNodeFactory) {
171         return this.then(IndicationModifierElement(interactionSource, indication))
172     }
173     // In the future we might want to remove this as a forcing function to migrate away from the
174     // error-deprecated rememberUpdatedInstance
175     return composed(
176         factory = {
177             @Suppress("DEPRECATION_ERROR")
178             val instance = indication.rememberUpdatedInstance(interactionSource)
179             remember(instance) { IndicationModifier(instance) }
180         },
181         inspectorInfo =
182             debugInspectorInfo {
183                 name = "indication"
184                 properties["interactionSource"] = interactionSource
185                 properties["indication"] = indication
186             }
187     )
188 }
189 
190 /**
191  * CompositionLocal that provides an [Indication] through the hierarchy. This [Indication] will be
192  * used by default to draw visual effects for interactions such as press and drag in components such
193  * as [clickable].
194  *
195  * By default this will provide a debug indication, this should always be replaced.
196  */
<lambda>null197 val LocalIndication = compositionLocalOf<Indication> { DefaultDebugIndication }
198 
199 /** Empty [IndicationInstance] for backwards compatibility - this is not expected to be used. */
200 @Suppress("DEPRECATION_ERROR")
201 private object NoIndicationInstance : IndicationInstance {
drawIndicationnull202     override fun ContentDrawScope.drawIndication() {
203         drawContent()
204     }
205 }
206 
207 /** Simple default [Indication] that draws a rectangular overlay when pressed. */
208 private object DefaultDebugIndication : IndicationNodeFactory {
209 
createnull210     override fun create(interactionSource: InteractionSource): DelegatableNode =
211         DefaultDebugIndicationInstance(interactionSource)
212 
213     override fun hashCode(): Int = -1
214 
215     override fun equals(other: Any?) = other === this
216 
217     private class DefaultDebugIndicationInstance(private val interactionSource: InteractionSource) :
218         Modifier.Node(), DrawModifierNode {
219         private var isPressed = false
220         private var isHovered = false
221         private var isFocused = false
222 
223         override fun onAttach() {
224             coroutineScope.launch {
225                 var pressCount = 0
226                 var hoverCount = 0
227                 var focusCount = 0
228                 interactionSource.interactions.collect { interaction ->
229                     when (interaction) {
230                         is PressInteraction.Press -> pressCount++
231                         is PressInteraction.Release -> pressCount--
232                         is PressInteraction.Cancel -> pressCount--
233                         is HoverInteraction.Enter -> hoverCount++
234                         is HoverInteraction.Exit -> hoverCount--
235                         is FocusInteraction.Focus -> focusCount++
236                         is FocusInteraction.Unfocus -> focusCount--
237                     }
238                     val pressed = pressCount > 0
239                     val hovered = hoverCount > 0
240                     val focused = focusCount > 0
241                     var invalidateNeeded = false
242                     if (isPressed != pressed) {
243                         isPressed = pressed
244                         invalidateNeeded = true
245                     }
246                     if (isHovered != hovered) {
247                         isHovered = hovered
248                         invalidateNeeded = true
249                     }
250                     if (isFocused != focused) {
251                         isFocused = focused
252                         invalidateNeeded = true
253                     }
254                     if (invalidateNeeded) invalidateDraw()
255                 }
256             }
257         }
258 
259         override fun ContentDrawScope.draw() {
260             drawContent()
261             if (isPressed) {
262                 drawRect(color = Color.Black.copy(alpha = 0.3f), size = size)
263             } else if (isHovered || isFocused) {
264                 drawRect(color = Color.Black.copy(alpha = 0.1f), size = size)
265             }
266         }
267     }
268 }
269 
270 /**
271  * ModifierNodeElement to create [IndicationNodeFactory] instances. More complicated modifiers such
272  * as [clickable] should manually delegate to the node returned by [IndicationNodeFactory]
273  * internally.
274  */
275 private class IndicationModifierElement(
276     private val interactionSource: InteractionSource,
277     private val indication: IndicationNodeFactory
278 ) : ModifierNodeElement<IndicationModifierNode>() {
createnull279     override fun create(): IndicationModifierNode {
280         return IndicationModifierNode(indication.create(interactionSource))
281     }
282 
inspectablePropertiesnull283     override fun InspectorInfo.inspectableProperties() {
284         name = "indication"
285         properties["interactionSource"] = interactionSource
286         properties["indication"] = indication
287     }
288 
updatenull289     override fun update(node: IndicationModifierNode) {
290         node.update(indication.create(interactionSource))
291     }
292 
equalsnull293     override fun equals(other: Any?): Boolean {
294         if (this === other) return true
295         if (other !is IndicationModifierElement) return false
296 
297         if (interactionSource != other.interactionSource) return false
298         if (indication != other.indication) return false
299 
300         return true
301     }
302 
hashCodenull303     override fun hashCode(): Int {
304         var result = interactionSource.hashCode()
305         result = 31 * result + indication.hashCode()
306         return result
307     }
308 }
309 
310 /**
311  * Wrapper [DelegatableNode] that allows us to replace the wrapped node fully when a new node is
312  * provided.
313  */
314 private class IndicationModifierNode(private var indicationNode: DelegatableNode) :
315     DelegatingNode() {
316     init {
317         delegate(indicationNode)
318     }
319 
updatenull320     fun update(indicationNode: DelegatableNode) {
321         undelegate(this.indicationNode)
322         this.indicationNode = indicationNode
323         delegate(indicationNode)
324     }
325 }
326 
327 @Suppress("DEPRECATION_ERROR")
328 private class IndicationModifier(val indicationInstance: IndicationInstance) : DrawModifier {
329 
drawnull330     override fun ContentDrawScope.draw() {
331         with(indicationInstance) { drawIndication() }
332     }
333 }
334 
335 private const val RememberUpdatedInstanceDeprecationMessage =
336     "rememberUpdatedInstance has been " +
337         "deprecated - implementers should instead implement IndicationNodeFactory#create for " +
338         "improved performance and efficiency. Callers should check if the Indication is an " +
339         "IndicationNodeFactory, and call that API instead. For a migration guide and background " +
340         "information, please visit developer.android.com"
341 
342 private const val IndicationInstanceDeprecationMessage =
343     "IndicationInstance has been deprecated " +
344         "along with the rememberUpdatedInstance that returns it. Indication implementations should " +
345         "instead use Modifier.Node APIs, and should be returned from " +
346         "IndicationNodeFactory#create. For a migration guide and background information, " +
347         "please visit developer.android.com"
348