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