domenica 20 settembre 2015

Customizing Android Spinner with custom layout and Enum values

In this article we will show how to customize the Android Spinner view using a custom layout and an Enum to bind the values. Suppose we want to represent a localized drop down list to let the user choose a color. Let's start by defining an interface to manage the color and the value of the Spinner:

package com.devsourcenter.customspinner.model;

import android.content.Context;

/**
 * Interface to manage the color and the value of the Spinner
 */
public interface IEnumSpinner {
    /**
     * Get the localized value of the Spinner
     *
     * @param context Context
     * @return The value of the Spinner
     */
    public String getLabel(Context context);

    /**
     * Get the color of the selected value
     *
     * @return The integer representation of the color
     */
    public int getColor();
}

Now we have to implement the interface in our Enum:

package com.devsourcenter.customspinner.model.enums;

import android.content.Context;
import android.graphics.Color;

import com.devsourcenter.customspinner.AppResources;
import com.devsourcenter.customspinner.R;
import com.devsourcenter.customspinner.model.IEnumSpinner;

/**
 * This Enum is used to represent a color
 */
public enum ColorEnum implements IEnumSpinner {
    RED(R.string.label_red),
    BLUE(R.string.label_blue),
    GREEN(R.string.label_green);

    /**
     * localized label key
     */
    private int key;

    private ColorEnum (int key) {
        this.key = key;
    }

    // we override toString() in order to translate the labels in the drop-down
    // list
    @Override
    public String toString() {
        return AppResources.getContext().getString(key);
    }

    public String getLabel(Context context) {
        return context.getResources().getString(key);
    }

    public int getColor() {
        switch (this) {
            case RED:
                return Color.rgb(255, 0, 0);
            case BLUE:
                return Color.rgb(0, 0, 255);
            case GREEN:
                return Color.rgb(0, 255, 0);
        }
        
        return -1;
    }
}

We need to implement an Application class to get the context in the toString() method of ColorEnum. In addition we also add the attribute android:name="AppResources" in the AndroidManifest.xml under the tag Application, in order to instantiate the class when the process for the application is created.

package com.devsourcenter.customspinner;

import android.app.Application;
import android.content.Context;

/**
 * Class used to get a context in any part of the app
 */
public class AppResources extends Application {

    private static Context mContext;

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
    }

    public static Context getContext() {
        return mContext;
    }
}


At this point we can implement the Adapter associated to the the Spinner. The holder pattern is used to increase performance:

package com.devsourcenter.customspinner.model.adapter;

import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import com.devsourcenter.customspinner.R;
import com.devsourcenter.customspinner.model.IEnumSpinner;

/**
 * Adapter to fill Spinner with items
 */
public class EnumAdapter extends ArrayAdapter<IEnumSpinner> {
    Context mContext;
    int mLayoutResourceId;
    IEnumSpinner[] mItems;

    public EnumAdapter(Context context, int layoutResourceId,
            IEnumSpinner[] data) {
        super(context, layoutResourceId, data);
        this.mLayoutResourceId = layoutResourceId;
        this.mContext = context;
        this.mItems = data;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View row = convertView;
        Holder holder;

        if (row == null) {
            // at this point we inflate the view with our custom layout
            LayoutInflater inflater = ((Activity) mContext).getLayoutInflater();
            row = inflater.inflate(mLayoutResourceId, parent, false);

            holder = new Holder();
            holder.txtTitle = (TextView) row
                    .findViewById(R.id.textView_spinner);

            row.setTag(holder);
        } else {
            holder = (Holder) row.getTag();
        }

        IEnumSpinner item = mItems[position];
        holder.txtTitle.setText(item.getLabel(mContext));
        holder.txtTitle.setTextColor(item.getColor());
        return row;
    }

    static class Holder {
        TextView txtTitle;
    }
}


Finally we can create the Activity to include the Spinner:

package com.devsourcenter.customspinner;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
import android.widget.Toast;

import com.devsourcenter.customspinner.model.adapter.EnumAdapter;
import com.devsourcenter.customspinner.model.enums.ColorEnum;

public class MainActivity extends Activity {

    private EnumAdapter mColorAdapter;
    private Spinner mSpinnerColor;

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

        mColorAdapter = new EnumAdapter(this, R.layout.spinner_layout,
                ColorEnum.values());
        mSpinnerColor = (Spinner) findViewById(R.id.spinner_color);
        mSpinnerColor.setAdapter(mColorAdapter);
        mSpinnerColor
                .setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(AdapterView<?> parent,
                            View view, int position, long id) {
                        // get the enum selected
                        ColorEnum color = (ColorEnum) parent
                                .getItemAtPosition(position);

                        Toast.makeText(MainActivity.this,
                                color.getLabel(MainActivity.this),
                                Toast.LENGTH_LONG).show();

                        // do whatever you want
                    }

                    @Override
                    public void onNothingSelected(AdapterView<?> parent) {

                    }
                });
    }

}


We used this simple layout for the Activity (activity_main.xml):

<LinearLayout 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"
    android:orientation="horizontal"
    tools:context="com.devsourcenter.customspinner.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="@string/label_choose"
        android:textAppearance="?android:attr/textAppearanceLarge" />

    <Spinner
        android:id="@+id/spinner_color"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>


And this is the layout for the Spinner (spinner_layout.xml):

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/textView_spinner"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:padding="7dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >
    

</TextView>


We also provided translated enum labels for English:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">CustomSpinner</string>
    <string name="label_choose">Choose a color:</string>
    
    <string name="label_red">Red</string>
    <string name="label_blue">Blue</string>
    <string name="label_green">Green</string>
</resources>


Italian:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="label_red">Rosso</string>
    <string name="label_blue">Blu</string>
    <string name="label_green">Verde</string>
</resources>


and French:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="label_red">Rouge</string>
    <string name="label_blue">Bleu</string>
    <string name="label_green">Vert</string>
</resources>


Here is the result:




So you can simply define your Enum and your spinner_layout.xml to customize a Spinner.