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