Skip to content

Object Cell

Object Cell is a list item view that can represent a business object.

Anatomy

An Object Cell consists of icon stack, image, labels (headline, sub headline, footnote), description, statuses, and a secondary action. Object Cell Anatomy

Example

Here is an example of Object Cells on a tablet: Object Cell Example

Usage

Object Cell can be used inside a RecyclerView, ListView or other ViewGroups. The example code below will assume the parent view is RecyclerView.

Construction

Object Cell can be created either by the constructor in code or by declaring an ObjectCell element in XML like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<com.sap.cloud.mobile.fiori.object.ObjectCell xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:lines="3"
    app:asyncRendering="true"
    app:descriptionWidthPercent=".60"
    app:statusWidth="@dimen/demo_object_cell_status_width"
    app:preserveDetailImageSpacing="true">

    </com.sap.cloud.mobile.fiori.object.ObjectCell>

Then the XML can be inflated in a RecyclerView.Adapter.

1
2
3
4
5
6
7
public ViewHolder onCreateViewHolder(
        ViewGroup parent, int viewType) {
    Context context = parent.getContext();
    View view = inflater.inflate(R.layout.object_cell, parent,
                false);
    return new ViewHolder(view);
}

The above XML declaration creates an ObjectCell with the following attributes:

  • app:lines="3" 3 lines of text enabled, with room for headline, sub headline and footnote. On tablet, description field can show at most 3 lines of text. (description is hidden on a phone.)
  • app:asyncRendering="true" On tablet, description field will delay the rendering and show "Loading..." first. This is a performance tuning approach to speed up frame rendering during scrolling since multiple line text measurement and layout is expensive. No effect on phone.
  • app:descriptionWidthPercent=".60" 60% horizontal space will be used for description, 40% will be used for headline. This percentage calculation excludes other fields, like image and statuses.
  • app:statusWidth Status fields will use the specified width. This ensures consistent layout across all the ObjectCells in the RecyclerView.
  • app:preserveDetailImageSpacing="true" When image for the ObjectCell is missing, the space will be preserved to maintain consistent layout. A letter can be specified as the placeholder.

See Object Cell XML attributes for all the XML attributes supported by ObjectCell.

Fields

Icon Stack

A set of up to 3 vertically stacked icons can be displayed on the far left. These icons indicate something about the object, such as its unread status or that it has attachments. Both image and text can be used as an icon.

1
2
cell.setIconColor(BaseObjectCellActivity.sSapUiNegativeText, 0);
cell.setIcon(R.drawable.ic_lock_outline_black_24dp, 0, R.string.object_cell_icon_protection_desc);

Or, icon stack can be specified in XML inside the <ObjectCell> element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_group="ICON_STACK"
    android:src="@drawable/ic_locked"
    />
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_group="ICON_STACK"
    android:text="@string/testInfo1"/>

Note

When specifying child elements for ObjectCell or other compound components provided by Fiori UI library, a common pattern is to use app:layout_group attribute to assign the child view into correct group.

Image

An image provides visual representation of the object and is highly recommended. Glide can be used to streamline image download and decryption without blocking UI.

1
2
3
4
5
RequestOptions cropOptions = new RequestOptions().placeholder(
        R.drawable.rectangle);
holder.target = mGlide.load(obj.getDetailImageUri()).apply(cropOptions).into(
        cell.prepareDetailImageView());
cell.setDetailImageDescription(R.string.avatar);

Note

For Glide to asynchronously load image into a view, the view instance must be created beforehand. cell.prepareDetailImageView() can make sure the image view exists. Also, to improve app accessibility, all images should provide content description, thus cell.setDetailImageDescription(R.string.avatar).

If image is missing, a letter can be used as placeholder instead.

1
2
cell.setDetailImage(null);//make sure we're not showing recycled images
cell.setDetailImageCharacter(obj.getHeadline().substring(0, 1));

Headline

The headline/title is the main area for text content. The headline is the only mandatory content for ObjectCell. Headline can be specified by:

1
cell.setHeadline(obj.getHeadline());

Sub Headline

The sub headline is under headline to show more information.

1
cell.setSubheadline(obj.getSubheadline());

Footnote

The footnote is under sub headline and shows further information.

1
cell.setFootnote(obj.getFootnote());

Description

If a description has been defined, it will appear in regular mode only. This is typically a longer string of text than what is displayed in the title content.

1
cell.setDescription(obj.getSubheadline());

Statuses

Up to two attributes can be displayed, stacked vertically, to show attributes of the object. A status could be either text or image.

1
2
cell.setStatusColor(BaseObjectCellActivity.sSapUiNegativeText, 0);
cell.setStatus(R.drawable.ic_error, 0, statusDescId);

Or, in XML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_group="STATUS"
    android:src="@drawable/ic_error_black_24dp"
    />
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_group="STATUS"
    android:text="@string/testStatus1"/>

Secondary Action

The secondary action is usually an information disclosure icon, which would bring up a model dialog. It can also be used for actions, such as download.

1
2
3
4
5
6
cell.setSecondaryActionIcon(R.drawable.ic_cloud_download_black_24dp);
cell.setSecondaryActionIconDescription(R.string.download);
cell.setSecondaryActionOnClickListener(new View.OnClickListener() {
    ...
    }
});

Style

Style of the component can be customized on application level or component level. The recommended approach is to extend the default FioriTheme and only customize the attributes you desired, so that other attributes can be retained, and all the components in the application will have consistent look and feel. Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- Base application theme. -->
<style name="AppTheme" parent="FioriTheme">
    <!-- Customize your theme here. -->
    <item name="objectCellStyle">@style/TestObjectCell</item>
</style>
<style name="AppTheme.NoActionBar" parent="AppTheme">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="FioriTheme.AppBarOverlay" />
<style name="AppTheme.PopupOverlay" parent="FioriTheme.PopupOverlay" />
<style name="TestObjectCell" parent="ObjectCell">
    <item name="headlineTextAppearance">@style/Test.ObjectCell.Headline</item>
</style>
<style name="Test.ObjectCell.Headline" parent="TextAppearance.Fiori.ObjectCell.Headline">
    <item name="android:textColor">@color/colorPrimaryDark</item>
    <item name="android:textStyle">italic</item>
</style>

AppTheme will be your application main theme, while AppTheme.NoActionBar can be used when the default toolbar is not used. Here we're only customizing the FioriTheme with a new objectCellStyle, which in turn only customizes the headline text appearance. The headline of ObjectCell is still going to use the default font and size defined in TextAppearance.Fiori.ObjectCell.Headline and all other fileds in ObjectCell will keep the default style.

The AppTheme can then be applied to your application in AndroidManifest.xml:

1
2
3
4
5
    <application
        android:name=".DemoApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

On the other hand, if just one instance of the ObjectCell needs to be customized, you can set the text appearances as this:

1
2
3
4
5
6
7
8
    <com.sap.cloud.mobile.fiori.object.ObjectCell
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:headlineTextAppearance="@style/Test.ObjectCell.Headline"
        app:subheadlineTextAppearance="@style/Test.ObjectCell.Subheadline"
        app:footnoteTextAppearance="@style/Test.ObjectCell.Footnote"
        app:descriptionTextAppearance="@style/Test.ObjectCell.Description"
        ...

Data Binding

Since ObjectCell supports setting attributes and fields in xml, Android Data Binding can be easily applied. Here is an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <import type="com.sap.cloud.mobile.fiori.demo.object.ObjectCellBindingAdapter"/>
        <variable
            name="bo"
            type="com.sap.cloud.mobile.fiori.demo.object.BizObject" />
    </data>

    <com.sap.cloud.mobile.fiori.object.ObjectCell
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:lines="3"
        app:descriptionWidthPercent=".60"
        app:statusWidth="@dimen/demo_object_cell_status_width"
        app:headlineTextAppearance="@style/Test.ObjectCell.Headline"
        app:subheadlineTextAppearance="@style/Test.ObjectCell.Subheadline"
        app:footnoteTextAppearance="@style/Test.ObjectCell.Footnote"
        app:descriptionTextAppearance="@style/Test.ObjectCell.Description"
        app:headline="@{bo.headline}"
        app:subheadline="@{bo.subHeadline}"
        app:footnote="@{bo.footnote}"
        app:description="@{bo.description}"
        app:detailImage="@{bo}"
        app:detailImageShape="oval"
        app:preserveDetailImageSpacing="true"
        app:secondaryActionIcon="@drawable/ic_more_vert_black_24dp"
        app:secondaryActionDescription="@string/more"
        app:actionTopAlign="true"
        app:onActionClick="@{(theView) -> ObjectCellBindingAdapter.showInfo(theView, bo)}"
        app:asyncRendering="true"
        >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_group="ICON_STACK"
            android:textColor="@color/sap_ui_content_label_color"
            android:text='@{""+bo.pendingTasks}'/>

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_group="ICON_STACK"
            android:tint="@color/sap_ui_content_non_interactive_icon_color"
            android:src="@{bo.protected ? @drawable/ic_lock_outline_black_24dp : @drawable/ic_lock_open_black_24dp}"
            android:contentDescription='@{"Protected: " + bo.protected}'/>

        <!--Note app:src and app:contentDescription is handled by ObjectCellBindingAdapter-->
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_group="STATUS"
            app:src="@{bo.statusId}"
            android:tint="@color/sap_ui_neutral_text"
            app:contentDescription="@{bo.statusId}"/>
        <!--When there is secondary action, only one status view would appear.
        To show 2 statuses, comment out secondaryAction.-->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{bo.priority}"
            android:textColor="@color/sap_ui_neutral_text"
            app:layout_group="STATUS"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Should not see me because layout_group is not set!"/>
    </com.sap.cloud.mobile.fiori.object.ObjectCell>
</layout>

In this example, the model object is an BizObject instance called bo. ObjectCell view access the bo object to render the view state. BindingAdapter can be used when model object properties cannot be directly mapped to view attributes. For example, BizObject has the status icon resource id, but the contentDescription of the status ImageView requires a String resource id, this conversion can be done in BindingAdapter as this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    @BindingAdapter("contentDescription")
    public static void setContentDescription(ImageView view, int iconId) {
        int statusDescId = android.R.string.ok;

        if (iconId == R.drawable.ic_error_black_24dp){
            statusDescId = R.string.error;
        }else if (iconId == R.drawable.ic_warning_black_24dp){
            statusDescId = R.string.warning;
        }
        view.setContentDescription(view.getResources().getString(statusDescId));
    }

Then, in the xml, the data binding can be as simple as:

1
2
3
4
<ImageView
    ...
    app:contentDescription="@{bo.statusId}"
    ...

The demo ObjectCellBindingAdapter also does things like loading image via Glide, converting data, handling click event etc. Here is the complete source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@BindingMethods({
        @BindingMethod(type = ObjectCell.class, attribute = "onActionClick", method = "setSecondaryActionOnClickListener"),
})
public class ObjectCellBindingAdapter {

    @BindingAdapter("contentDescription")
    public static void setContentDescription(ImageView view, int iconId) {
        int statusDescId = android.R.string.ok;

        if (iconId == R.drawable.ic_error_black_24dp){
            statusDescId = R.string.error;
        }else if (iconId == R.drawable.ic_warning_black_24dp){
            statusDescId = R.string.warning;
        }
        view.setContentDescription(view.getResources().getString(statusDescId));
    }


    @BindingAdapter("src")
    public static void setImageSrc(ImageView view, int resId) {
        view.setImageResource(resId);
        if (resId == R.drawable.ic_error_black_24dp){
            view.getDrawable().setTint(BaseObjectCellActivity.sSapUiNegativeText);
        }
    }

    @BindingAdapter("detailImage")
    public static void setImageUrl(ObjectCell cell, BizObject obj) {
        Context context = cell.getContext();

        if (obj.getDetailImageResId() != 0) {
            cell.setDetailImageCharacter(null);
            cell.setDetailImage(obj.getDetailImageResId());
            cell.setDetailImageDescription(R.string.avatar);
        } else if (obj.getDetailImageUri() != null) {
            RequestOptions cropOptions = new RequestOptions().placeholder(
                    R.drawable.rectangle);
            cell.setDetailImageCharacter(null);
            Glide.with(context).load(obj.getDetailImageUri()).apply(cropOptions).into(
                    cell.prepareDetailImageView());
            cell.setDetailImageDescription(R.string.avatar);
        } else {
            cell.setDetailImage(null);
            cell.setDetailImageCharacter(obj.getSubHeadline().substring(0, 1));
        }
    }

    @BindingAdapter("android:text")
    public static void setText(TextView view, Priority priority) {
        view.setText(priority.toString());
        if (priority == Priority.HIGH){
            view.setTextColor(BaseObjectCellActivity.sSapUiNegativeText);
        }
    }

    public static void showInfo(View view, BizObject obj){
        Toast toast = Toast.makeText(view.getContext(),
                "Item: " + obj.getHeadline() + " is clicked.",
                Toast.LENGTH_SHORT);
        toast.show();
    }
}

With the above setup, the RecyclerView.Adapter would become much simplified as most of the work is carried out by the data binding mechanism.

1
2
3
4
5
6
    @Override
    public void onBindViewHolder(ViewHolder holder,
            int position) {
        ObjectCellBindingBinding binding = DataBindingUtil.getBinding(holder.itemView);
        binding.setBo(mObjects.get(position));
    }

API Reference

ObjectCell