Skip to content

5.x | Selection Modes

jcuypers edited this page Feb 5, 2018 · 18 revisions

In this page


In this page all selection is managed without ActionModeHelper.
Go to the page ActionModeHelper for the new way to handle selection.

Introduction

From Android®️ suggestion I decided to use view Activation state instead of view Selection state as others libraries mistakenly apply:

A view can be activated or not. Note that activation is not the same as selection. Selection is a transient property, representing the view (hierarchy) the user is currently interacting with.
Activation is a longer-term state that the user can move views in and out of. For example, in a list view with single or multiple selection enabled, the views in the current selection set are activated. (Um, yeah, we are deeply sorry about the terminology here.) The activated state is propagated down to children of the view it is set on.

Configuration

No XML configuration

You can now call the new DrawableUtils static methods to compose dynamic backgrounds, see wiki page Utils. You don't need XML anymore.

XML Configuration

Create the tap selection with a State Drawable for API < 21 (drawable/selector_item_light.xml):

<?xml version="1.0" encoding="utf-8"?>
<selector android:exitFadeDuration="@android:integer/config_longAnimTime"
    xmlns:android="http://schemas.android.com/apk/res/android">
	
    <item android:state_focused="true" android:drawable="@color/list_choice_pressed_bg_light"/>
    <item android:state_pressed="true" android:drawable="@color/list_choice_pressed_bg_light"/>
    <item android:state_activated="true" android:drawable="@color/list_choice_pressed_bg_light"/>
    <item android:drawable="@android:color/transparent"/><!-- The default idle color desired -->

</selector>

And for API >= 21 (drawable-v21/selector_item_light.xml)

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:exitFadeDuration="@android:integer/config_shortAnimTime"
        android:color="?android:attr/colorControlHighlight">

    <item android:id="@android:id/mask"
          android:drawable="@color/list_choice_pressed_bg_light" />
    <item>
        <selector>
            <item android:state_activated="true" android:drawable="@color/list_choice_pressed_bg_light"/>
        </selector>
    </item>

</ripple>

Override the selectableItemBackground attribute only in (values/style.xml):

<style name="AppTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- For selection -->
    <item name="selectableItemBackground">@drawable/selector_item_light</item>
    <item name="primaryTextSelector">@drawable/primary_text_selector_light</item>
</style>

Finally use the referenced style in your Item view (layout/recycler_item.xml):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/listPreferredItemHeightLarge"
    ...
    android:background="?attr/selectableItemBackground">
...
</RelativeLayout>

ℹ️ Note: By using the above configuration, don't use ?android:attr prefix or selection will not be visible.

Simple selection (IDLE)

Mode IDLE is the default value at the start up. However to switch from another mode to IDLE, write the following statement:

import eu.davidea.flexibleadapter.SelectableAdapter.Mode;

mAdapter.setMode(Mode.IDLE);

Single selection (SINGLE)

In your Activity/Fragment creation, set the Mode SINGLE.

import eu.davidea.flexibleadapter.SelectableAdapter.Mode;

mAdapter.setMode(Mode.SINGLE);

In onItemClick(), call toggleSelection() to register the selection on that position (it won't trigger notifyItemChanged()) and return true for the itemView activation:

@Override
public boolean onItemClick(int position) {
    if (position != mActivatedPosition) setActivatedPosition(position);
    return true; //Important!
}
// Optional
// To evaluate if setActivatedPosition can be included in the library
private void setActivatedPosition(int position) {
    mActivatedPosition = position;
    mAdapter.toggleSelection(position); //Important!
}

That's it!

ℹ️ Note: With version 5.x the ViewHolder automatically handles the view activation. Check the wiki page ViewHolders for more details.

Optionally, you can now set View Elevation for the activation, API compatibility is maintained.

/**
 * Allows to set elevation while the view is activated.
 * Override to return desired value of elevation on this itemView.
 *
 * @return never elevate, returns 0dp if not overridden
 */
public float getActivationElevation() {
    return 0f;
}

ℹ️ Note: Also the binding of the selection status (when user scrolls) is automatically handled by the Adapter, you don't have to call any view activation. This is an extract of the pre-implemented onBindViewHolder():

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
    // When user scrolls, this line binds the correct selection status
    holder.itemView.setActivated(isSelected(position));
    ...
}

Multi selection (MULTI)

In your Activity/Fragment change the Modes for the ActionMode object:

import eu.davidea.flexibleadapter.SelectableAdapter.Mode;

public class MainActivity extends AppCompatActivity implements
        ActionMode.Callback,
        FlexibleAdapter.OnClickListener, FlexibleAdapter.OnItemLongClickListener {

    ...
    private FlexibleAdapter mAdapter;
    private ActionMode mActionMode;
    ...
	
    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
        mode.getMenuInflater().inflate(R.menu.menu_context, menu);
        mAdapter.setMode(Mode.MULTI);
        return true;
    }
	
    @Override
    public void onDestroyActionMode(ActionMode mode) {
        mAdapter.setMode(Mode.IDLE);
        mActionMode = null;
    }

    // onPrepareActionMode + onActionItemClicked
    ...
}

Handle the click events in your Activity/Fragment:

@Override
public boolean onItemClick(int position) {
    if (mActionMode != null && position != RecyclerView.NO_POSITION) {
        // Mark the position selected
        toggleSelection(position);
        return true;
    } else {
        // Handle the item click listener
        ...
        // We don't need to activate anything
        return false;
    }
}

@Override
public void onItemLongClick(int position) {
    if (mActionMode == null) {
        mActionMode = startSupportActionMode(this);
    }
    toggleSelection(position);
}

Make sure the onItemClick & onItemLongClick are triggered

mAdapter.addListener(this);

Toggle the selection state of an item when ActionMode is active:

/**
 * Toggle the selection state of an item.
 * If the item was the last one in the selection and is unselected, the ActionMode
 * is stopped.
 */
private void toggleSelection(int position) {
    // Mark the position selected
    mAdapter.toggleSelection(position);

    int count = mAdapter.getSelectedItemCount();

    if (count == 0) {
        mActionMode.finish();
    } else {
        setContextTitle(count);
    }
}

private void setContextTitle(int count) {
    mActionMode.setTitle(String.valueOf(count) + " " + (count == 1 ?
            getString(R.string.action_selected_one) :
            getString(R.string.action_selected_many)));
}

Handling rotation

Preserve the current selection state, together with other flags:

@Override
public void onSaveInstanceState(Bundle outState) {
    mAdapter.onSaveInstanceState(outState);
    super.onSaveInstanceState(outState);
}

Restore the previous selection state:

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    // Restore previous state
    if (savedInstanceState != null) {
        // Selection
        mAdapter.onRestoreInstanceState(savedInstanceState);
        if (mAdapter.getSelectedItemCount() > 0) {
            mActionMode = startSupportActionMode(this);
            setContextTitle(mAdapter.getSelectedItemCount());
        }
    }
}

selectAll() and clearSelection()

No more notifyItemRangeChanged() in selectAll() and clearSelection() methods: This means no more item binding!
Now, bound selectable ViewHolders will have the StateListDrawable background switching status (activated/normal) when invoking internally FlexibleViewHolder.toggleSelection(), so that, all inner views can complete animations with no interruptions.

ℹ️ Note:

  • The cached ViewHolders are removed when they are recycled by the RecyclerView.
  • The ViewHolders must extend FlexibleViewHolder, otherwise, item binding still occurs.
Clone this wiki locally