Skip to content

Fiori Horizon Object Cards

Design

Fiori Horizon Object Cards, based on the Material Card View, may consist of as little as a single Title, or as much as a section comprised of a Card Header, Content Description, and two Action Buttons. Object Cards support both Light mode and Dark mode, as determined by the device's settings.

Object Cards in Light Mode Object Cards in Dark Mode
Object Cards in Light Mode Object Cards in Dark Mode

Development

Fiori Horizon Object Cards are implemented entirely using Jetpack Compose, including a Composable FioriHorizonTheme. To understand the fundamentals of Jetpack Compose, refer to the Android Developer's documentation.

The fiori-horizon-cards module provides four UI components for application developers to use out of the box:

  • ObjectCard: a UI component to display a single Object Card.
  • ObjectCardRow: a UI component to display a series of Object Cards in a horizontally scrollable row.
  • ObjectCardColumn: a UI component to display a series of Object Cards in a vertically scrollable column.
  • ObjectCardGrid: a UI component to display a series of Object Cards in a vertically scrollable grid.

The latter three components are all responsive to different screen sizes.

Usage in an Application

This section describes how to use the Object Card components in an application. A sample Android application is bundled in this repository to demonstrate the usage of the above components on various screens.

Sample Application on a Phone in Light Mode Sample Application on a Phone in Dark Mode

Sample Android Application on a Large Tablet

1. How to Create an ObjectCard

ObjectCard is a Composable function that takes four parameters:

  • An ObjectCardData that specifies the data to be displayed on the card
  • A Modifier that is used to define the height and width of the card and/or a TestTag needed for Unit Tests. A default objectCardModifier is predefined to set the minimum height and width requirements.
  • An ObjectCardTextColors to customize the default text colors used.
  • An ObjectCardTextStyles to customize the default text styles used.
@Composable
fun ObjectCard(
    data: ObjectCardData,
    modifier: Modifier = objectCardModifier,
    textColors: ObjectCardTextColors = ObjectCardDefaults.textColors(),
    textStyles: ObjectCardTextStyles = ObjectCardDefaults.textStyles()
) {
    val uiState = rememberObjectCardUiState(data = data)
    ObjectCardContent(
        darkTheme = isSystemInDarkTheme(),
        uiState = uiState,
        modifier = modifier,
        textColors = textColors,
        textStyles = textStyles
    )
}

An object of the ObjectCardData has to be passed in from the application in order to create an ObjectCard.

@Parcelize
data class ObjectCardData(
    val title: String,
    val subtitle: String? = null,
    val footnote: String? = null,
    val status: Pair<String, ObjectCardStatusCode>? = null,
    val imageThumbnail: @RawValue ImageThumbnail? = null,
    val contentDesc: String? = null,
    val showContentDescBelowHeader: Boolean = true,
    val primaryActionButton: @RawValue ActionButton? = null,
    val secondaryActionButton: @RawValue ActionButton? = null,
    val menuItems: List<Pair<String, () -> Unit>>? = listOf(),
    val menuItemsWithIcon: @RawValue List<MenuItem>? = listOf(),
    val cardClickable: Boolean = true,
    val onCardClick: (() -> Unit)? = null,
    val cardStyles: @RawValue CardStyles? = null
) : Parcelable

Creating an ObjectCard can be as simple as the following (if there is no need to provide a custom Modifier):

    FioriHorizonTheme(darkTheme = false) {
        val objectCardData = remember { generateObjectCardData()}
        ObjectCard (
            data = objectCardData
        )
    }

You can customize the ObjectCard's style, including Action Button styles and card background color, by supplying new values in the ObjectCardData. For example, to change the background color of the ObjectCard to light gray:

val objectCardFullHeaderOnlyCustomColor = ObjectCardData(
    title = TITLE,
    subtitle = SUBTITLE,
    footnote = FOOTNOTE,
    statusButton = STATUSBUTTON,
    imageThumbnail = IMAGETHUMBNAIL,
    menuItems = MENUITEMS,
    cardStyles = CardStyles(
        0, 300, Color.LightGray, Color.DarkGray,
        null, null, null, 4.dp
    )
)

When this card is drawn on the screen, it will always have a light gray background.

Object Card with custom background color

2. How to Create an ObjectCardRow, ObjectCardColumn, or ObjectCardGrid

In a lot of use cases, an application won't display a single ObjectCard. Instead, it will display a series of Object Cards in a certain layout that could be a horizontal row, a vertical column, or a grid.

All three layouts are defined similarly as a Composable function in the fiorinextcard module, even though the implementation details differ from one another. In order to create one of these ObjectCard series layouts, an application only needs to know the signatures of these functions.

@Composable
fun ObjectCardRow(
    cardList: List<ObjectCardData>,
    screenType: Enum<ScreenType>,
    modifier: Modifier = Modifier
){
    ...
}

@Composable
fun ObjectCardColumn(
    cardList: List<ObjectCardData>,
    screenType: Enum<ScreenType>,
    modifier: Modifier = Modifier
){
    ...
}

@Composable
fun ObjectCardGrid(
    cardList: List<ObjectCardData>,
    screenType: Enum<ScreenType>,
    modifier: Modifier = Modifier
){
    ...
}

Then, to create one of the Object Card layouts, an application should pass in a list of ObjectCardData objects as well as an Enum value for ScreenType. For example:

    FioriHorizonTheme {
        var objectCardData = remember { generateObjectCardData()}
        ObjectCardRow(
            cardList = listOf(objectCardData, objectCardData, objectCardData),
            screenType = ScreenType.SMALL
        )
    }

Within the fiori-horizon-cards module, a utility Composable function is provided for any application to detect the ScreenType based on the Android Developers' recommendation documented in Support different screen sizes.

@Composable
fun getScreenType(): ScreenType {
    val screenWidth =
        LocalContext.current.resources.displayMetrics.widthPixels.dp / LocalDensity.current.density
    return when {
        screenWidth < 600.dp -> ScreenType.SMALL
        screenWidth >= 600.dp && screenWidth < 840.dp -> ScreenType.MEDIUM
        screenWidth > 840.dp -> ScreenType.LARGE
        else -> ScreenType.SMALL
    }
}

In order to create the layouts with custom text color or styles, use the following Composable functions:

@Composable
fun ObjectCardRow(
    cardList: List<ObjectCardDataCustomStyleForCollection>,
    screenType: Enum<ScreenType>,
    modifier: Modifier = Modifier
) {
    ...
}

@Composable
fun ObjectCardColumn(
    cardList: List<ObjectCardDataCustomStyleForCollection>,
    screenType: Enum<ScreenType>,
    modifier: Modifier = Modifier
) {
    ...
}

@Composable
fun ObjectCardGrid(
    cardList: List<ObjectCardDataCustomStyleForCollection>,
    screenType: Enum<ScreenType>,
    modifier: Modifier = Modifier
) {
    ...
}

Where the ObjectCardDataCustomStyleForCollection comprises of:

data class ObjectCardDataCustomStyleForCollection(
    val data: ObjectCardData,
    val textColors: @RawValue ObjectCardTextColors? = null,
    val textStyles: @RawValue ObjectCardTextStyles? = null
) : Parcelable

3. Using These Composable Components in a Non-Compose, View-Based Application

Jetpack Compose is still fairly new. Many applications are still built on the traditional view-based UI. Using a Composable component in a view-based application consists of two steps:

  1. In the XML layout, define the Composable component as a ComposeView. For example in the Fiori Next Card Demo app, in the fragment_view_based_object_card.xml, a ComposeView is defined for displaying an ObjectCardRow.

        <androidx.compose.ui.platform.ComposeView
            android:id="@+id/compose_view_alerts"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/alert_title"/>
    
  2. In the Fragment class, inside the onCreateView function, the actual Compose view can be added to the view tree programmatically while the rest of the Fragment class remains intact.

        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            _binding = FragmentViewBasedObjectCardBinding.inflate(inflater, container, false)
            val view = binding.root.rootView
            val screenType = getScreenType(requireContext())
    
            view.compose_view_alerts.apply {
    
                setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
                setContent {
    
                    // In Compose world
                    FioriHorizonTheme(darkTheme = isSystemInDarkTheme()) {
                        ObjectCardRow(objectCardList = objectCardList6, screenType = screenType)
                    }
                }
            }
    
            ...
    
            return view
        }
    

4. Customizing the ObjectCard Text Color and Style

The default colors and styles of the text in an Object Card adhere to the Fiori Horizon design. However, users can also customize them with their own colors and styles. In order to change the text color and text style, use the ObjectCardDefaults.textColors() and ObjectCardDefaults.textStyles() Composable functions respectively and pass on the required colors and styles for the different texts seen within the Object Card.

val customTitleColor = MaterialTheme.fioriHorizonAttributes.SapFioriColorSemanticCritical
val customSubtitleColor = MaterialTheme.fioriHorizonAttributes.SapFioriColorAccent4
val customFootnoteColor = MaterialTheme.fioriHorizonAttributes.SapFioriColorSemanticNegative
val customContentDescriptionColor = MaterialTheme.fioriHorizonAttributes.SapFioriColorAccent12

val customTitleStyle = FioriTextStyleH6
val customSubtitleStyle = FioriTextStyleSubtitle2
val customFootnoteStyle = FioriTextStyleSubtitle3
val customContentDescriptionStyle = FioriTextStyleBody1
val customStatusStyle = FioriTextStyleBody1

FioriHorizonTheme {
    ObjectCard(
        data = objectCardData,
        textColors = ObjectCardDefaults.textColors(
            titleColor = customTitleColor,
            subtitleColor = customSubtitleColor,
            footnoteColor = customFootnoteColor,
            contentDescriptionColor = customContentDescriptionColor
        ),
        textStyles = ObjectCardDefaults.textStyles(
            titleStyle = customTitleStyle,
            subtitleStyle = customSubtitleStyle,
            footnoteStyle = customFootnoteStyle,
            contentDescriptionStyle = customContentDescriptionStyle,
            statusStyle = customStatusStyle
        )
    )
}

Last update: February 20, 2023