[Java & Kotlin] Create multiple selectable RecyclerView

RecyclerView doesn't have any function

I myself have been a ListView believer for about a year since I started developing Android. I thought, "Because ** RecyclerView has no function **, ** It's a degraded version of ListView? ** It seems difficult and difficult to use, ** Physiologically impossible **". ListView has Divider, FastScroll, and ChoiceMode. And yet, the successor ** RecyclerView doesn't have anything **! I think there was a time when everyone thought so.

However, in reality, RecyclerView is easier to handle. This is why it is flourishing. Even if I can implement ListView more easily, I will implement it with RecyclerView. It may be the result of practicing making ** RecyclerAdapter desperately all night long ago, but considering the extensibility later, it is better to implement it with RecyclerView. He betrayed ListView and is now a RecyclerView believer.

Let's say that the convenience appeal of RecyclerView is around here, and we will get into the main subject. There are many good articles written by RecyclerView followers, so please read that as well (I'm sorry to make you a believer without permission).

Basics of RecyclerViewI've just summarized the basics of RecyclerView

I also want to select in RecyclerView

It means that you want to make selectable items. It's a RecyclerView with no features, but ** let's modify it to a SelectableRecyclerView that I can select directly **. That is the content of this article. This function is almost certainly implemented in recent apps, but surprisingly there are few articles that explain it, so I wrote this article.

Selectable Recycler View features

Since it is implemented in various apps, you probably know what kind of function it is, but I will make a list of functions for the time being.

・ Usually behaves as a normal Recycler View ・ Press and hold an item to enter selection mode ・ In the selection mode, you can also select with NormalClick (you can also select with long press) -Click on a selected item to deselect it -When all items are deselected, the selection mode is automatically turned off. ・ You can get the selected item ・ In some cases, you can always enter the selection mode.

Is it about this? I think that these functions are common even if there are differences in functions depending on the application. If you need something else, implement it yourself. This extensibility is also a good point of RecyclerView.

environment

It's called SelectableRecyclerView, so I thought I'd make it View, but considering extensibility, that's not the case. Different people need different functions. So this time I will implement it with Adapter. Before that, I will write about this environment. I will write based on basic Kotlin, but since there seems to be demand in Java, I will also write the implementation in Java for the Adapter itself. (Because I am a beginner about Java, please tell me if you make a mistake)

· Java 8 ・ Kotlin 1.4 ・ Android Studio 4.0.1 ・ Target SDK 30 ・ Min SDK 24 ・ Build tools 30.0.1

Try to make it selectable

Adapter and Holder

Kotlin version (click to expand)
Since I wrote it in Java at first, Holder uses Java. Kotlin is a god who can use the Java one as it is.

SelectableAdapterWithKotlin.kt


class SelectableAdapterWithKotlin(private val context: Context, private val itemDataList: List<String>, private val isAlwaysSelectable: Boolean): RecyclerView.Adapter<SelectableHolder>(){

    //When isAlwaysSelectable is ON, select mode from the beginning
    private var isSelectableMode = isAlwaysSelectable
    private val selectedItemPositions = mutableSetOf<Int>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectableHolder {
        return SelectableHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_multi_url_card, parent, false))
    }

    @SuppressLint("SetTextI18n")
    override fun onBindViewHolder(holder: SelectableHolder, position: Int) {
        with(holder) {
            mainTextView.text = itemDataList[position]
            subTextView.text = "position $position"

            //If this item is selected, check it (display the image of ✓)
            checkLayout.visibility = if (isSelectedItem(position)) View.VISIBLE else View.GONE

            cardView.setOnClickListener {

                //Treat as a normal click when not in selection mode
                if (!isSelectableMode && !isAlwaysSelectable) Toast.makeText(context, "Normal click", Toast.LENGTH_SHORT).show()
                else {
                    if (isSelectedItem(position)) removeSelectedItem(position)
                    else addSelectedItem(position)

                    onBindViewHolder(holder, position)
                }
            }
            cardView.setOnLongClickListener {

                //Long click to enter selection mode
                if (isSelectedItem(position)) removeSelectedItem(position)
                else addSelectedItem(position)

                onBindViewHolder(holder, position)

                return@setOnLongClickListener true
            }
        }
    }

    override fun getItemCount(): Int = itemDataList.size

    //Pass the Set in which the Position of the selected item is recorded to the outside
    fun getSelectedItemPositions() = selectedItemPositions.toSet()
    
    //Check if the item of the specified Position is selected
    private fun isSelectedItem(position: Int): Boolean = (selectedItemPositions.contains(position))

    //Enter selection mode when not in selection mode
    private fun addSelectedItem(position: Int){
        if(selectedItemPositions.isEmpty() && !isAlwaysSelectable){
            isSelectableMode = true
            Toast.makeText(context, "Selectable Mode ON", Toast.LENGTH_SHORT).show()
        }
        selectedItemPositions.add(position)
    }

    //If the last one is deselected in selection mode, turn off selection mode
    private fun removeSelectedItem(position: Int){
        selectedItemPositions.remove(position)
        if(selectedItemPositions.isEmpty() && !isAlwaysSelectable){
            isSelectableMode = false
            Toast.makeText(context, "Selectable Mode OFF", Toast.LENGTH_SHORT).show()
        }
    }
}
Java version (click to expand)

SelectableHolder.java


public class SelectableHolder extends RecyclerView.ViewHolder {

    public TextView mainTextView;
    public TextView subTextView;
    public CardView cardView;
    public ConstraintLayout checkLayout;

    public SelectableHolder(View itemView) {
        super(itemView);

        mainTextView = itemView.findViewById(R.id.VMU_MainText);
        subTextView = itemView.findViewById(R.id.VMU_SubText);
        cardView = itemView.findViewById(R.id.VMU_CardView);
        checkLayout = itemView.findViewById(R.id.VMU_CheckLayout);
    }
}

SelectableAdapter.java


public class SelectableAdapter extends RecyclerView.Adapter<SelectableHolder> {

    private Context context;
    private List<String> itemDataList;

    private Boolean isSelectableMode;
    private Boolean isAlwaysSelectable;
    private Set<Integer> selectedItemPositionsSet = new ArraySet<>();

    public SelectableAdapter(Context context, List<String> itemDataList, Boolean isAlwaysSelectable){
        this.context = context;
        this.itemDataList = itemDataList;
        this.isAlwaysSelectable = isAlwaysSelectable;

        //When isAlwaysSelectable is ON, select mode from the beginning
        isSelectableMode = isAlwaysSelectable;
    }

    @NonNull
    @Override
    public SelectableHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new SelectableHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.view_multi_url_card, parent, false));
    }

    @SuppressLint("SetTextI18n")
    @Override
    public void onBindViewHolder(@NonNull final SelectableHolder holder, final int position) {
        holder.mainTextView.setText(itemDataList.get(position));
        holder.subTextView.setText("position " + position);

        //If this item is selected, check it (display the image of ✓)
        if(isSelectedItem(position)){
            holder.checkLayout.setVisibility(View.VISIBLE);
        }
        else {
            holder.checkLayout.setVisibility(View.GONE);
        }

        holder.cardView.setOnClickListener(view -> {

            //Treat as a normal click when not in selection mode
            if(!isSelectableMode && !isAlwaysSelectable) Toast.makeText(context, "Normal click", Toast.LENGTH_SHORT).show();
            else {
                if(isSelectedItem(position)) removeSelectedItem(position);
                else addSelectedItem(position);

                onBindViewHolder(holder, position);
            }
        });

        holder.cardView.setOnLongClickListener(view -> {

            //Long click to enter selection mode
            if (isSelectedItem(position)) removeSelectedItem(position);
            else addSelectedItem(position);

            onBindViewHolder(holder, position);

            return true;
        });
    }

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

    //Pass the Set in which the Position of the selected item is recorded to the outside
    Set<Integer> getSelectedItemPositions(){
        return selectedItemPositionsSet;
    }

    //Check if the item of the specified Position is selected
    private Boolean isSelectedItem(int position){
        return selectedItemPositionsSet.contains(position);
    }

    //Enter selection mode when not in selection mode
    private void addSelectedItem(int position){
        if(selectedItemPositionsSet.isEmpty() && !isAlwaysSelectable) {
            isSelectableMode = true;
            Toast.makeText(context, "Selectable Mode ON", Toast.LENGTH_SHORT).show();
        }
        selectedItemPositionsSet.add(position);
    }

    //If the last one is deselected in selection mode, turn off selection mode
    private void removeSelectedItem(int position){
        selectedItemPositionsSet.remove(position);
        if(selectedItemPositionsSet.isEmpty() && !isAlwaysSelectable){
            isSelectableMode = false;
            Toast.makeText(context, "Selectable Mode OFF", Toast.LENGTH_SHORT).show();
        }
    }
}

Activity Activity is written in Kotlin. I don't write a big deal in the first place, so I think it can be easily ported. When you press the Button, the Position of the selected item is received from the Adapter (getSelectedItemPositions), and the value is obtained and displayed by comparing it with the List held by the Activity.

Code below (click to expand)

MainActivity.kt


class MainActivity : AppCompatActivity(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val itemDataList = listOf("Pomeranian", "toy poodle", "Shiba inu", "Bulldog", "Dachshund", "Doberman", "beagle", "Labrador retriever", "Golden retriever", "Siberian husky")
        val selectableAdapter = SelectableAdapterWithKotlin(this, itemDataList, false)

        AM_RecyclerView.apply {
            setHasFixedSize(false)
            adapter = selectableAdapter
            layoutManager = LinearLayoutManager(this@MainActivity)
        }

        AM_Button.setOnClickListener {
            MaterialAlertDialogBuilder(this)
                .setTitle("Selected item")
                .setMessage(selectableAdapter.getSelectedItemPositions().joinToString(separator = "\n") { itemDataList[it] })
                .setPositiveButton("OK", null)
                .show()
        }
    }
}

Layout

It's a bit complicated to indicate that it's selected. This time, the selected state is shown by showing / hiding the View, but I think that you can put Check in the Checkbox.

Code below (click to expand)

view_multi_url_card.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.cardview.widget.CardView
        android:id="@+id/VMU_CardView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        android:foreground="?android:attr/selectableItemBackground"
        app:cardCornerRadius="8dp"
        app:cardBackgroundColor="@color/colorBackgroundZ4"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <androidx.cardview.widget.CardView
                android:id="@+id/cardView"
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:cardBackgroundColor="@android:color/transparent"
                app:cardElevation="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintDimensionRatio="1:1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent">

                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_gravity="center">

                    <ImageView
                        android:id="@+id/VMU_Image"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:layout_gravity="center"
                        android:scaleType="centerCrop"
                        android:src="@drawable/im_dog"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintEnd_toEndOf="parent"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent" />

                    <androidx.constraintlayout.widget.ConstraintLayout
                        android:id="@+id/VMU_CheckLayout"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:alpha="0.9"
                        android:background="@color/colorAccent"
                        android:visibility="gone">

                        <ImageView
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:layout_gravity="center"
                            android:layout_marginStart="16dp"
                            android:layout_marginTop="16dp"
                            android:layout_marginEnd="16dp"
                            android:layout_marginBottom="16dp"
                            android:scaleType="fitCenter"
                            android:src="@drawable/ic_check"
                            app:layout_constraintBottom_toBottomOf="parent"
                            app:layout_constraintEnd_toEndOf="parent"
                            app:layout_constraintStart_toStartOf="parent"
                            app:layout_constraintTop_toTopOf="parent" />

                    </androidx.constraintlayout.widget.ConstraintLayout>

                </androidx.constraintlayout.widget.ConstraintLayout>
            </androidx.cardview.widget.CardView>

            <LinearLayout
                android:id="@+id/linearLayout"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="16dp"
                android:layout_marginTop="16dp"
                android:layout_marginEnd="16dp"
                android:layout_marginBottom="16dp"
                android:orientation="vertical"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@+id/cardView"
                app:layout_constraintTop_toTopOf="parent">

                <TextView
                    android:id="@+id/VMU_MainText"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:ellipsize="end"
                    android:singleLine="true"
                    android:text="Main text"
                    android:textColor="@color/colorChar"
                    android:textSize="14sp" />

                <TextView
                    android:id="@+id/VMU_SubText"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:ellipsize="end"
                    android:gravity="end"
                    android:singleLine="true"
                    android:text="Subtext"
                    android:textColor="@color/colorCharSec"
                    android:textSize="12sp" />

            </LinearLayout>
        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorBackgroundZ3"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/AM_RecyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/AM_Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_margin="8dp"
        android:text="View selected items" />

</LinearLayout>

Gradle To use RecyclerView, you need to add to Gradle. This time I also use CardView etc. so I need to write them as well.

Code below (click to expand)

build.gradle(app)


dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'com.google.android.material:material:1.2.0'
}

I tried to move

I tried it on Pixel 3 (Android 11). Both Kotlin version and Java version work the same. 実機で動いているZIF

Finally

This time I made a selectable RecyclerView. In the above GIF, isAlwaysSelectable is false, but if you set it to true, it will always be selectable. There may be mistakes in Java, so if you find one, please comment. Please LGTM whether it is helpful or not (it's a lie. If it's not helpful, please comment. I'll help you as much as possible)

Recommended Posts

[Java & Kotlin] Create multiple selectable RecyclerView
Java string multiple replacement
[Java, Kotlin] Type Variance
Create JSON in Java
Create hyperlinks in Java PowerPoint
[Java] Create a temporary file
Create Azure Functions in Java
Create your own Java annotations
[Java] Multiple OR condition judgment
[Java] Combine multiple Lists (Collections)