Skip to content

Object Cell

Object Cell is a list item view that can represent a business object. Object Cell can be used inside a RecyclerView, ListView, or other ViewGroups. The example code below assumes the parent view is RecyclerView.

Anatomy

An Object Cell consists of an unread object indicator, icon stack, image, labels (headline, subheadline, and footnote), tags, description, statuses, attribute icons, avatar grid, and a secondary action.

Object Cell Anatomy
Object Cell Anatomy

Example

Object Cell Example
Object Cell on a Tablet

Selection Modes

Object Cell now supports selection modes which include:

  • Tap or click
  • Long press
  • Read
  • Swiped
Object Cell in Selection Mode
Object Cell in Selection Mode

Construction

Object Cell can be created by the constructor in code:

ObjectCell cell = new ObjectCell(getContext());

Object Cell can also be created by declaring an ObjectCell element in XML:

<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=".50"
    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.

public ViewHolder onCreateViewHolder(
        ViewGroup parent, int viewType) {
    Context context = parent.getContext();
    LayoutInflater inflater = LayoutInflater.from(context);
    View view = inflater.inflate(R.layout.object_cell, parent,
                false);
    return new ViewHolder(view);
}

This XML declaration creates an ObjectCell with the following attributes:

  • app:lines="3" – Three lines of text enabled, with room for headline, subheadline, and footnote. The description field can display at most three lines of text. Set it to "0" to dynamically calculate the Object Cell height if the contents vary significantly from cell to cell.
  • app:asyncRendering="true" – On a tablet, the description field will delay the rendering and display "Loading..." first. This is a performance tuning approach to speed up frame rendering during scrolling, since multiple line text measurement and layout is expensive. This has no effect on a phone.
  • app:descriptionWidthPercent=".50" – On a tablet, 50% of the total Object Cell width will be the start position for the description. Note that the meaning of this attribute has changed, starting with version 2.0, to ensure the headline/description divider line stays consistent across all cells in a list.
  • app:statusWidth – Status fields use the specified width. If status lengths vary significantly, use app:dynamicStatusWidth="true" instead.
  • app:preserveDetailImageSpacing="true" – When an image for the ObjectCell is missing, the space will be preserved to maintain consistent layout. A letter can be specified as the placeholder. See Layout Variations below for other layout behavior.

Fields

Unread Object Indicator

An icon in the margin that represents whether an object is read or unread. If an object is read, the indicator will not be displayed.

cell.setUnreadIcon(R.drawable.ic_circle_unread);

Icon Stack

A set of up to two vertically-stacked icons can be displayed to the right of the unread object indicator. These icons should represent persistent information about the object. Both image and text can be used as an icon.

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

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

<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 the Fiori UI library, a common pattern is to use the app:layout_group attribute to assign the child view into the correct group.

Image

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

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 an 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 a content description, for example: cell.setDetailImageDescription(R.string.avatar).

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

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

Two images can be displayed together in the form of a FioriAvatarStack. There are two options for managing the images in an avatar stack. The first option assigns each Drawable using an index.

cell.setDetailImage(0, R.drawable.image1); //use drawable resource int ID
cell.setDetailImage(1, getResources().getDrawable(R.drawable.image2)); //use drawable object

The second option is creating a list of FioriAvatar objects.

ArrayList<FioriAvatar> avatarStackList = new ArrayList<>();

FioriAvatar avatar1 = new FioriAvatar(getContext()); //use Glide to load an image into avatar1.getImageView()
mGlide.load(obj.getDetailImageUri()).apply(cropOptions).into(avatar1.getImageView());
avatarStackList.add(avatar1);

FioriAvatar avatar2 = new FioriAvatar(getContext()); //use FioriAvatar's setImage(Drawable) or setImage(int) function
avatar2.setImage(R.drawable.image2);
avatarStackList.add(avatar2);

cell.setAvatarStack(avatarStackList);

FioriAvatar has additional customization options.

FioriAvatar avatar = new FioriAvatar(getContext());

// single avatar size; if stack has two avatars, size depends on cell.setImageSize()
avatar.setImageSize(100);

// badging in corner; if stack has two avatars, badge will not be visible
avatar.setBadge(R.drawable.avatar_badge);

// color behind text and icon
avatar.setShapeColor(getResources().getColor(R.color.shape_color, null));

// text and icon color
avatar.setTextColor(getResources().getColor(R.color.text_color, null));

// border around avatar
avatar.setUseBorder(true);

// drawable icon automatically resized to fit within center
avatar.setUseIcon(true);

// 0: Rectangle; 1: Oval; 2: Rounded Rectangle
avatar.setImageOutlineShape(1);

// Toggle off badge cutout effect if performance is dropping
avatar.setUseBadgeCutOut(false);
Object Cell Avatars Example
Avatar Variations

Avatar Grid

The avatar grid is a FioriAvatarStack that displays a row of FioriAvatars at the bottom of the cell.

ArrayList<FioriAvatar> avatarGridList = new ArrayList<>();
for (int i = 0; i < 9; ++i) {
    FioriAvatar avatar = new FioriAvatar(getContext());
    // FioriAvatar customization here
    // In a grid, the size of each avatar is fixed at 16dp, and no badging will be displayed
    avatarGridList.add(avatar);
}

// set max of 7 avatars in the grid; 6 will display the assigned image, 1 will display "+3"
cell.setAvatarGridMax(7);
cell.setAvatarGrid(avatarGridList);

// Toggle off avatar stack and avatar grid cutout effect if performance is dropping
cell.setUseCutOut(false);

Headline

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

cell.setHeadline(obj.getHeadline());

Subheadline

The subheadline is under the headline and provides additional information.

cell.setSubheadline(obj.getSubheadline());

Footnote

The footnote is under the subheadline and provides further information.

cell.setFootnote(obj.getFootnote());

Tags

Tags may be used to indicate categories, types, or statuses. Tags are displayed next to the footnote if space is available.

cell.setTag(R.string.object_cell_tag1, 0);
cell.setTagTextColor(ObjectCell.PINK_DARK, 0);
cell.setTagTint(ObjectCell.PINK_LIGHT, 0);

cell.setTag(R.string.object_cell_tag2, 1);
cell.setTagTextColor(ObjectCell.INDIGO_DARK, 1);
cell.setTagTint(ObjectCell.INDIGO_LIGHT, 1);

Description

The description is typically a longer string of text than what is displayed in the title content.

cell.setDescription(obj.getSubheadline());

Statuses

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

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

If the status is an image, text can be displayed next to it:

cell.setStatusLabel("Status label", 0);

Or, in XML:

<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"
    />

Attribute Icons

A set of icons can be displayed horizontally under the statuses. These icons can represent both persistent and variable information about the object. It is recommended to have at most four attribute icons. The icons are displayed from right to left, with the lowest index position on the right.

cell.setAttributeIconColor(BaseObjectCellActivity.sSapUiNeutralText, 0);
cell.setAttributeIcon(R.drawable.ic_emoji_events_black_24dp, 0, R.string.object_cell_icon_event_desc);

Alternatively, an attribute icon can be specified using XML inside the <ObjectCell> element.

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_group="ATTRIBUTE_ICON"
    android:src="@drawable/ic_emoji_events_black_24dp"
    />

Secondary Action

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

cell.setSecondaryActionIcon(R.drawable.ic_cloud_download_black_24dp);
cell.setSecondaryActionIconDescription(R.string.download);
cell.setSecondaryActionOnClickListener(new View.OnClickListener() {
    ...
    }
});

Style

The style of the component can be customized on the application level or the component level. The recommended approach is to extend the default Theme.Fiori.Horizon and only customize the attributes you desire. This means that other attributes can be retained and all the components in the application will have a consistent look and feel. For example:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.Fiori.Horizon">
    <!-- 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. In this example, we're only customizing the Theme.Fiori.Horizon 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 fields in ObjectCell will keep the default style.

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

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

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

    <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"
        ...

Layout Variations

Container and Preserved Spacing

The container refers to the virtual bounding box which contains the icon stack and the image. To maintain this virtual container, whether an icon stack or image is provided, call setPreserveIconImageContainer(true) so that the headline will always start from the same horizontal position. Similarly, there are other APIs and attributes that can toggle the preserving behavior for other ObjectCell fields.

The following APIs/XML attributes are provided:

  • setPreserveIconStackSpacing/preserveIconStackSpacing – When this is true, the icon stack space will be preserved even if icons are not provided.
  • setPreserveDetailImageSpacing/preserveDetailImageSpacing – When this is true, the detail image space will be preserved even if it's not provided.
  • setPreserveIconImageContainer/preserveIconImageContainer – Preserves the combined container space including the icon stack and the detail image. When this is true, preserveIconStackSpacing and preserveIconImageContainer will be ignored. Also, contents in the container will be right-aligned, close to the headline (in a left-to-right layout direction).
  • setPreserveDescriptionSpacing/preserveDescriptionSpacing – When this is true, the headline cannot use the description space even if the description is null.
  • setDynamicStatusWidth/dynamicStatusWidth – When this is true, the status width will be dynamic, up to maxStatusWidth. statusWidth will be ignored.

Combining these APIs/attributes can customize how ObjectCell uses the horizontal space. For example, maximum real estate usage can be achieved using the following configuration:

<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="0"
    app:descriptionWidthPercent=".50"
    app:dynamicStatusWidth="true"
    app:preserveDescriptionSpacing="false"
    app:preserveIconImageContainer="false"
    app:preserveIconStackSpacing="false"
    app:preserveDetailImageSpacing="false">

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

Data Binding

Because ObjectCell supports setting attributes and fields in XML, Android Data Binding can easily be applied. For example:

<?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 are 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 a BizObject instance called bo. ObjectCell view accesses the bo object to render the view state. BindingAdapter can be used when the 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:

    @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:

<ImageView
    ...
    app:contentDescription="@{bo.statusId}"
    ...

The demo ObjectCellBindingAdapter can also load the image via Glide, convert data, handle a click event and so on. Here is the complete source code:

@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();
    }
}

Using this setup, the RecyclerView.Adapter can be simplified, as most of the work is now carried out by the data binding mechanism.

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

For more information, see ObjectCell in the API Reference documentation.

Enabling READ and SELECTION Modes

Simple Tap/Click to Enable Read Mode

By default, Object Cell is not clickable. In order to support the touch event of Tap or Click, the following Android XML attributes should be added to Object Cell:

    android:clickable="true"
    android:background="@drawable/fiori_ripple_selected"

Upon clicking or tapping an Object Cell, the entire cell will display a ripple effect and then the details about the clicked Object Cell will open up as a new activity. Returning to the list of Object Cells, the clicked Object Cell will be marked as Read, i.e. the font of the Object Cell's headline changes from bold to regular, and the unread object indicator will disappear.

Default Object Cell {.ui-image }
Object Cell in Default Mode
Default Object Cell {.ui-image }
Object Cell in Read Mode

To disable the unread object indicator and maintain the same headline font regardless of whether the cell has been read or not, pass false into setIsReadStateEnabled(boolean) or set the XML attribute isReadStateEnabled:

    app:isReadStateEnabled="false"

Long Press to Enable Selection Mode

An Object Cell can enter and exit Selection mode using the setIsSelectable(boolean) method. The Object Cell gains a checkbox in Selection mode, which can be toggled using the setCheckBox(boolean) method.

The XML attribute backgroundSelected defines the background color of an Object Cell when true is passed into setIsSelected(boolean).

    app:backgroundSelected="@color/selectedColor"
Selected Object Cell {.ui-image }
Object Cell in Selection Mode

Enabling Actions on Swipe

Object Cell can also be swiped from left to right or from right to left. App developers can define their own actions in response to the swipe gesture in two directions. In the demo app provided by Fiori UI, a DELETE action is defined for swiping from right to left, and a mock ARCHIVE action is defined for swiping from left to right.

Left-to-right Object Cell {.ui-image }
Object Cell Swiped from Left to Right
Right-to-left Object Cell {.ui-image }
Object Cell Swiped from Right to Left

After the swipe gesture is completed in the demo application, a toast message displays on the bottom of the screen to indicate the completion of the swipe gesture and allow the end user to restore the deleted Object Cell to the list.

Swipe Undo {.ui-image }
Toast Message to Undo Deletion of the Object Cell
Swipe Undeleted {.ui-image }
Deleted Object Cell Returns to the List

Usage

As most of the functionality related to selection modes has been implemented in Fiori UI Object Cell component, there are some additional code changes needed to enable these features at the app level. The following section focuses on how to add selection modes into an activity using RecyclerView.

SELECTED/UNSELECTED and READ

RecyclerView ViewHolder

The recycler view's viewholder must implement the onClick and onLongClick methods of the View.OnClickListener interface in order to detect a Tap/Click or Long Press gesture on the list of Object Cells.

       public static class ViewHolder extends AbstractObjectCellRecyclerAdapter.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
            public Target target;
            private ClickListener listener;
            public ViewHolder(@NonNull View itemView) {
                super(itemView);
            }
            public ViewHolder(@NonNull View itemView, ClickListener listener) {
                super(itemView);
                itemView.setOnClickListener(this);
                itemView.setOnLongClickListener(this);
                this.listener = listener;
            }
            /* Implement the onClick method of View.OnClickListener interface */
            @Override
            public void onClick(View v) {
                Log.d(TAG, "Item clicked at position " + getAdapterPosition());
                if(listener!=null) {
                    listener.onItemClicked(getAdapterPosition());
                }
            }
            /* Implement the onLongClick method of View.OnLongClickListener interface */
            @Override
            public boolean onLongClick(View v) {
                Log.d(TAG, "Item long-clicked at position " + getAdapterPosition());
                if(listener!=null) {
                    listener.onItemLongClicked(getAdapterPosition());
                }
                return true;
            }
        }

RecyclerView Adapter

In the recycler view adapter, two more member variables should be added to keep track of what is selected versus what is read, as well as a variable to keep track of whether the recycler view's cells should all be in Selection mode:

        protected SparseBooleanArray mSelectedObjects = new SparseBooleanArray();
        protected SparseBooleanArray mReadObjects = new SparseBooleanArray();
        protected boolean mIsMultiSelect;

In onBindViewHolder(), the statuses of isSelectable, isSelected, and isRead for each cell should be set according to mIsMultiSelect and the Boolean values in the above two SparseBooleanArrays, and the display style of the cell will be updated accordingly.

        public void onBindViewHolder(ViewHolder holder, int position) {
            BizObject obj = mObjects.get(position);
            ObjectCell cell = holder.objectCell;

            /* Have cells display checkboxes */
            cell.setIsSelectable(mIsMultiSelect);
            cell.setCheckBox(mSelectedObjects.get(position, false));

            /* Set cell's isSelected and isRead statuses*/
            cell.setIsSelected(mSelectedObjects.get(position, false));
            cell.setIsRead(mReadObjects.get(position, false));
            /* Set up the object cell */
            ....

            /* Depending on the cell's isRead and isSelected statuses, update UI accordingly */
            cell.updateUiOnRead();
            cell.updateUiOnSelected();
        }

The two SparseBooleanArrays are updated in response to touch events. Therefore, methods to update the two arrays need to be implemented in the RecyclerViewAdapter:

        public void objectRead(int adapterPosition) {
            mReadObjects.put(adapterPosition, true);
        }
        public void objectUnread(int adapterPosition) {
            mReadObjects.delete(adapterPosition);
        }
        public void objectSelected(int adapterPosition) {
            mSelectedObjects.put(adapterPosition, true);
        }
        public void objectUnselected(int adapterPosition) {
            mSelectedObjects.delete(adapterPosition);
        }

For a list view, similar changes should be applied to its list view adapter.

Activity

The updating methods in the adapter should be called on the touch event handlers for Tap/Click and Long Press in the activity. An example of the implementation for these event handlers is as follows:

   @Override
   public void onItemLongClicked(int adapterPosition) {
       final ObjectCellRecyclerAdapter.ViewHolder viewHolder = (ObjectCellRecyclerAdapter.ViewHolder) mRecyclerView.findViewHolderForAdapterPosition(adapterPosition);
       if (viewHolder != null) {
           ObjectCell cell = ((ObjectCell) viewHolder.itemView);
           if (!cell.getIsSelected()) {
               mAdapter.setIsMultiSelect(true);
               cell.setIsSelected(true);
               mAdapter.objectSelected(adapterPosition);
           } else {
               mAdapter.setIsMultiSelect(false);
               cell.setIsSelected(false);
               mAdapter.objectUnselected(adapterPosition);
           }
           cell.updateUiOnSelected();
           mAdapter.notifyItemChanged(adapterPosition);
       }
   }
   @Override
   public void onItemClicked(int adapterPosition) {
       final ObjectCellRecyclerAdapter.ViewHolder viewHolder = (ObjectCellRecyclerAdapter.ViewHolder) mRecyclerView.findViewHolderForAdapterPosition(adapterPosition);
       if (viewHolder != null) {
           mClickedPos = adapterPosition;
           if (!mAdapter.getIsMultiSelect()) {
               mAdapter.objectRead(adapterPosition);
               Context context = viewHolder.itemView.getContext();
               Intent intent = new Intent(context, ObjectHeaderActivity.class);
               context.startActivity(intent);
           } else {
               ObjectCell cell = ((ObjectCell) viewHolder.itemView);
               if (cell.getIsChecked()) {
                   mAdapter.objectUnselected(adapterPosition);
               } else {
                   mAdapter.objectSelected(adapterPosition);
               }
               cell.updateUiOnSelected();
               mAdapter.notifyItemChanged(adapterPosition);
           }
       }
   }

DELETE/RESTORE on Swipe

The first step to implementing swiping functionality is to have the activity implement ObjectCellSwipeControl.ObjectCellSwipeListener:

public class ObjectCellActivity implements ObjectCellSwipeControl.ObjectCellSwipeListener

Next, create and attach ObjectCellSwipeControl:

mSwipeControl = new ObjectCellSwipeControl(this, this);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mSwipeControl);
itemTouchHelper.attachToRecyclerView(mRecyclerView);

Depending on the action(s) defined on the swipe gesture, appropriate functionality should be implemented in the recycler view adapter. In the demo application, an Object Cell is deleted after being swiped from right to left and can be restored by clicking the Undo button on the toast message.

An example of the implementation for DELETE and RESTORE an Object Cell in the recycler view adapter is as follows:

        public void removeItem(int position) {
            mReadObjects = updateObjectsOnRemoveItem(position, mReadObjects);
            mSelectedObjects = updateObjectsOnRemoveItem(position, mSelectedObjects);
            mObjects.remove(position);
            notifyItemRemoved(position);
            notifyItemRangeChanged(position, getItemCount());
        }
        public void restoreItem(Object removedItem, int position){
            mObjects.add(position, (BizObject) removedItem);
            mReadObjects = updateObjectsOnRestoreItem(position, mReadObjects);
            mSelectedObjects = updateObjectsOnRestoreItem(position, mSelectedObjects);
            notifyItemInserted(position);
            notifyItemRangeChanged(position, getItemCount());
        }

Note that, in order for the list of cells to behave properly, the two SparseBooleanArrays for SELECTED and READ modes should be updated properly when a cell is being removed or restored to accurately reflect the current state of each cell.

In the activity's onSwipeToLeft() event handler, the adapter's removeItem() and restoreItem() methods are being called in response to a swipe and an Undo button click on the toast message, respectively.

    @Override
    public void onSwipeToLeft(RecyclerView.ViewHolder viewHolder, int position) {
        if (viewHolder instanceof AbstractObjectCellRecyclerAdapter.ViewHolder) {
            if (position < mAdapter.getObjects().size()) {}
                final BizObject deletedItem = mAdapter.getObjects().get(position);
                mAdapter.removeItem(position);
                Snackbar snackbar = Snackbar.make(mConstraintLayout, R.string.object_cell_item_deleted, Snackbar.LENGTH_LONG);
                snackbar.setAction(R.string.object_cell_snackbar_undo_button, new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        // undo is selected, restore the deleted item
                        mAdapter.restoreItem(deletedItem, position);
                    }
                });
                snackbar.setActionTextColor(sSapUiSnackbarToastButtonText);
                snackbar.show();
            }
        }
    }

For different actions, a similar pattern can be applied in the onSwipeToLeft() event handler or the onSwipeToRight() event handler.

The icons, background colors, distances, and sensitivities of the swipe gestures can be configured through the provided functions in ObjectCellSwipeControl:

int leftBackgroundColor = ContextCompat.getColor(this, R.color.sap_ui_positive_text);
int rightBackgroundColor = ContextCompat.getColor(this, R.color.sap_ui_negative_text);
Drawable leftIconDrawable = ContextCompat.getDrawable(this, R.drawable.ic_document_white_24dp);
Drawable rightIconDrawable = ContextCompat.getDrawable(this, R.drawable.ic_delete_white_24dp);
mSwipeControl.setLeftBackgroundColor(leftBackgroundColor);
mSwipeControl.setRightBackgroundColor(rightBackgroundColor);
mSwipeControl.setLeftIcon(leftIconDrawable);
mSwipeControl.setRightIcon(rightIconDrawable);
mSwipeControl.setSwipeToLeftRatio(1.0f); // the cell will be swiped its full width
mSwipeControl.setSwipeToRightRatio(.25f); // the cell will be swiped up to 25% of its width
mSwipeControl.setSwipeThreshold(.8f); // assuming no flicking, the swipe gesture is registered if the cell moves past the point that is 80% of its swiping distance

To have the icons and background colors vary between different cells depending on certain conditions, use addLeftMapping() and addRightMapping() along with getLeftKey() and getRightKey() to define the conditions and their corresponding icons and background colors:

public void configureMappings() {
    int leftBackgroundColor_1 = ContextCompat.getColor(this, R.color.sap_ui_positive_text);
    int leftBackgroundColor_2 = ContextCompat.getColor(this, R.color.sap_ui_critical_text);
    Drawable leftIconDrawable_1 = ContextCompat.getDrawable(this, R.drawable.ic_document_white_24dp);
    Drawable leftIconDrawable_2 = ContextCompat.getDrawable(this, R.drawable.ic_settings_white_24dp);
    mSwipeControl.addLeftMapping("DEFAULT", leftBackgroundColor_1, leftIconDrawable_1);
    mSwipeControl.addLeftMapping("SELECTED", leftBackgroundColor_2, leftIconDrawable_2);
}

@Override
public void getLeftKey(RecyclerView.ViewHolder viewHolder) {
    if (((ObjectCell)viewHolder.itemView).getIsSelected()) {
        return "SELECTED";
    } else {
        return "DEFAULT";
    }
}

Last update: June 23, 2023