Introduction

Android Custom Spinner is also like a drop-down list that can be used to display the multiple options to the user in which the user can select only one option. Although we have discussed, its implementation and uses. Check out this article Android Spinner Container.

However, in this article, we are going to learn how to customize a Spinner. As shown in the below demonstration.

Example of a Custom Spinner Widget

Step by step Android Spinner Customization

Let’s begin customization by paying a little attention to this below code snippet, we are actually using a custom spinner. I mean this com.example.customspinners.CustomSpinner is the Java class extended with the Spinner class. And as you go through this article, I promise you will understand why am I using this approach. In short, I’m going to track the Spinner’s state whether the popup window appears or not! For that, I will be using an interface to do that.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
    tools:context=".MainActivity">

    <com.example.customspinners.CustomSpinner
        android:id="@+id/spinner_fruits"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@drawable/bg_spinner"
        android:popupBackground="@drawable/bg_spinner_popup_window"
        android:popupElevation="8dp"
        android:dropDownVerticalOffset="44dp"
        android:layout_margin="16dp"/>
    
</RelativeLayout>

com.example.customspinners.CustomSpinner.java

package com.example.customspinners;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class CustomSpinner extends androidx.appcompat.widget.AppCompatSpinner {

    public interface OnSpinnerEventsListener {
        void onPopupWindowOpened(Spinner spinner);
        void onPopupWindowClosed(Spinner spinner);
    }

    private OnSpinnerEventsListener mListener;
    private boolean mOpenInitiated = false;

    public CustomSpinner(Context context) {
        super(context);
    }

    public CustomSpinner(@NonNull Context context, int mode) {
        super(context, mode);
    }

    public CustomSpinner(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomSpinner(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public CustomSpinner(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int mode) {
        super(context, attrs, defStyleAttr, mode);
    }

    public CustomSpinner(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme) {
        super(context, attrs, defStyleAttr, mode, popupTheme);
    }

    @Override
    public boolean performClick() {
        mOpenInitiated = true;
        if (mListener != null) {
            mListener.onPopupWindowOpened(this);
        }
        return super.performClick();
    }

    @Override
    public void onWindowFocusChanged (boolean hasFocus) {
        if (hasBeenOpened() &amp;&amp; hasFocus) {
            performClosedEvent();
        }
    }

    /**
     * Register the listener which will listen for events.
     */
    public void setSpinnerEventsListener(
            OnSpinnerEventsListener onSpinnerEventsListener) {
        mListener = onSpinnerEventsListener;
    }

    /**
     * Propagate the closed Spinner event to the listener from outside if needed.
     */
    public void performClosedEvent() {
        mOpenInitiated = false;
        if (mListener != null) {
            mListener.onPopupWindowClosed(this);
        }
    }

    /**
     * A boolean flag indicating that the Spinner triggered an open event.
     *
     * @return true for opened Spinner
     */
    public boolean hasBeenOpened() {
        return mOpenInitiated;
    }

}

We are not yet started working on the MainActivity.java class. So OnSpinnerEventsListener‘s implementation will be discussed later in this article.


Creating Drawable For Background and Popup Background

Now create two backgrounds for android:background and android:popupBackground and set them in the required attributes. As shown below.

android:background="@drawable/bg_spinner"
android:popupBackground="@drawable/bg_spinner_popup_window"

bg_spinner.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <layer-list>
            <item >
                <shape android:shape="rectangle">
                    <solid android:color="@color/colorAccent"/>
                    <padding android:right="12dp"
                        android:left="12dp"/>
                    <corners android:radius="4dp"/>
                </shape>
            </item>

            <item
                android:drawable="@drawable/ic_down_arrow"
                android:gravity="end|center_vertical"/>

        </layer-list>
    </item>
</selector>

bg_spinner_popup_window.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <stroke android:color="@color/colorAccent"
        android:width="2dp"/>
    <corners android:radius="4dp"/>
    <solid android:color="@android:color/white"/>
</shape>

Spinner Drop Down Vertical Offset

Spinner Popup Window Overlapped By Default

Now let’s talk about android:dropDownVerticalOffset attribute. So if you take a look at our activity_main.xml again, you can see layout_height is 40dp and dropDownVerticalOffset is 44dp. Otherwise, every time you run the code, Spinner’s Popup window will overlap on top of the spinner itself. So it is better to specify the layout_height and dropDownVerticalOffset just to avoid popup window overlapping issues.

android:layout_height="40dp"
android:dropDownVerticalOffset="44dp"

Preparing Spinner Drop Down List

So basically I have created two classes. One is the model class and the other one is used to set some default values and get the list. As shown below spinnets.

Fruit.java model class

package com.example.customspinners.inventory;
import java.io.Serializable;

public class Fruit implements Serializable {

    private String name;
    private int image;

    public Fruit() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getImage() {
        return image;
    }

    public void setImage(int image) {
        this.image = image;
    }
}

Data.java for setting some data and getting a list.

package com.example.customspinners.inventory;
import com.example.customspinners.R;
import java.util.ArrayList;
import java.util.List;

public class Data {

    public static List<Fruit> getFruitList() {
        List<Fruit> fruitList = new ArrayList<>();

        Fruit Avocado = new Fruit();
        Avocado.setName("Avocado");
        Avocado.setImage(R.drawable.avocado);
        fruitList.add(Avocado);

        Fruit Banana = new Fruit();
        Banana.setName("Banana");
        Banana.setImage(R.drawable.banana);
        fruitList.add(Banana);

        Fruit Coconut = new Fruit();
        Coconut.setName("Coconut");
        Coconut.setImage(R.drawable.coconut);
        fruitList.add(Coconut);

        Fruit Guava = new Fruit();
        Guava.setName("Guava");
        Guava.setImage(R.drawable.guava);
        fruitList.add(Guava);

        Fruit Lemon = new Fruit();
        Lemon.setName("Lemon");
        Lemon.setImage(R.drawable.lemon);
        fruitList.add(Lemon);

        Fruit Mango = new Fruit();
        Mango.setName("Mango");
        Mango.setImage(R.drawable.mango);
        fruitList.add(Banana);

        Fruit Orange = new Fruit();
        Orange.setName("Orange");
        Orange.setImage(R.drawable.orange);
        fruitList.add(Orange);

        return fruitList;
    }

}

Creating an Adapter Class

Now we are going to create an adapter and set in our Spinner. So create an Java class extend whatever adapter you like. I mean BaseAdapter or RecyclerView Adapter class. As shown in the below code snippet.

FruitAdapter.java is the adapter class which is extending with the BaseAdapter.

package com.example.customspinners;
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.customspinners.inventory.Fruit;
import java.util.List;

public class FruitAdapter extends BaseAdapter {
    private Context context;
    private List<Fruit> fruitList;

    private int selectedItemPosition = -1;

    public FruitAdapter(Context context, List<Fruit> fruitList) {
        this.context = context;
        this.fruitList = fruitList;
    }

    public void setSelectedItemPosition(int selectedItemPosition) {
        this.selectedItemPosition = selectedItemPosition;
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return fruitList != null ? fruitList.size() : 0;
    }

    @Override
    public Object getItem(int i) {
        return i;
    }

    @Override
    public long getItemId(int i) {
        return fruitList != null ? fruitList.size() : 0;
    }

    @Override
    public View getView(int pos, View view, ViewGroup viewGroup) {

        View rootView = LayoutInflater.from(context)
                .inflate(R.layout.item_fruit, viewGroup, false);
        TextView txtName = rootView.findViewById(R.id.name);
        ImageView image = rootView.findViewById(R.id.image);

        txtName.setText(fruitList.get(pos).getName());
        image.setImageResource(fruitList.get(pos).getImage());

        if (selectedItemPosition == pos) {
            txtName.setTextSize(18f);
            txtName.setTypeface(null, Typeface.BOLD);
        } else {
            txtName.setTextSize(14f);
            txtName.setTypeface(null, Typeface.NORMAL);
        }

        return rootView;
    }
}

item_fruit.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginEnd="60dp">
    <ImageView
        android:id="@+id/image"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_marginStart="8dp"
        android:src="@drawable/orange"/>

    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Name"
        android:layout_toRightOf="@+id/image"
        android:layout_centerVertical="true"
        android:layout_marginStart="8dp"/>
</RelativeLayout>

Setup BaseAdapter with a Spinner

In order to setup BaseAdapter with a Spinner check out this below code snippet.

MainActivity.java with BaseAdapter

public class MainActivity extends AppCompatActivity {

    private CustomSpinner spinner_fruits;
    private FruitAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        spinner_fruits = findViewById(R.id.spinner_fruits);

        adapter = new FruitAdapter(this, Data.getFruitList());
        spinner_fruits.setAdapter(adapter);
    }
}

Adding interface OnItemSelectedListener for Customization

Using this OnItemSelectedListener we can get the selected item and its position. So every time the user taps on the Spinner popup window will appear with a list of items. And if the user clicks on an item, onItemSelected function will get called. Inside which I’m just updating the current selected item’s position in the Adapter. In order to highlight the item

MainActivity.java with OnItemSelectedListener

public class MainActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener{

    private CustomSpinner spinner_fruits;
    private FruitAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        spinner_fruits = findViewById(R.id.spinner_fruits);

        spinner_fruits.setOnItemSelectedListener(this);
        adapter = new FruitAdapter(this, Data.getFruitList());
        spinner_fruits.setAdapter(adapter);

    }

    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
        adapter.setSelectedItemPosition(i);
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {
        Toast.makeText(this, "onNothingSelected", Toast.LENGTH_SHORT).show();
    }
}

Adding interface OnSpinnerEventListener for Android Custom Spinner

Now, add the interface OnSpinnerEventsListener. We need to override two methods onSpinnerOpened and onSpinnerClosed. Basically, I’m changing the background drawable in those two methods.

MainActivity.java with OnSpinnerEventListener

public class MainActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener,
        CustomSpinner.OnSpinnerEventsListener {

    private CustomSpinner spinner_fruits;
    private FruitAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        spinner_fruits = findViewById(R.id.spinner_fruits);

        spinner_fruits.setOnItemSelectedListener(this);
        spinner_fruits.setSpinnerEventsListener(this);

        adapter = new FruitAdapter(this, Data.getFruitList());
        spinner_fruits.setAdapter(adapter);
    }

    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
        adapter.setSelectedItemPosition(i);
        Toast.makeText(this, "onItemSelected" + i, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {
        Toast.makeText(this, "onNothingSelected", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onPopuoWindowOpened(Spinner spinner) {
        spinner_fruits.setBackground(getResources().getDrawable(R.drawable.bg_spinner_fruits_up));
    }

    @Override
    public void onPopuoWindowClosed(Spinner spinner) {
        spinner_fruits.setBackground(getResources().getDrawable(R.drawable.bg_spinner_fruits));
    }
}

Wallaahhh!!!! Now just run your code. So far we have talked about all the areas of a Spinner customization.


Also watch this below video. If you still have any questions.

Conclusion

Alright! Now we know how to set background and popup background both. We also know that how should we use CustomSpinner.java class and its interface called OnSpinnerEventListener.

I hope you enjoyed this article. If you have any questions let me know. Thanks for reading.


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *