blob: 53add8d6425c151e1a548625bab56482b884b69a [file] [log] [blame]
Aditya Arora26dd0be2023-03-09 12:15:34 +05301/*
2 * Copyright 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
17package androidx.tv.material3
18
19import androidx.annotation.FloatRange
20import androidx.compose.foundation.BorderStroke
21import androidx.compose.foundation.interaction.Interaction
22import androidx.compose.foundation.interaction.MutableInteractionSource
Aditya Arora99293512023-03-31 13:03:37 +053023import androidx.compose.foundation.layout.Box
24import androidx.compose.foundation.layout.BoxScope
Aditya Arora26dd0be2023-03-09 12:15:34 +053025import androidx.compose.foundation.layout.Column
26import androidx.compose.foundation.layout.ColumnScope
Aditya Arora99293512023-03-31 13:03:37 +053027import androidx.compose.foundation.layout.PaddingValues
28import androidx.compose.foundation.layout.Row
29import androidx.compose.foundation.layout.aspectRatio
30import androidx.compose.foundation.layout.padding
Aditya Arora26dd0be2023-03-09 12:15:34 +053031import androidx.compose.foundation.shape.RoundedCornerShape
32import androidx.compose.runtime.Composable
Aditya Arora99293512023-03-31 13:03:37 +053033import androidx.compose.runtime.CompositionLocalProvider
Aditya Arora26dd0be2023-03-09 12:15:34 +053034import androidx.compose.runtime.ReadOnlyComposable
35import androidx.compose.runtime.remember
Aditya Arora99293512023-03-31 13:03:37 +053036import androidx.compose.ui.Alignment
Aditya Arora26dd0be2023-03-09 12:15:34 +053037import androidx.compose.ui.Modifier
Aditya Arora99293512023-03-31 13:03:37 +053038import androidx.compose.ui.draw.drawWithCache
39import androidx.compose.ui.graphics.Brush
Aditya Arora26dd0be2023-03-09 12:15:34 +053040import androidx.compose.ui.graphics.Color
41import androidx.compose.ui.graphics.Shape
Aditya Arora99293512023-03-31 13:03:37 +053042import androidx.compose.ui.graphics.graphicsLayer
Aditya Arora26dd0be2023-03-09 12:15:34 +053043import androidx.compose.ui.unit.dp
44
45/**
46 * Cards contain content and actions that relate information about a subject.
47 *
48 * This Card handles click events, calling its [onClick] lambda.
49 *
50 * @param onClick called when this card is clicked
51 * @param modifier the [Modifier] to be applied to this card
52 * @param shape [CardShape] defines the shape of this card's container in different interaction
53 * states. See [CardDefaults.shape].
54 * @param colors [CardColors] defines the background & content colors used in this card for
55 * different interaction states. See [CardDefaults.colors].
56 * @param scale [CardScale] defines size of the card relative to its original size for different
57 * interaction states. See [CardDefaults.scale].
58 * @param border [CardBorder] defines a border around the card for different interaction states.
59 * See [CardDefaults.border].
60 * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
61 * states. See [CardDefaults.glow].
62 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
63 * for this card. You can create and pass in your own `remember`ed instance to observe
64 * [Interaction]s and customize the appearance / behavior of this card in different states.
65 * @param content defines the [Composable] content inside the Card.
66 */
67@ExperimentalTvMaterial3Api
68@Composable
69fun Card(
70 onClick: () -> Unit,
71 modifier: Modifier = Modifier,
72 shape: CardShape = CardDefaults.shape(),
73 colors: CardColors = CardDefaults.colors(),
74 scale: CardScale = CardDefaults.scale(),
75 border: CardBorder = CardDefaults.border(),
76 glow: CardGlow = CardDefaults.glow(),
77 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
78 content: @Composable ColumnScope.() -> Unit
79) {
80 Surface(
81 onClick = onClick,
82 modifier = modifier,
83 shape = shape.toClickableSurfaceShape(),
84 color = colors.toClickableSurfaceContainerColor(),
85 contentColor = colors.toClickableSurfaceContentColor(),
86 scale = scale.toClickableSurfaceScale(),
87 border = border.toClickableSurfaceBorder(),
88 glow = glow.toClickableSurfaceGlow(),
89 interactionSource = interactionSource,
90 ) {
91 Column(content = content)
92 }
93}
94
95/**
Aditya Arora99293512023-03-31 13:03:37 +053096 * [ClassicCard] is an opinionated TV Material card that offers a 4 slot layout to show
97 * information about a subject.
98 *
99 * This card has a vertical layout with the interactive surface [Surface], which provides the image
100 * slot at the top, followed by the title, subtitle, and description slots.
101 *
102 * This Card handles click events, calling its [onClick] lambda.
103 *
104 * @param onClick called when this card is clicked
105 * @param image defines the [Composable] image to be displayed on top of the Card.
106 * @param title defines the [Composable] title placed below the image in the Card.
107 * @param modifier the [Modifier] to be applied to this card.
108 * @param subtitle defines the [Composable] supporting text placed below the title of the Card.
109 * @param description defines the [Composable] description placed below the subtitle of the Card.
110 * @param shape [CardShape] defines the shape of this card's container in different interaction
111 * states. See [CardDefaults.shape].
112 * @param colors [CardColors] defines the background & content colors used in this card for
113 * different interaction states. See [CardDefaults.colors].
114 * @param scale [CardScale] defines size of the card relative to its original size for different
115 * interaction states. See [CardDefaults.scale].
116 * @param border [CardBorder] defines a border around the card for different interaction states.
117 * See [CardDefaults.border].
118 * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
119 * states. See [CardDefaults.glow].
120 * @param contentPadding [PaddingValues] defines the inner padding applied to the card's content.
121 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
122 * for this card. You can create and pass in your own `remember`ed instance to observe
123 * [Interaction]s and customize the appearance / behavior of this card in different states.
124 */
125@ExperimentalTvMaterial3Api
126@Composable
127fun ClassicCard(
128 onClick: () -> Unit,
129 image: @Composable BoxScope.() -> Unit,
130 title: @Composable () -> Unit,
131 modifier: Modifier = Modifier,
132 subtitle: @Composable () -> Unit = {},
133 description: @Composable () -> Unit = {},
134 shape: CardShape = CardDefaults.shape(),
135 colors: CardColors = CardDefaults.colors(),
136 scale: CardScale = CardDefaults.scale(),
137 border: CardBorder = CardDefaults.border(),
138 glow: CardGlow = CardDefaults.glow(),
139 contentPadding: PaddingValues = PaddingValues(),
140 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
141) {
142 Card(
143 onClick = onClick,
144 modifier = modifier,
145 interactionSource = interactionSource,
146 shape = shape,
147 colors = colors,
148 scale = scale,
149 border = border,
150 glow = glow
151 ) {
152 Column(
153 modifier = Modifier.padding(contentPadding)
154 ) {
155 Box(
156 contentAlignment = CardDefaults.ContentImageAlignment,
157 content = image
158 )
159 Column {
160 CardContent(
161 title = title,
162 subtitle = subtitle,
163 description = description
164 )
165 }
166 }
167 }
168}
169
170/**
171 * [CompactCard] is an opinionated TV Material card that offers a 4 slot layout to show
172 * information about a subject.
173 *
174 * This card provides the interactive surface [Surface] with the image slot as the background
175 * (with an overlay scrim gradient). Other slots for the title, subtitle, and description are
176 * placed over it.
177 *
178 * This Card handles click events, calling its [onClick] lambda.
179 *
180 * @param onClick called when this card is clicked
181 * @param image defines the [Composable] image to be displayed on top of the Card.
182 * @param title defines the [Composable] title placed below the image in the Card.
183 * @param modifier the [Modifier] to be applied to this card.
184 * @param subtitle defines the [Composable] supporting text placed below the title of the Card.
185 * @param description defines the [Composable] description placed below the subtitle of the Card.
186 * @param shape [CardShape] defines the shape of this card's container in different interaction
187 * states. See [CardDefaults.shape].
188 * @param colors [CardColors] defines the background & content colors used in this card for
189 * different interaction states. See [CardDefaults.compactCardColors].
190 * @param scale [CardScale] defines size of the card relative to its original size for different
191 * interaction states. See [CardDefaults.scale].
192 * @param border [CardBorder] defines a border around the card for different interaction states.
193 * See [CardDefaults.border].
194 * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
195 * states. See [CardDefaults.glow].
196 * @param scrimBrush [Brush] defines a brush/gradient to be used to draw the scrim over the image
197 * in the background. See [CardDefaults.ContainerGradient].
198 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
199 * for this card. You can create and pass in your own `remember`ed instance to observe
200 * [Interaction]s and customize the appearance / behavior of this card in different states.
201 */
202@ExperimentalTvMaterial3Api
203@Composable
204fun CompactCard(
205 onClick: () -> Unit,
206 image: @Composable BoxScope.() -> Unit,
207 title: @Composable () -> Unit,
208 modifier: Modifier = Modifier,
209 subtitle: @Composable () -> Unit = {},
210 description: @Composable () -> Unit = {},
211 shape: CardShape = CardDefaults.shape(),
212 colors: CardColors = CardDefaults.compactCardColors(),
213 scale: CardScale = CardDefaults.scale(),
214 border: CardBorder = CardDefaults.border(),
215 glow: CardGlow = CardDefaults.glow(),
216 scrimBrush: Brush = CardDefaults.ContainerGradient,
217 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
218) {
219 Card(
220 onClick = onClick,
221 modifier = modifier,
222 interactionSource = interactionSource,
223 shape = shape,
224 colors = colors,
225 scale = scale,
226 border = border,
227 glow = glow
228 ) {
229 Box(contentAlignment = Alignment.BottomStart) {
230 Box(
231 modifier = Modifier
232 .drawWithCache {
233 onDrawWithContent {
234 drawContent()
235 drawRect(brush = scrimBrush)
236 }
237 },
238 contentAlignment = CardDefaults.ContentImageAlignment,
239 content = image
240 )
241 Column {
242 CardContent(
243 title = title,
244 subtitle = subtitle,
245 description = description
246 )
247 }
248 }
249 }
250}
251
252/**
253 * [WideClassicCard] is an opinionated TV Material card that offers a 4 slot layout to show
254 * information about a subject.
255 *
256 * This card has a horizontal layout with the interactive surface [Surface], which provides the
257 * image slot at the start, followed by the title, subtitle, and description slots at the end.
258 *
259 * This Card handles click events, calling its [onClick] lambda.
260 *
261 * @param onClick called when this card is clicked
262 * @param image defines the [Composable] image to be displayed on top of the Card.
263 * @param title defines the [Composable] title placed below the image in the Card.
264 * @param modifier the [Modifier] to be applied to this card.
265 * @param subtitle defines the [Composable] supporting text placed below the title of the Card.
266 * @param description defines the [Composable] description placed below the subtitle of the Card.
267 * @param shape [CardShape] defines the shape of this card's container in different interaction
268 * states. See [CardDefaults.shape].
269 * @param colors [CardColors] defines the background & content colors used in this card for
270 * different interaction states. See [CardDefaults.colors].
271 * @param scale [CardScale] defines size of the card relative to its original size for different
272 * interaction states. See [CardDefaults.scale].
273 * @param border [CardBorder] defines a border around the card for different interaction states.
274 * See [CardDefaults.border].
275 * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
276 * states. See [CardDefaults.glow].
277 * @param contentPadding [PaddingValues] defines the inner padding applied to the card's content.
278 * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
279 * for this card. You can create and pass in your own `remember`ed instance to observe
280 * [Interaction]s and customize the appearance / behavior of this card in different states.
281 */
282@ExperimentalTvMaterial3Api
283@Composable
284fun WideClassicCard(
285 onClick: () -> Unit,
286 image: @Composable BoxScope.() -> Unit,
287 title: @Composable () -> Unit,
288 modifier: Modifier = Modifier,
289 subtitle: @Composable () -> Unit = {},
290 description: @Composable () -> Unit = {},
291 shape: CardShape = CardDefaults.shape(),
292 colors: CardColors = CardDefaults.colors(),
293 scale: CardScale = CardDefaults.scale(),
294 border: CardBorder = CardDefaults.border(),
295 glow: CardGlow = CardDefaults.glow(),
296 contentPadding: PaddingValues = PaddingValues(),
297 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
298) {
299 Card(
300 onClick = onClick,
301 modifier = modifier,
302 interactionSource = interactionSource,
303 shape = shape,
304 colors = colors,
305 scale = scale,
306 border = border,
307 glow = glow
308 ) {
309 Row(
310 modifier = Modifier.padding(contentPadding)
311 ) {
312 Box(
313 contentAlignment = CardDefaults.ContentImageAlignment,
314 content = image
315 )
316 Column {
317 CardContent(
318 title = title,
319 subtitle = subtitle,
320 description = description
321 )
322 }
323 }
324 }
325}
326
327@Composable
328private fun CardContent(
329 title: @Composable () -> Unit,
330 subtitle: @Composable () -> Unit = {},
331 description: @Composable () -> Unit = {},
332 contentColor: Color
333) {
334 CompositionLocalProvider(LocalContentColor provides contentColor) {
335 CardContent(title, subtitle, description)
336 }
337}
338
339@Composable
340private fun CardContent(
341 title: @Composable () -> Unit,
342 subtitle: @Composable () -> Unit = {},
343 description: @Composable () -> Unit = {}
344) {
345 ProvideTextStyle(MaterialTheme.typography.titleMedium) {
346 title.invoke()
347 }
348 ProvideTextStyle(MaterialTheme.typography.bodySmall) {
349 Box(Modifier.graphicsLayer { alpha = SubtitleAlpha }) {
350 subtitle.invoke()
351 }
352 }
353 ProvideTextStyle(MaterialTheme.typography.bodySmall) {
354 Box(Modifier.graphicsLayer { alpha = DescriptionAlpha }) {
355 description.invoke()
356 }
357 }
358}
359
360/**
Aditya Arora26dd0be2023-03-09 12:15:34 +0530361 * Contains the default values used by all card types.
362 */
363@ExperimentalTvMaterial3Api
364object CardDefaults {
Aditya Arora99293512023-03-31 13:03:37 +0530365 internal val ContentImageAlignment = Alignment.Center
366
Aditya Arora26dd0be2023-03-09 12:15:34 +0530367 /**
Aditya Arora99293512023-03-31 13:03:37 +0530368 * The default [Shape] used by Cards.
369 */
Aditya Arora26dd0be2023-03-09 12:15:34 +0530370 private val ContainerShape = RoundedCornerShape(8.dp)
371
372 /**
Aditya Arora99293512023-03-31 13:03:37 +0530373 * Recommended aspect ratio [Float] to get square images, can be applied using the modifier
374 * [Modifier.aspectRatio].
375 */
376 const val SquareImageAspectRatio = 1f
377
378 /**
379 * Recommended aspect ratio [Float] for vertical images, can be applied using the modifier
380 * [Modifier.aspectRatio].
381 */
382 const val VerticalImageAspectRatio = 2f / 3
383
384 /**
385 * Recommended aspect ratio [Float] for horizontal images, can be applied using the modifier
386 * [Modifier.aspectRatio].
387 */
388 const val HorizontalImageAspectRatio = 16f / 9
389
390 /**
391 * Gradient used in cards to give more emphasis to the textual content that is generally
392 * displayed above an image.
393 */
394 val ContainerGradient = Brush.verticalGradient(
395 listOf(
396 Color(red = 28, green = 27, blue = 31, alpha = 0),
397 Color(red = 28, green = 27, blue = 31, alpha = 204)
398 )
399 )
400
401 /**
402 * Returns the content color [Color] from the colors [CardColors] for different
403 * interaction states.
404 */
405 internal fun contentColor(
406 focused: Boolean,
407 pressed: Boolean,
408 colors: CardColors
409 ): Color {
410 return when {
411 focused -> colors.focusedContentColor
412 pressed -> colors.pressedContentColor
413 else -> colors.contentColor
414 }
415 }
416
417 /**
Aditya Arora26dd0be2023-03-09 12:15:34 +0530418 * Creates a [CardShape] that represents the default container shapes used in a Card.
419 *
420 * @param shape the default shape used when the Card has no other [Interaction]s.
421 * @param focusedShape the shape used when the Card is focused.
422 * @param pressedShape the shape used when the Card is pressed.
423 */
424 fun shape(
425 shape: Shape = ContainerShape,
426 focusedShape: Shape = shape,
427 pressedShape: Shape = shape
428 ) = CardShape(
429 shape = shape,
430 focusedShape = focusedShape,
431 pressedShape = pressedShape
432 )
433
434 /**
435 * Creates [CardColors] that represents the default container & content colors used in a Card.
436 *
437 * @param containerColor the default container color of this Card.
438 * @param contentColor the default content color of this Card.
439 * @param focusedContainerColor the container color of this Card when focused.
440 * @param focusedContentColor the content color of this Card when focused.
441 * @param pressedContainerColor the container color of this Card when pressed.
442 * @param pressedContentColor the content color of this Card when pressed.
443 */
444 @ReadOnlyComposable
445 @Composable
446 fun colors(
Aditya Arora99293512023-03-31 13:03:37 +0530447 containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
Aditya Arora26dd0be2023-03-09 12:15:34 +0530448 contentColor: Color = contentColorFor(containerColor),
449 focusedContainerColor: Color = containerColor,
450 focusedContentColor: Color = contentColorFor(focusedContainerColor),
451 pressedContainerColor: Color = focusedContainerColor,
452 pressedContentColor: Color = contentColorFor(pressedContainerColor)
453 ) = CardColors(
454 containerColor = containerColor,
455 contentColor = contentColor,
456 focusedContainerColor = focusedContainerColor,
457 focusedContentColor = focusedContentColor,
458 pressedContainerColor = pressedContainerColor,
459 pressedContentColor = pressedContentColor
460 )
461
462 /**
Aditya Arora99293512023-03-31 13:03:37 +0530463 * Creates [CardColors] that represents the default colors used in a Compact Card.
464 *
465 * @param containerColor the default container color of this Card.
466 * @param contentColor the default content color of this Card.
467 * @param focusedContainerColor the container color of this Card when focused.
468 * @param focusedContentColor the content color of this Card when focused.
469 * @param pressedContainerColor the container color of this Card when pressed.
470 * @param pressedContentColor the content color of this Card when pressed.
471 */
472 @ReadOnlyComposable
473 @Composable
474 fun compactCardColors(
475 containerColor: Color = MaterialTheme.colorScheme.surfaceVariant,
476 contentColor: Color = Color.White,
477 focusedContainerColor: Color = containerColor,
478 focusedContentColor: Color = contentColor,
479 pressedContainerColor: Color = focusedContainerColor,
480 pressedContentColor: Color = focusedContentColor
481 ) = CardColors(
482 containerColor = containerColor,
483 contentColor = contentColor,
484 focusedContainerColor = focusedContainerColor,
485 focusedContentColor = focusedContentColor,
486 pressedContainerColor = pressedContainerColor,
487 pressedContentColor = pressedContentColor
488 )
489
490 /**
Aditya Arora26dd0be2023-03-09 12:15:34 +0530491 * Creates a [CardScale] that represents the default scales used in a Card.
492 * Scales are used to modify the size of a composable in different [Interaction] states
493 * e.g. 1f (original) in default state, 1.1f (scaled up) in focused state, 0.8f (scaled down)
494 * in pressed state, etc.
495 *
496 * @param scale the default scale to be used for this Card.
497 * @param focusedScale the scale to be used for this Card when focused.
498 * @param pressedScale the scale to be used for this Card when pressed.
499 */
500 fun scale(
501 @FloatRange(from = 0.0) scale: Float = 1f,
502 @FloatRange(from = 0.0) focusedScale: Float = 1.1f,
503 @FloatRange(from = 0.0) pressedScale: Float = scale
504 ) = CardScale(
505 scale = scale,
506 focusedScale = focusedScale,
507 pressedScale = pressedScale
508 )
509
510 /**
511 * Creates a [CardBorder] that represents the border [Border]s applied on a Card in
512 * different [Interaction] states.
513 *
514 * @param border the default [Border] to be used for this Card.
515 * @param focusedBorder the [Border] to be used for this Card when focused.
516 * @param pressedBorder the [Border] to be used for this Card when pressed.
517 */
518 @ReadOnlyComposable
519 @Composable
520 fun border(
521 border: Border = Border.None,
522 focusedBorder: Border = Border(
523 border = BorderStroke(
524 width = 3.dp,
525 color = MaterialTheme.colorScheme.border
526 ),
527 shape = ContainerShape
528 ),
529 pressedBorder: Border = focusedBorder
530 ) = CardBorder(
531 border = border,
532 focusedBorder = focusedBorder,
533 pressedBorder = pressedBorder
534 )
535
536 /**
537 * Creates a [CardGlow] that represents the default [Glow]s used in a card.
538 *
539 * @param glow the default [Glow] behind this Card.
540 * @param focusedGlow the [Glow] behind this Card when focused.
541 * @param pressedGlow the [Glow] behind this Card when pressed.
542 */
543 fun glow(
544 glow: Glow = Glow.None,
545 focusedGlow: Glow = glow,
546 pressedGlow: Glow = glow
547 ) = CardGlow(
548 glow = glow,
549 focusedGlow = focusedGlow,
550 pressedGlow = pressedGlow
551 )
552}
553
Aditya Arora99293512023-03-31 13:03:37 +0530554private const val SubtitleAlpha = 0.6f
555private const val DescriptionAlpha = 0.8f
556
Aditya Arora26dd0be2023-03-09 12:15:34 +0530557@OptIn(ExperimentalTvMaterial3Api::class)
558private fun CardColors.toClickableSurfaceContainerColor() =
559 ClickableSurfaceColor(
560 color = containerColor,
561 focusedColor = focusedContainerColor,
562 pressedColor = pressedContainerColor,
563 disabledColor = containerColor
564 )
565
566@OptIn(ExperimentalTvMaterial3Api::class)
567private fun CardColors.toClickableSurfaceContentColor() =
568 ClickableSurfaceColor(
569 color = contentColor,
570 focusedColor = focusedContentColor,
571 pressedColor = pressedContentColor,
572 disabledColor = contentColor
573 )
574
575@OptIn(ExperimentalTvMaterial3Api::class)
576private fun CardShape.toClickableSurfaceShape() =
577 ClickableSurfaceShape(
578 shape = shape,
579 focusedShape = focusedShape,
580 pressedShape = pressedShape,
581 disabledShape = shape,
582 focusedDisabledShape = shape
583 )
584
585@OptIn(ExperimentalTvMaterial3Api::class)
586private fun CardScale.toClickableSurfaceScale() =
587 ClickableSurfaceScale(
588 scale = scale,
589 focusedScale = focusedScale,
590 pressedScale = pressedScale,
591 disabledScale = scale,
592 focusedDisabledScale = scale
593 )
594
595@OptIn(ExperimentalTvMaterial3Api::class)
596private fun CardBorder.toClickableSurfaceBorder() =
597 ClickableSurfaceBorder(
598 border = border,
599 focusedBorder = focusedBorder,
600 pressedBorder = pressedBorder,
601 disabledBorder = border,
602 focusedDisabledBorder = border
603 )
604
605@OptIn(ExperimentalTvMaterial3Api::class)
606private fun CardGlow.toClickableSurfaceGlow() =
607 ClickableSurfaceGlow(
608 glow = glow,
609 focusedGlow = focusedGlow,
610 pressedGlow = pressedGlow
611 )