Problems with Android Spinners in a RecyclerView

Viewed 8649

I have a RecyclerView which contains complex items, represented by a class containing a TextView widget, a Spinner widget & an EditText widget. (Think of a list of tasks, with task name, a spinner to select which stage of processing the task has reached, and a completion date for that stage of processing.)

The data for this list comes from a file, and changes to items in the list will be written back to the file. When the user comes back to the app later, the list should reflect the data as he saw it previously.

  1. Currently, my app displays the list of items, except that I don't know how to set the initial value of the Spinner to the value read from the file. How can I set a different initial selection for each spinner in the list?

  2. Currently, when the user makes a selection from the Spinner, the list disappears and the selection does not appear as the user's choice. I saw from another post - Android: Spinner not showing the selected value - that I should call setSelection() (inherited by Spinner class from AbsSpinner). But that did nothing. In the XML layout, my Spinner has: android:textColor="@color/colorPrimaryDark" so I don't think it's a problem of the text being there in a color that matches the background. See this List of Tasks. How can I get the user's selection to be retained?

  3. In OnItemSelected(), how do I tell the activity to note the selected value so it can be saved back to file? The example code I've found online usually just uses a Toast to show that the selection was registered. I think I need to know which item in the RecyclerView contains this Spinner...

Here are the details...

activity_task.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="match_parent"
    android:background="@color/colorBG"
    tools:context="org.myorg.myapp.DetailActivity">

    ...

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvChapList"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@+id/txtTaskLabel"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/imgBorder"
        app:layout_constraintRight_toRightOf="parent"
        />

</android.support.constraint.ConstraintLayout>

subtask_detail.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="24dp"
    android:background="@color/colorBG"
    android:id="@+id/subtask_detail"
    >

    <android.support.constraint.Guideline
        android:id="@+id/LGuideLine2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="40dp"
        tools:layout_editor_absoluteY="0dp"
        tools:layout_editor_absoluteX="40dp" />

    <android.support.constraint.Guideline
        android:id="@+id/RGuideLine2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="280dp"
        tools:layout_editor_absoluteY="0dp"
        tools:layout_editor_absoluteX="280dp" />

    <TextView
        android:id="@+id/txtChapNum"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="6dp"
        tools:text="150"
        android:textAppearance="@style/TextAppearance.AppCompat"
        android:textColor="@color/colorPrimaryDark"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/LGuideLine2"
        />

    <Spinner
        android:id="@+id/spnSteps"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:spinnerMode="dialog"
        tools:text="Quality Check"
        android:prompt="@string/step_prompt"
        style="@android:style/Widget.Holo.Light.Spinner"
        android:entries="@array/step_array"
        android:textColor="@color/colorPrimaryDark"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/LGuideLine2"
        app:layout_constraintRight_toLeftOf="@+id/RGuideLine2"
        />

    <EditText
        android:id="@+id/edtDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="date"
        android:paddingTop="0dp"
        android:paddingBottom="0dp"
        android:paddingStart="0dp"
        android:paddingEnd="0dp"
        tools:text="12-30-2020"
        android:textSize="14sp"
        android:textColor="@color/colorPrimaryDark"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/RGuideLine2"
        app:layout_constraintRight_toRightOf="parent"
        />

</android.support.constraint.ConstraintLayout>

DetailActivity.java:

package org.myorg.myapp;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;

import java.util.ArrayList;

public class DetailActivity extends AppCompatActivity {
    private RecyclerView _rv;
    private LayoutInflater _li;
    private SubtaskDetailAdapter _adapter;
    private ArrayList _alEntries;
    private ArrayList _alTaskEntries;
    private String _sTaskName = null;
    private static final String EXTRA_TASK = "EXTRA_TASK";

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

        Intent i = getIntent();
        _sTaskName = i.getStringExtra(EXTRA_TASK);
        TextView tvTaskName = (TextView) findViewById(R.id.txtTaskName2);
        tvTaskName.setText(_sTaskName);

        // create item detail array & populate it based on task item array
        _alEntries = Globals.getArrayList();
        _alTaskEntries = new ArrayList();
        PopulateTaskDetailList();

        _rv = (RecyclerView) findViewById(R.id.rvChapList);
        _li = getLayoutInflater();
        _rv.setLayoutManager(new LinearLayoutManager(this));

        _adapter = new SubtaskDetailAdapter();
        _rv.setAdapter(_adapter);
    }

    public void PopulateTaskDetailList() {
        int iNumEntries = _alEntries.size();
        String sSubtask = "";
        TaskItem tiEntry = null;
        DetailItem diEntry = null;

        // extract subtasks for indicated task
        for (int i = 0; i < iNumEntries; i++) {
            tiEntry = (TaskItem) _alEntries.get(i);

            // if this task entry has the indicated Task name, save it's data
            if (tiEntry.get_TaskName().equals(_sTaskName)) {
                diEntry = new DetailItem(tiEntry.get_Subtask(),
                                         tiEntry.get_StepCompleted(),
                                         tiEntry.get_DateCompleted());

                _alTaskEntries.add(diEntry);
            }
        }
    }

    private class DetailItem {
        private String _sSubTaskName = "";
        private String _sStep = "";
        private String _sDate = "";

        private DetailItem(String sSubTaskName, String sStep, String sDate) {
            _sSubTaskName = sSubTaskName;
            _sStep = sStep;
            _sDate = sDate;
        }

        private String get_Subtask() { return _sSubTaskName; }

        public void set_Subtask(String sTaskName) { _sSubTaskName = sTaskName; }

        private String get_Step() { return _sStep; }

        public void set_Step(String sStep) { _sStep = sStep; }

        private String get_Date() { return _sDate; }

        public void set_Date(String sDate) { _sDate = sDate; }
    }

    private class SubtaskDetailAdapter extends RecyclerView.Adapter<SubtaskDetailAdapter.DetailViewHolder> {

        /**
         * Inflates (creates & fills) a new subtask_detail View, and then creates/returns a new
         * DetailViewHolder object for that view.
         * @param parent Unfortunately the docs currently don't explain this at all :(
         * @param viewType Unfortunately the docs currently don't explain this at all :(
         * @return
         */
        @Override
        public SubtaskDetailAdapter.DetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            // "inflate" (create & fill) a new holder / container view based on the task_item
            // layout, without "attaching" it to the parent view
            View v = _li.inflate(R.layout.subtask_detail, parent, false);

            // create an instance of DetailViewHolder based on this "inflated" "holder" view
            return new SubtaskDetailAdapter.DetailViewHolder(v);
        }

        /**
         * This method "Binds" or assigns Data (from _alTaskEntries) to each SubtaskDetail (ViewHolder).
         * @param holder The SubtaskDetail instance at a given position in the list
         * @param position The current position of the SubtaskDetail we are Binding to, based upon
         *                 our (listOfData). So for the second ViewHolder we create, we'll bind data
         *                 from the second Item in listOfData.
         */
        @Override
        public void onBindViewHolder(SubtaskDetailAdapter.DetailViewHolder holder, int position) {
            // the ViewHolder data
            DetailItem currentItem = (DetailItem) _alTaskEntries.get(position);

            holder._tvSubtask.setText(currentItem.get_Subtask());
            holder._etDate.setText(currentItem.get_Date());
            holder._spSteps.setSelection(position, true);
        }

        /**
         * This method helps our Adapter determine how many ViewHolders it needs to create,
         * based on the size of the Dataset (List) it is working with.
         * Returning 0 here would tell our Adapter not to make any Items.
         *
         * @return the size of the dataset to be represented in the RecyclerView
         **/
        @Override
        public int getItemCount() { return _alTaskEntries.size(); }

        /**
         * A ViewHolder is a container for a set of Views we want to populate with Data
         **/
        class DetailViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

            // view holders for views to bind in the layout
            private TextView  _tvSubtask;
            private EditText  _etDate;
            private Spinner   _spSteps;
            private ViewGroup _vgContainer;

            private DetailViewHolder(View itemView) {
                super(itemView);

                // use itemView with findViewByID, because we are looking for an ID in
                // the SubtaskDetail view container we created / inflated above
                _tvSubtask = (TextView) itemView.findViewById(R.id.txtChapNum);
                _spSteps = (Spinner) itemView.findViewById(R.id.spnSteps);
                _etDate = (EditText) itemView.findViewById(R.id.edtDate);

                _vgContainer = (ViewGroup) itemView.findViewById(R.id.subtask_detail);

                _spSteps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(AdapterView<?> adapter, View v,
                                               int position, long id) {
                        // On selecting a spinner item
                        String sStep = adapter.getItemAtPosition(position).toString();

                        _spSteps.setSelection(position);
                    }

                    @Override
                    public void onNothingSelected(AdapterView<?> arg0) {
                        // TODO Auto‐generated method stub
                    }
                });

                /* We can pass "this" as an Argument, because DetailViewHolder implements the
                   View.OnClickListener interface. */
                _vgContainer.setOnClickListener(this);
            }

            @Override
            public void onClick(View v) {
                // currently unused
            }
        }
    }
}

Thanks for any help you can offer as I try to learn this stuff!

NEW INFORMATION

I have found a solution for my question #3, though it feels like a kluge. There may be a better way, but since no one has enlightened me to it, I'm left with this.

My array of data to display in the RecyclerView is an array of DetailItem objects. I have added to the DetailItem class another instance variable to hold the Spinner view which will be associated with that subtask step.

Here's the updated DetailItem class definition:

private class DetailItem {
    private String   _sSubTaskName = "";
    private String   _sStep = "";
    private Spinner  _spSteps;
    private String   _sDate = "";

    private DetailItem(String sSubTaskName, String sStep, String sDate) {
    _sSubTaskName = sSubTaskName;
    _sStep = sStep;
    _sDate = sDate;
    }

    private String get_Subtask() { return _sSubTaskName; }
    public void set_Subtask(String sTaskName) { _sSubTaskName = sTaskName; }
    private String get_Step() { return _sStep; }
    public void set_Step(String sStep) { _sStep = sStep; }
    private Spinner get_Spin() { return _spSteps; }
    public void set_Spin(Spinner spSteps) { _spSteps = spSteps; }
    private String get_Date() { return _sDate; }
    public void set_Date(String sDate) { _sDate = sDate; }
}

I modified the adapter to store with the task step the Spinner that displays / selects it. I also moved the code to set the Listener for the Spinner from the ViewHolder to the Adapter.

Here's the updated SubtaskDetailAdapter class definition with its expanded onBindViewHolder method:

private class SubtaskDetailAdapter extends RecyclerView.Adapter<SubtaskDetailAdapter.DetailViewHolder> {
    /** no changes */
    @Override
    public SubtaskDetailAdapter.DetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // ...
    }

    @Override
    public void onBindViewHolder(SubtaskDetailAdapter.DetailViewHolder holder, int position) {
        // the ViewHolder data
        DetailItem currentItem = (DetailItem) _alTaskEntries.get(position);

        holder._tvSubtask.setText(currentItem.get_Subtask());
        holder._etDate.setText(currentItem.get_Date());

        // store the spinner in the DetailItem object
        currentItem.set_Spin(holder._spSteps);

        // store DetailItem object in the array
        _alTaskEntries.set(position, currentItem);

        // look for Spinner step matching this entry's step
        String sStep = currentItem.get_Step();
        int iSel = 0;
        while (iSel < sSteps.length && !sSteps[iSel].equals(sStep))
            iSel++;

        // if matching step is found, set Spinner to show it
        if (iSel < sSteps.length) holder._spSteps.setSelection(iSel, true);

        // if matching step isn't found, show error message
        else Toast.makeText(getApplicationContext(),
                            "Unrecognized Step: " + sStep,
                            Toast.LENGTH_SHORT).show();

        // set listener for spinner selections
        holder._spSteps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapter, View v,
                                       int position,           long id) {
                // get selected step
                String sStep = adapter.getItemAtPosition(position).toString();

                DetailItem currentItem = null;
                int iNumDetails = _alTaskEntries.size();
                int iDetail = 0;
                // fast-forward to array entry for this adapter (spinner)
                while (iDetail < iNumDetails) {
                    currentItem = (DetailItem) _alTaskEntries.get(iDetail);
                    if (currentItem.get_Spin().equals(adapter))
                        break;
                    else
                        iDetail++;
                }

                // if found, save it in the array of detail items
                if ((iDetail < iNumDetails) && (currentItem != null)) {
                    currentItem.set_Step(sStep);
                    _alTaskEntries.set(iDetail, currentItem);
                }

                adapter.setSelection(position);
            }

            @Override
            public void onNothingSelected(AdapterView<?> arg0) {
                // TODO Auto-generated method stub
            }
        });
    }

I removed the code in the ViewHolder class to set an OnClickListener on the container view, since I don't want to respond to taps on the RecyclerView item, but only on individual views within it. An empty OnClick is required, since the holder is declared to implement the OnClickListener interface (I don't know if this is required).

Here's the updated (simpler) ViewHolder class :

class DetailViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    // view holders for views to bind in the layout
    private TextView  _tvSubtask;
    private EditText  _etDate;
    private Spinner   _spSteps;

    private DetailViewHolder(View itemView) {
        super(itemView);

        // use itemView with findViewByID, because we are looking for an ID in
        // the SubtaskDetail view container we created / inflated above
        _tvSubtask = (TextView) itemView.findViewById(R.id.txtChapNum);
        _spSteps = (Spinner) itemView.findViewById(R.id.spnSteps);
        _etDate = (EditText) itemView.findViewById(R.id.edtDate);
    }

    @Override
    public void onClick(View v) {
        // currently unused
    }
}

My questions #1 & #2 are still unsolved, which makes my app fairly unusable at the moment... Anyone want credit for solving this???

2 Answers

Big thanks to my friend Sheldon for solving this one!

Questions #1 & #2 are both resolved now. The problem was simply a matter of text color! The selected items from the Spinner were being displayed in white in the dropdown, which is fine, because the background of the dropdown is black. But then the text color is still white after the dropdown is removed, and my activity has an off-white background with white text.

Here is the updated code with the relevant changes:

@Override
public void onBindViewHolder(SubtaskDetailAdapter.DetailViewHolder holder, int position) {
    // the ViewHolder data
    DetailItem currentItem = (DetailItem) _alTaskEntries.get(position);

    holder._tvSubtask.setText(currentItem.get_Subtask());
    holder._etDate.setText(currentItem.get_Date());

    // store the spinner in the DetailItem object
    currentItem.set_Spin(holder._spSteps);

    // store DetailItem object in the array
    _alTaskEntries.set(position, currentItem);

    // initialize spinner - 1st, set correct text color
    holder._spSteps.setSelection(0, true);
    View v = holder._spSteps.getSelectedView();
    ((TextView)v).setTextColor(ContextCompat.getColor(DetailActivity.this,
                                                 R.color.colorPrimaryDark));

    // look for Spinner step matching this entry's step
    String sStep = currentItem.get_Step();
    int iSel = 0;
    while (iSel < sSteps.length && !sSteps[iSel].equals(sStep))
        iSel++;

    // if matching step is found, set Spinner to show it
    if (iSel < sSteps.length) holder._spSteps.setSelection(iSel, true);

    // if matching step isn't found, show error message
    else Toast.makeText(getApplicationContext(),
                        "Unrecognized Step: " + sStep,
                        Toast.LENGTH_SHORT).show();

    // set listener for spinner selections
    holder._spSteps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> adapter, View v,
                                   int position,           long id) {
            // ensure selected Spinner text has correct color
            int tc = ContextCompat.getColor(BookActivity.this,
                                            R.color.colorPrimaryDark);
            ((TextView) v).setTextColor(tc);

            // get selected step
            String sStep = adapter.getItemAtPosition(position).toString();

            DetailItem currentItem = null;
            int iNumDetails = _alTaskEntries.size();
            int iDetail = 0;
            // fast-forward to array entry for this adapter (spinner)
            while (iDetail < iNumDetails) {
                currentItem = (DetailItem) _alTaskEntries.get(iDetail);
                if (currentItem.get_Spin().equals(adapter))
                    break;
                else
                    iDetail++;
            }

            // if found, save it in the array of detail items
            if ((iDetail < iNumDetails) && (currentItem != null)) {
                currentItem.set_Step(sStep);
                _alTaskEntries.set(iDetail, currentItem);
            }

            adapter.setSelection(position);
        }

        @Override
        public void onNothingSelected(AdapterView<?> arg0) {
            // TODO Auto-generated method stub
        }
    });
}
Related