1 /*
<lambda>null2 * Copyright 2025 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.tv.material3
18
19 import androidx.compose.animation.core.animateFloatAsState
20 import androidx.compose.foundation.background
21 import androidx.compose.foundation.interaction.MutableInteractionSource
22 import androidx.compose.foundation.interaction.PressInteraction
23 import androidx.compose.foundation.interaction.collectIsFocusedAsState
24 import androidx.compose.foundation.interaction.collectIsPressedAsState
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.BoxScope
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.CompositionLocalProvider
29 import androidx.compose.runtime.compositionLocalOf
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.rememberCoroutineScope
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.composed
37 import androidx.compose.ui.focus.onFocusChanged
38 import androidx.compose.ui.geometry.Offset
39 import androidx.compose.ui.graphics.Color
40 import androidx.compose.ui.graphics.Shape
41 import androidx.compose.ui.graphics.graphicsLayer
42 import androidx.compose.ui.input.key.NativeKeyEvent
43 import androidx.compose.ui.input.key.onKeyEvent
44 import androidx.compose.ui.platform.debugInspectorInfo
45 import androidx.compose.ui.unit.Dp
46 import androidx.compose.ui.unit.dp
47 import androidx.compose.ui.zIndex
48 import kotlinx.coroutines.launch
49
50 @Composable
51 internal fun SurfaceImpl(
52 modifier: Modifier,
53 selected: Boolean,
54 enabled: Boolean,
55 shape: Shape,
56 color: Color,
57 contentColor: Color,
58 scale: Float,
59 border: Border,
60 glow: Glow,
61 tonalElevation: Dp,
62 interactionSource: MutableInteractionSource? = null,
63 content: @Composable (BoxScope.() -> Unit)
64 ) {
65 @Suppress("NAME_SHADOWING")
66 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
67 val focused by interactionSource.collectIsFocusedAsState()
68 val pressed by interactionSource.collectIsPressedAsState()
69
70 val surfaceAlpha =
71 calculateAlphaBasedOnState(
72 enabled = enabled,
73 focused = focused,
74 pressed = pressed,
75 selected = selected
76 )
77
78 val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
79
80 CompositionLocalProvider(
81 LocalContentColor provides contentColor,
82 LocalAbsoluteTonalElevation provides absoluteElevation
83 ) {
84 val zIndex by
85 animateFloatAsState(
86 targetValue = if (focused) FocusedZIndex else NonFocusedZIndex,
87 label = "zIndex"
88 )
89
90 val backgroundColorByState =
91 calculateSurfaceColorAtElevation(
92 color = color,
93 elevation = LocalAbsoluteTonalElevation.current
94 )
95
96 Box(
97 modifier =
98 modifier
99 .tvSurfaceScale(
100 scale = scale,
101 interactionSource = interactionSource,
102 )
103 .ifElse(API_28_OR_ABOVE, Modifier.tvSurfaceGlow(shape, glow))
104 // Increasing the zIndex of this Surface when it is in the focused state to
105 // avoid the glowIndication from being overlapped by subsequent items if
106 // this Surface is inside a list composable (like a Row/Column).
107 .zIndex(zIndex)
108 .ifElse(border != Border.None, Modifier.tvSurfaceBorder(shape, border))
109 .background(backgroundColorByState, shape)
110 .graphicsLayer {
111 this.alpha = surfaceAlpha
112 this.shape = shape
113 this.clip = true
114 },
115 propagateMinConstraints = true
116 ) {
117 Box(
118 modifier =
119 Modifier.graphicsLayer {
120 this.alpha = if (!enabled) DisabledContentAlpha else EnabledContentAlpha
121 },
122 content = content
123 )
124 }
125 }
126 }
127
handleDPadEnternull128 internal fun Modifier.handleDPadEnter(
129 enabled: Boolean,
130 interactionSource: MutableInteractionSource,
131 onClick: (() -> Unit)? = null,
132 onLongClick: (() -> Unit)? = null,
133 selected: Boolean = false,
134 ) =
135 composed(
136 inspectorInfo =
137 debugInspectorInfo {
138 name = "handleDPadEnter"
139 properties["enabled"] = enabled
140 properties["interactionSource"] = interactionSource
141 properties["onClick"] = onClick
142 properties["onLongClick"] = onLongClick
143 properties["selected"] = selected
144 }
<lambda>null145 ) {
146 if (!enabled) return@composed this
147
148 val coroutineScope = rememberCoroutineScope()
149 val pressInteraction = remember { PressInteraction.Press(Offset.Zero) }
150 var isLongClick by remember { mutableStateOf(false) }
151 val isPressed by interactionSource.collectIsPressedAsState()
152
153 this.onFocusChanged {
154 if (!it.isFocused && isPressed) {
155 coroutineScope.launch {
156 interactionSource.emit(PressInteraction.Release(pressInteraction))
157 }
158 }
159 }
160 .onKeyEvent { keyEvent ->
161 if (AcceptableEnterClickKeys.contains(keyEvent.nativeKeyEvent.keyCode)) {
162 when (keyEvent.nativeKeyEvent.action) {
163 NativeKeyEvent.ACTION_DOWN -> {
164 when (keyEvent.nativeKeyEvent.repeatCount) {
165 0 ->
166 coroutineScope.launch {
167 interactionSource.emit(pressInteraction)
168 }
169 1 ->
170 onLongClick?.let {
171 isLongClick = true
172 coroutineScope.launch {
173 interactionSource.emit(
174 PressInteraction.Release(pressInteraction)
175 )
176 }
177 it.invoke()
178 }
179 }
180 }
181 NativeKeyEvent.ACTION_UP -> {
182 if (!isLongClick) {
183 coroutineScope.launch {
184 interactionSource.emit(
185 PressInteraction.Release(pressInteraction)
186 )
187 }
188 onClick?.invoke()
189 } else isLongClick = false
190 }
191 }
192 return@onKeyEvent KeyEventPropagation.StopPropagation
193 }
194 KeyEventPropagation.ContinuePropagation
195 }
196 }
197
calculateAlphaBasedOnStatenull198 private fun calculateAlphaBasedOnState(
199 enabled: Boolean,
200 focused: Boolean,
201 pressed: Boolean,
202 selected: Boolean
203 ): Float {
204 return when {
205 !enabled && pressed -> DisabledPressedStateAlpha
206 !enabled && focused -> DisabledFocusedStateAlpha
207 !enabled && selected -> DisabledSelectedStateAlpha
208 enabled -> EnabledContentAlpha
209 else -> DisabledDefaultStateAlpha
210 }
211 }
212
213 @Composable
calculateSurfaceColorAtElevationnull214 internal fun calculateSurfaceColorAtElevation(color: Color, elevation: Dp): Color {
215 return if (color == MaterialTheme.colorScheme.surface) {
216 MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
217 } else {
218 color
219 }
220 }
221
222 private const val DisabledPressedStateAlpha = 0.8f
223 private const val DisabledFocusedStateAlpha = 0.8f
224 private const val DisabledSelectedStateAlpha = 0.8f
225 private const val DisabledDefaultStateAlpha = 0.6f
226
227 private const val FocusedZIndex = 0.5f
228 private const val NonFocusedZIndex = 0f
229
230 private const val DisabledContentAlpha = 0.8f
231 private const val EnabledContentAlpha = 1f
232
233 private val AcceptableEnterClickKeys =
234 intArrayOf(
235 NativeKeyEvent.KEYCODE_DPAD_CENTER,
236 NativeKeyEvent.KEYCODE_ENTER,
237 NativeKeyEvent.KEYCODE_NUMPAD_ENTER
238 )
239
240 /**
241 * CompositionLocal containing the current absolute elevation provided by Surface components. This
242 * absolute elevation is a sum of all the previous elevations. Absolute elevation is only used for
243 * calculating surface tonal colors, and is *not* used for drawing the shadow in a [SurfaceImpl].
244 */
<lambda>null245 internal val LocalAbsoluteTonalElevation = compositionLocalOf { 0.dp }
246