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 RecyclerView ・ I've just summarized the basics of 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.
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.
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
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()
}
}
}
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.
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()
}
}
}
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.
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.
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 it on Pixel 3 (Android 11). Both Kotlin version and Java version work the same.
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