1 /*
2 * Copyright 2021 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.wear.compose.material
18
19 import androidx.compose.foundation.Image
20 import androidx.compose.foundation.interaction.Interaction
21 import androidx.compose.foundation.interaction.MutableInteractionSource
22 import androidx.compose.foundation.layout.Column
23 import androidx.compose.foundation.layout.ColumnScope
24 import androidx.compose.foundation.layout.PaddingValues
25 import androidx.compose.foundation.layout.Row
26 import androidx.compose.foundation.layout.RowScope
27 import androidx.compose.foundation.layout.Spacer
28 import androidx.compose.foundation.layout.fillMaxWidth
29 import androidx.compose.foundation.layout.height
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.CompositionLocalProvider
32 import androidx.compose.runtime.Immutable
33 import androidx.compose.ui.Alignment
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.geometry.Offset
36 import androidx.compose.ui.geometry.Size
37 import androidx.compose.ui.graphics.Brush
38 import androidx.compose.ui.graphics.Color
39 import androidx.compose.ui.graphics.LinearGradientShader
40 import androidx.compose.ui.graphics.Shader
41 import androidx.compose.ui.graphics.ShaderBrush
42 import androidx.compose.ui.graphics.Shape
43 import androidx.compose.ui.graphics.TileMode
44 import androidx.compose.ui.graphics.compositeOver
45 import androidx.compose.ui.graphics.painter.BrushPainter
46 import androidx.compose.ui.graphics.painter.ColorPainter
47 import androidx.compose.ui.graphics.painter.Painter
48 import androidx.compose.ui.platform.LocalLayoutDirection
49 import androidx.compose.ui.semantics.Role
50 import androidx.compose.ui.unit.Dp
51 import androidx.compose.ui.unit.LayoutDirection
52 import androidx.compose.ui.unit.dp
53 import androidx.wear.compose.materialcore.ImageWithScrimPainter
54 import kotlin.math.min
55
56 /**
57 * Base level Wear Material [Card] that offers a single slot to take any content.
58 *
59 * Is used as the container for more opinionated [Card] components that take specific content such
60 * as icons, images, titles, subtitles and labels.
61 *
62 * The [Card] is Rectangle shaped rounded corners by default.
63 *
64 * Cards can be enabled or disabled. A disabled card will not respond to click events.
65 *
66 * For more information, see the
67 * [Cards](https://developer.android.com/training/wearables/components/cards) Wear OS Material
68 * design guide.
69 *
70 * Note that the Wear OS design guidance recommends a gradient or image background for Cards which
71 * is not the case for Mobile Cards. As a result you will see a backgroundPainter rather than a
72 * backgroundColor for Cards. If you want to workaround this recommendation you could use a
73 * [ColorPainter] to produce a solid colored background.
74 *
75 * @param onClick Will be called when the user clicks the card
76 * @param modifier Modifier to be applied to the card
77 * @param backgroundPainter A painter used to paint the background of the card. A card will normally
78 * have a gradient background. Use [CardDefaults.cardBackgroundPainter()] to obtain an appropriate
79 * painter
80 * @param contentColor The default color to use for content() unless explicitly set.
81 * @param enabled Controls the enabled state of the card. When false, this card will not be
82 * clickable and there will be no ripple effect on click. Wear cards do not have any specific
83 * elevation or alpha differences when not enabled - they are simply not clickable.
84 * @param contentPadding The spacing values to apply internally between the container and the
85 * content
86 * @param shape Defines the card's shape. It is strongly recommended to use the default as this
87 * shape is a key characteristic of the Wear Material Theme
88 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
89 * emitting [Interaction]s for this card. You can use this to change the card's appearance or
90 * preview the card in different states. Note that if `null` is provided, interactions will still
91 * happen internally.
92 * @param role The type of user interface element. Accessibility services might use this to describe
93 * the element or do customizations
94 * @param content Slot for composable body content displayed on the Card
95 */
96 @Composable
Cardnull97 public fun Card(
98 onClick: () -> Unit,
99 modifier: Modifier = Modifier,
100 backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
101 contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
102 enabled: Boolean = true,
103 contentPadding: PaddingValues = CardDefaults.ContentPadding,
104 shape: Shape = MaterialTheme.shapes.large,
105 interactionSource: MutableInteractionSource? = null,
106 role: Role? = null,
107 content: @Composable ColumnScope.() -> Unit,
108 ) {
109 androidx.wear.compose.materialcore.Card(
110 onClick = onClick,
111 modifier = modifier,
112 border = null,
113 containerPainter = backgroundPainter,
114 enabled = enabled,
115 contentPadding = contentPadding,
116 shape = shape,
117 interactionSource = interactionSource,
118 role = role,
119 ripple = ripple(),
120 ) {
121 CompositionLocalProvider(
122 LocalContentColor provides contentColor,
123 LocalTextStyle provides MaterialTheme.typography.button,
124 ) {
125 content()
126 }
127 }
128 }
129
130 /**
131 * Opinionated Wear Material [Card] that offers a specific 5 slot layout to show information about
132 * an application, e.g. a notification. AppCards are designed to show interactive elements from
133 * multiple applications. They will typically be used by the system UI, e.g. for showing a list of
134 * notifications from different applications. However it could also be adapted by individual
135 * application developers to show information about different parts of their application.
136 *
137 * The first row of the layout has three slots, 1) a small optional application [Image] or [Icon] of
138 * size [CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] dp, 2) an application name
139 * (emphasised with the [CardColors.appColor()] color), it is expected to be a short start aligned
140 * [Text] composable, and 3) the time that the application activity has occurred which will be shown
141 * on the top row of the card, this is expected to be an end aligned [Text] composable showing a
142 * time relevant to the contents of the [Card].
143 *
144 * The second row shows a title, this is expected to be a single row of start aligned [Text].
145 *
146 * The rest of the [Card] contains the content which can be either [Text] or an [Image]. If the
147 * content is text it can be single or multiple line and is expected to be Top and Start aligned.
148 *
149 * If more than one composable is provided in the content slot it is the responsibility of the
150 * caller to determine how to layout the contents, e.g. provide either a row or a column.
151 *
152 * Example of an [AppCard] with icon, title, time and two lines of body text:
153 *
154 * @sample androidx.wear.compose.material.samples.AppCardWithIcon
155 *
156 * Example of an [AppCard] with image content:
157 *
158 * @sample androidx.wear.compose.material.samples.AppCardWithImage
159 *
160 * For more information, see the
161 * [Cards](https://developer.android.com/training/wearables/components/cards) guide.
162 *
163 * @param onClick Will be called when the user clicks the card
164 * @param appName A slot for displaying the application name, expected to be a single line of start
165 * aligned text of [Typography.title3]
166 * @param time A slot for displaying the time relevant to the contents of the card, expected to be a
167 * short piece of end aligned text.
168 * @param title A slot for displaying the title of the card, expected to be one or two lines of
169 * start aligned text of [Typography.button]
170 * @param modifier Modifier to be applied to the card
171 * @param enabled Controls the enabled state of the card. When false, this card will not be
172 * clickable and there will be no ripple effect on click. Wear cards do not have any specific
173 * elevation or alpha differences when not enabled - they are simply not clickable.
174 * @param appImage A slot for a small ([CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] )
175 * [Image] associated with the application.
176 * @param backgroundPainter A painter used to paint the background of the card. A card will normally
177 * have a gradient background. Use [CardDefaults.cardBackgroundPainter()] to obtain an appropriate
178 * painter
179 * @param contentColor The default color to use for content() slot unless explicitly set.
180 * @param appColor The default color to use for appName() and appImage() slots unless explicitly
181 * set.
182 * @param timeColor The default color to use for time() slot unless explicitly set.
183 * @param titleColor The default color to use for title() slot unless explicitly set.
184 * @param content Slot for composable body content displayed on the Card
185 */
186 @Composable
AppCardnull187 public fun AppCard(
188 onClick: () -> Unit,
189 appName: @Composable RowScope.() -> Unit,
190 time: @Composable RowScope.() -> Unit,
191 title: @Composable RowScope.() -> Unit,
192 modifier: Modifier = Modifier,
193 enabled: Boolean = true,
194 appImage: @Composable (RowScope.() -> Unit)? = null,
195 backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
196 contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
197 appColor: Color = contentColor,
198 timeColor: Color = contentColor,
199 titleColor: Color = MaterialTheme.colors.onSurface,
200 content: @Composable ColumnScope.() -> Unit,
201 ) {
202 androidx.wear.compose.materialcore.AppCard(
203 onClick = onClick,
204 modifier = modifier,
205 enabled = enabled,
206 border = null,
207 contentPadding = CardDefaults.ContentPadding,
208 containerPainter = backgroundPainter,
209 interactionSource = null,
210 shape = MaterialTheme.shapes.large,
211 ripple = ripple(),
212 appImage = appImage?.let { { appImage() } },
213 appName = {
214 CompositionLocalProvider(
215 LocalContentColor provides appColor,
216 LocalTextStyle provides MaterialTheme.typography.caption1
217 ) {
218 appName()
219 }
220 },
221 time = {
222 CompositionLocalProvider(
223 LocalContentColor provides timeColor,
224 LocalTextStyle provides MaterialTheme.typography.caption1,
225 ) {
226 time()
227 }
228 },
229 title = {
230 CompositionLocalProvider(
231 LocalContentColor provides titleColor,
232 LocalTextStyle provides MaterialTheme.typography.title3
233 ) {
234 title()
235 }
236 }
237 ) {
238 CompositionLocalProvider(
239 LocalContentColor provides contentColor,
240 LocalTextStyle provides MaterialTheme.typography.body1,
241 ) {
242 content()
243 }
244 }
245 }
246
247 /**
248 * Opinionated Wear Material [Card] that offers a specific 3 slot layout to show interactive
249 * information about an application, e.g. a message. TitleCards are designed for use within an
250 * application.
251 *
252 * The first row of the layout has two slots. 1. a start aligned title (emphasised with the
253 * [titleColor] and expected to be start aligned text). The title text is expected to be a maximum
254 * of 2 lines of text. 2. An optional time that the application activity has occurred shown at the
255 * end of the row, expected to be an end aligned [Text] composable showing a time relevant to the
256 * contents of the [Card].
257 *
258 * The rest of the [Card] contains the content which is expected to be [Text] or a contained
259 * [Image].
260 *
261 * If the content is text it can be single or multiple line and is expected to be Top and Start
262 * aligned and of type of [Typography.body1].
263 *
264 * Overall the [title] and [content] text should be no more than 5 rows of text combined.
265 *
266 * If more than one composable is provided in the content slot it is the responsibility of the
267 * caller to determine how to layout the contents, e.g. provide either a row or a column.
268 *
269 * Example of a [TitleCard] with two lines of body text:
270 *
271 * @sample androidx.wear.compose.material.samples.TitleCardStandard
272 *
273 * Example of a title card with a background image:
274 *
275 * @sample androidx.wear.compose.material.samples.TitleCardWithImageBackground
276 *
277 * For more information, see the
278 * [Cards](https://developer.android.com/training/wearables/components/cards) guide.
279 *
280 * @param onClick Will be called when the user clicks the card
281 * @param title A slot for displaying the title of the card, expected to be one or two lines of text
282 * of [Typography.button]
283 * @param modifier Modifier to be applied to the card
284 * @param enabled Controls the enabled state of the card. When false, this card will not be
285 * clickable and there will be no ripple effect on click. Wear cards do not have any specific
286 * elevation or alpha differences when not enabled - they are simply not clickable.
287 * @param time An optional slot for displaying the time relevant to the contents of the card,
288 * expected to be a short piece of end aligned text.
289 * @param backgroundPainter A painter used to paint the background of the card. A title card can
290 * have either a gradient background or an image background, use
291 * [CardDefaults.cardBackgroundPainter()] or [CardDefaults.imageBackgroundPainter()] to obtain an
292 * appropriate painter
293 * @param contentColor The default color to use for content() slot unless explicitly set.
294 * @param titleColor The default color to use for title() slot unless explicitly set.
295 * @param timeColor The default color to use for time() slot unless explicitly set.
296 * @param content Slot for composable body content displayed on the Card
297 */
298 @Composable
TitleCardnull299 public fun TitleCard(
300 onClick: () -> Unit,
301 title: @Composable RowScope.() -> Unit,
302 modifier: Modifier = Modifier,
303 enabled: Boolean = true,
304 time: @Composable (RowScope.() -> Unit)? = null,
305 backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
306 contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
307 titleColor: Color = MaterialTheme.colors.onSurface,
308 timeColor: Color = contentColor,
309 content: @Composable ColumnScope.() -> Unit,
310 ) {
311 androidx.wear.compose.materialcore.Card(
312 onClick = onClick,
313 modifier = modifier,
314 enabled = enabled,
315 containerPainter = backgroundPainter,
316 border = null,
317 contentPadding = CardDefaults.ContentPadding,
318 interactionSource = null,
319 role = null,
320 shape = MaterialTheme.shapes.large,
321 ripple = ripple(),
322 ) {
323 Column {
324 Row(
325 modifier = Modifier.fillMaxWidth(),
326 verticalAlignment = Alignment.CenterVertically
327 ) {
328 CompositionLocalProvider(
329 LocalContentColor provides titleColor,
330 LocalTextStyle provides MaterialTheme.typography.title3,
331 ) {
332 title()
333 }
334 time?.let {
335 Spacer(modifier = Modifier.weight(1.0f))
336 CompositionLocalProvider(
337 LocalContentColor provides timeColor,
338 LocalTextStyle provides MaterialTheme.typography.caption1,
339 ) {
340 time()
341 }
342 }
343 }
344 Spacer(modifier = Modifier.height(2.dp))
345 CompositionLocalProvider(
346 LocalContentColor provides contentColor,
347 LocalTextStyle provides MaterialTheme.typography.body1,
348 ) {
349 content()
350 }
351 }
352 }
353 }
354
355 /** Contains the default values used by [Card] */
356 public object CardDefaults {
357 /**
358 * Creates a [Painter] for background colors for a [Card]. Cards typically have a linear
359 * gradient for a background. The gradient will be between startBackgroundColor and
360 * endBackgroundColor and at an angle of 45 degrees.
361 *
362 * Cards should have a content color that contrasts with the background gradient.
363 *
364 * @param startBackgroundColor The background color used at the start of the gradient of this
365 * [Card]
366 * @param endBackgroundColor The background color used at the end of the gradient of this [Card]
367 * @param gradientDirection Whether the cards gradient should be start to end (indicated by
368 * [LayoutDirection.Ltr]) or end to start (indicated by [LayoutDirection.Rtl]).
369 */
370 @Composable
cardBackgroundPainternull371 public fun cardBackgroundPainter(
372 startBackgroundColor: Color =
373 MaterialTheme.colors.primary
374 .copy(alpha = 0.30f)
375 .compositeOver(MaterialTheme.colors.background),
376 endBackgroundColor: Color =
377 MaterialTheme.colors.onSurfaceVariant
378 .copy(alpha = 0.20f)
379 .compositeOver(MaterialTheme.colors.background),
380 gradientDirection: LayoutDirection = LocalLayoutDirection.current
381 ): Painter {
382 return BrushPainter(
383 FortyFiveDegreeLinearGradient(
384 colors = listOf(startBackgroundColor, endBackgroundColor),
385 ltr = gradientDirection == LayoutDirection.Ltr
386 )
387 )
388 }
389
390 /**
391 * Creates a [Painter] for the background of a [Card] that displays an Image with a scrim over
392 * the image to make sure that any content above the background will be legible.
393 *
394 * An Image background is a means to reinforce the meaning of information in a Card, e.g. To
395 * help to contextualize the information in a TitleCard
396 *
397 * Cards should have a content color that contrasts with the background image and scrim
398 *
399 * @param backgroundImagePainter The [Painter] to use to draw the background of the [Card]
400 * @param backgroundImageScrimBrush The [Brush] to use to paint a scrim over the background
401 * image to ensure that any text drawn over the image is legible
402 */
403 @Composable
imageWithScrimBackgroundPainternull404 public fun imageWithScrimBackgroundPainter(
405 backgroundImagePainter: Painter,
406 backgroundImageScrimBrush: Brush =
407 Brush.linearGradient(
408 colors =
409 listOf(
410 MaterialTheme.colors.surface.copy(alpha = 1.0f),
411 MaterialTheme.colors.surface.copy(alpha = 0f)
412 )
413 )
414 ): Painter {
415 return ImageWithScrimPainter(
416 imagePainter = backgroundImagePainter,
417 brush = backgroundImageScrimBrush
418 )
419 }
420
421 private val CardHorizontalPadding = 12.dp
422 private val CardVerticalPadding = 12.dp
423
424 /** The default content padding used by [Card] */
425 public val ContentPadding: PaddingValues =
426 PaddingValues(
427 start = CardHorizontalPadding,
428 top = CardVerticalPadding,
429 end = CardHorizontalPadding,
430 bottom = CardVerticalPadding
431 )
432
433 /** The default size of the app icon/image when used inside a [AppCard]. */
434 public val AppImageSize: Dp = 16.dp
435 }
436
437 /** A linear gradient that draws the gradient at 45 degrees from Top|Start. */
438 @Immutable
439 internal class FortyFiveDegreeLinearGradient
440 internal constructor(
441 private val colors: List<Color>,
442 private val stops: List<Float>? = null,
443 private val tileMode: TileMode = TileMode.Clamp,
444 private val ltr: Boolean
445 ) : ShaderBrush() {
446
createShadernull447 override fun createShader(size: Size): Shader {
448 val minWidthHeight = min(size.height, size.width)
449 val from = if (ltr) Offset(0f, 0f) else Offset(minWidthHeight, 0f)
450 val to = if (ltr) Offset(minWidthHeight, minWidthHeight) else Offset(0f, minWidthHeight)
451 return LinearGradientShader(
452 colors = colors,
453 colorStops = stops,
454 from = from,
455 to = to,
456 tileMode = tileMode
457 )
458 }
459
equalsnull460 override fun equals(other: Any?): Boolean {
461 if (this === other) return true
462 if (other !is FortyFiveDegreeLinearGradient) return false
463
464 if (colors != other.colors) return false
465 if (stops != other.stops) return false
466 if (tileMode != other.tileMode) return false
467 if (ltr != other.ltr) return false
468
469 return true
470 }
471
hashCodenull472 override fun hashCode(): Int {
473 var result = colors.hashCode()
474 result = 31 * result + (stops?.hashCode() ?: 0)
475 result = 31 * result + tileMode.hashCode()
476 return result
477 }
478
toStringnull479 override fun toString(): String {
480 return "FortyFiveDegreeLinearGradient(colors=$colors, " +
481 "stops=$stops, " +
482 "tileMode=$tileMode)"
483 }
484 }
485