blob: d85462d455a2b56fc3e3e4c6e0d8ce47e295c335 [file] [log] [blame]
/*
* Copyright 2020 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.compose.foundation
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.debugInspectorInfo
/**
* Indication represents visual effects that occur when certain interactions happens. For
* example: showing a ripple effect when a component is pressed, or a highlight when a component
* is focused.
*
* An instance of Indication is a factory that is required to produce [IndicationInstance]s on
* demand for each component that uses an [indication] modifier using [rememberUpdatedInstance].
*
* Indication is typically provided throughout the hierarchy through [LocalIndication] - you can
* provide a custom Indication to [LocalIndication] to change the default [Indication] used for
* components such as [clickable].
*/
@Stable
interface Indication {
/**
* [remember]s a new [IndicationInstance], and updates its state based on [Interaction]s
* emitted via [interactionSource] . Typically this will be called by [indication],
* so one [IndicationInstance] will be used for one component that draws [Indication], such
* as a button.
*
* Implementations of this function should observe [Interaction]s using [interactionSource],
* using them to launch animations / state changes inside [IndicationInstance] that will
* then be reflected inside [IndicationInstance.drawIndication].
*
* @param interactionSource the [InteractionSource] representing the stream of
* [Interaction]s the returned [IndicationInstance] should represent
* @return an [IndicationInstance] that represents the stream of [Interaction]s emitted by
* [interactionSource]
*/
@Composable
fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance
}
/**
* IndicationInstance is a specific instance of an [Indication] that draws visual effects on
* certain interactions, such as press or focus.
*
* IndicationInstances can be stateful or stateless, and are created by
* [Indication.rememberUpdatedInstance] - they should be used in-place and not re-used between
* different [indication] modifiers.
*/
interface IndicationInstance {
/**
* Draws visual effects for the current interactions present on this component.
*
* Typically this function will read state within this instance that is mutated by
* [Indication.rememberUpdatedInstance]. This allows [IndicationInstance] to just read state
* and draw visual effects, and not actually change any state itself.
*
* This method MUST call [ContentDrawScope.drawContent] at some point in order to draw the
* component itself underneath any indication. Typically this is called at the beginning, so
* that indication can be drawn as an overlay on top.
*/
fun ContentDrawScope.drawIndication()
}
/**
* Draws visual effects for this component when interactions occur.
*
* @sample androidx.compose.foundation.samples.IndicationSample
*
* @param interactionSource [InteractionSource] that will be used by [indication] to draw
* visual effects - this [InteractionSource] represents the stream of [Interaction]s for this
* component.
* @param indication [Indication] used to draw visual effects. If `null`, no visual effects will
* be shown for this component.
*/
fun Modifier.indication(
interactionSource: InteractionSource,
indication: Indication?
) = composed(
factory = {
val resolvedIndication = indication ?: NoIndication
val instance = resolvedIndication.rememberUpdatedInstance(interactionSource)
remember(instance) {
IndicationModifier(instance)
}
},
inspectorInfo = debugInspectorInfo {
name = "indication"
properties["indication"] = indication
properties["interactionSource"] = interactionSource
}
)
/**
* CompositionLocal that provides an [Indication] through the hierarchy. This [Indication] will
* be used by default to draw visual effects for interactions such as press and drag in components
* such as [clickable].
*
* By default this will provide [DefaultDebugIndication].
*/
val LocalIndication = staticCompositionLocalOf<Indication> {
DefaultDebugIndication
}
private object NoIndication : Indication {
private object NoIndicationInstance : IndicationInstance {
override fun ContentDrawScope.drawIndication() {
drawContent()
}
}
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
return NoIndicationInstance
}
}
/**
* Simple default [Indication] that draws a rectangular overlay when pressed.
*/
private object DefaultDebugIndication : Indication {
private class DefaultDebugIndicationInstance(
private val isPressed: State<Boolean>,
private val isHovered: State<Boolean>
) : IndicationInstance {
override fun ContentDrawScope.drawIndication() {
drawContent()
if (isPressed.value) {
drawRect(color = Color.Black.copy(alpha = 0.3f), size = size)
} else if (isHovered.value) {
drawRect(color = Color.Black.copy(alpha = 0.1f), size = size)
}
}
}
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
val isPressed = interactionSource.collectIsPressedAsState()
val isHovered = interactionSource.collectIsHoveredAsState()
return remember(interactionSource) {
DefaultDebugIndicationInstance(isPressed, isHovered)
}
}
}
private class IndicationModifier(
val indicationInstance: IndicationInstance
) : DrawModifier {
override fun ContentDrawScope.draw() {
with(indicationInstance) {
drawIndication()
}
}
}