4

On Android, I want to users to be able to select multiple rows from a list. I read that I can use SelectionTracker with a RecyclerView to enable list-item selection.

But all the code examples are in Kotlin. Are there any examples of SelectionTracker in Java?

Michael Osofsky
  • 11,429
  • 16
  • 68
  • 113
  • You should try this : [Reference](https://stackoverflow.com/questions/36369913/how-to-implement-multi-select-in-recyclerview) – Arjun Vyas Apr 25 '19 at 05:06
  • Thank you @ArjunVyas in this case I was mainly asking about how to implement SelectionTracker specifically, not alternative ways of enabling list-item selection. – Michael Osofsky Apr 25 '19 at 05:12

1 Answers1

7

Here is a settings menu that allows the user to choose multiple settings. To begin the selection, the user has to long press any setting. Then they can tap any setting to choose more.

Activity

package com.locuslabs.android.sdk;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.selection.ItemDetailsLookup;
import androidx.recyclerview.selection.Selection;
import androidx.recyclerview.selection.SelectionPredicates;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.selection.StableIdKeyProvider;
import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.gson.Gson;
import com.locuslabs.android.sdk.api.ConfigurationExperiments;
import com.locuslabs.android.sdk.api.MapExperiments;
import com.locuslabs.android.sdk.api.MapViewExperiments;
import com.locuslabs.android.sdk.api.PositionExperiments;
import com.locuslabs.android.sdk.api.VenueExperiments;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

public class SettingsActivity extends Activity {
    private static final String TAG = "SettingsActivity";
    SelectionTracker<Long> selectedSettingTracker;
    private RecyclerView settingsRecyclerView;
    private List<String> listOfUsableApis;
    private ApiSettings mApiSettings;

    private void setApiSettings(List<String> settingNamesSelected) {
        for (String settingName : settingNamesSelected) {
            if (settingName.equals(getResources().getString(R.string.api_setting_draw_line)))
                mApiSettings.mDrawLine = true;
            if (settingName.equals(getResources().getString(R.string.api_setting_search)))
                mApiSettings.mLogSearch = true;

            /* omitted rest of options for brevity */
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mApiSettings = new ApiSettings();

        setContentView(R.layout.activity_settings);

        settingsRecyclerView = findViewById(R.id.settingsRecyclerView);
        settingsRecyclerView.setLayoutManager(new LinearLayoutManager(this));

        Button backButton = findViewById(R.id.settings_back_button);
        Button saveButton = findViewById(R.id.settings_apply_button);

        backButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onBackPressed();
            }
        });

        saveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                setApiSettings(getSettingNamesSelected());
                Intent intent = new Intent();
                intent.putExtra("apiSettings", new Gson().toJson(mApiSettings));
                setResult(RESULT_OK, intent);
                finish();
            }
        });

        listOfUsableApis = /* omitted for brevity */

        final SettingsAdapter settingsAdapter = new SettingsAdapter();
        settingsRecyclerView.setAdapter(settingsAdapter);

        // Handle selection of settings
        selectedSettingTracker = new SelectionTracker.Builder<Long>(
                "selectedSettingTrackerId",
                settingsRecyclerView,
                new StableIdKeyProvider(settingsRecyclerView),
                new SettingsDetailsLookup(),
                StorageStrategy.createLongStorage()
        ).
                withSelectionPredicate(SelectionPredicates.<Long>createSelectAnything()).
                build();
    }

    private List<String> getSettingNamesSelected() {
        Selection<Long> settingsSelection = selectedSettingTracker.getSelection();
        Iterator<Long> settingSelectionIterator = settingsSelection.iterator();
        List<String> settingNamesSelected = new ArrayList<>();
        while (settingSelectionIterator.hasNext()) {
            Long settingSelectionId = settingSelectionIterator.next();
            String settingNameSelected = listOfUsableApis.get(settingSelectionId.intValue());
            settingNamesSelected.add(settingNameSelected);
        }
        return settingNamesSelected;
    }

    public static class ApiSettings {
        public boolean mDrawLine = false;
        public boolean mWalkSimulator = false;

        /* omitted most options for brevity */            

        public ApiSettings() {

        }
    }

    private class SettingsAdapter extends RecyclerView.Adapter<SettingsAdapter.SettingViewHolder> {
        public SettingsAdapter() {
            setHasStableIds(true);
        }

        @NonNull
        @Override
        public SettingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            TextView textView = (TextView) LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.setting_list_item, parent, false);
            SettingViewHolder settingViewHolder = new SettingViewHolder(textView);
            return settingViewHolder;
        }

        @Override
        public void onBindViewHolder(@NonNull SettingViewHolder holder, final int position) {
            holder.textView.setText(listOfUsableApis.get(position));
            holder.textView.setActivated(selectedSettingTracker.isSelected((long) position));
            holder.position = position;
        }

        @Override
        public int getItemCount() {
            return listOfUsableApis.size();
        }

        @Override
        public long getItemId(int position) {
            return Long.valueOf(position);
        }

        public class SettingViewHolder extends RecyclerView.ViewHolder {
            public int position;

            public TextView textView;

            public SettingViewHolder(TextView v) {
                super(v);
                textView = v;
            }
        }
    }

    private class SettingsDetailsLookup extends ItemDetailsLookup<Long> {

        @Nullable
        @Override
        public ItemDetails<Long> getItemDetails(@NonNull MotionEvent event) {
            View view = settingsRecyclerView.findChildViewUnder(event.getX(), event.getY());
            if (view != null) {
                final RecyclerView.ViewHolder viewHolder = settingsRecyclerView.getChildViewHolder(view);
                if (viewHolder instanceof SettingsAdapter.SettingViewHolder) {
                    final SettingsAdapter.SettingViewHolder settingViewHolder = (SettingsAdapter.SettingViewHolder) viewHolder;
                    return new ItemDetailsLookup.ItemDetails<Long>() {
                        @Override
                        public int getPosition() {
                            return viewHolder.getAdapterPosition();
                        }

                        @Nullable
                        @Override
                        public Long getSelectionKey() {
                            return Long.valueOf(settingViewHolder.position);
                        }
                    };
                }
            }
            return null;
        }
    }
}

Layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:baselineAligned="false"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#454545"
        android:weightSum="100">

        <Button
            android:id="@+id/settings_back_button"
            android:background="#454545"
            android:drawableStart="@drawable/arrow_white"
            android:drawableLeft="@drawable/arrow_white"
            android:layout_gravity="start"
            android:layout_width="@dimen/ll_mdu_10"
            android:layout_height="@dimen/ll_mdu_10"
            android:layout_weight="5"/>

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="90"
            />

        <Button
            android:id="@+id/settings_apply_button"
            android:background="#454545"
            android:drawableStart="@android:drawable/ic_menu_save"
            android:drawableLeft="@android:drawable/ic_menu_save"
            android:layout_gravity="end"
            android:layout_width="@dimen/ll_mdu_10"
            android:layout_height="@dimen/ll_mdu_10"
            android:layout_weight="5"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">

        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Long-press for first setting, then tap other settings for multiple selection"
            app:layout_constraintBottom_toTopOf="@+id/settingsRecyclerView"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/settingsRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />

    </LinearLayout>

</LinearLayout>

Layout setting_list_item.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/setting_list_item_text_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/setting_background"
    android:gravity="center_vertical"
    android:minHeight="?android:attr/listPreferredItemHeightSmall"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:textAppearance="?android:attr/textAppearanceListItemSmall" />

Background drawable setting_background.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/holo_green_dark" android:state_activated="true" />
    <item android:drawable="@android:color/white" />
</selector>

References:

Michael Osofsky
  • 11,429
  • 16
  • 68
  • 113