P. Savrov P. Savrov - 3 months ago 26
Android Question

How to update certain View of invisible item in ListView? [Android]

community!
I need in a help with updating invisible items in ListView. It's not about item content, but item's View representation.
Ok, let me show you my example. I have a string-array:

<string-array name="reminder_notifications">
<item>15 minutes before</item>
<item>30 minutes before</item>
<item>1 hour before</item>
<item>1.5 hour before</item>
<item>5 hours before</item>
<item>8 hours before</item>
<item>1 day before</item>
</string-array>


In Activity i have created adapter:

adapterNotifications = ArrayAdapter.createFromResource(this, R.array.reminder_notifications, R.layout.dialog_list_multiple_choise);


After, with some methods i calculate whitch items from string-array are avaliable for current reminder. E.g. if user at 16:00 set reminder for 16:45 then he can only pick items
15 minutes before
and
30 minutes before
. Other items should be disabled.
So, after google i found out how to get access to invisible ListView child at certain position:

public View getViewByPosition(int position, ListView listView) {
final int firstListItemPosition = listView.getFirstVisiblePosition();
final int lastListItemPosition = firstListItemPosition + listView.getChildCount() - 1;
if (position < firstListItemPosition || position > lastListItemPosition ) {
return listView.getAdapter().getView(position, listView.getChildAt(position), listView);
} else {
final int childIndex = position - firstListItemPosition;
return listView.getChildAt(childIndex);
}
}


Now, I'm facing with last problem (i hope) - how to update item's View that i get from method above? I tried to use this:

View v = getViewByPosition(position, lvNotifications);
v.setEnabled(true);


But it updates View only after first opening dialog, another words i have to open the dialog window with ListView, close it and re-open. Only in that case i'll get an updated view.
I know, that my english is awful so there are screenshots below:

Main dialog. Before opening the dialog with ListView
Main dialog. Before opening the dialog with ListView

List view dialog. First opening. No items are disabled. WRONG
List view dialog. First opening. No items are disabled. WRONG

List view dialog. Second opening. 5 items are disabled. RIGHT
List view dialog. Second opening. 5 items are disabled. RIGHT

Thank you.

Answer

You tackled the issue from the wrong side. You should not edit the views from outside of a adapter, that's what the adapter is for. Instead, write your own adapter. Do it like this:

import android.content.Context;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;

/**
 * A {@link ArrayAdapter} to let the user select multiple notification times.
 */
public class ReminderNotificationsAdapter extends BaseAdapter implements CompoundButton.OnCheckedChangeListener {

    /**
     * A array with all currently selected entries
     */
    private boolean[] mSelected;

    /**
     * A array with all enabled entries
     */
    private boolean[] mEnabled;

    /**
     * The items to be shown
     */
    private String[] mItems;

    /**
     * A {@link Context}
     */
    private Context mContext;

    /**
     * Creates a new instance
     *
     * @param context a {@link Context}
     * @param items all selectable items
     * @param checkedItems all selected items. This array will be updated with the users selectiion
     * @param enabledItems all enabled items
     */
    public ReminderNotificationsAdapter(Context context, String[] items, boolean[] checkedItems, boolean[] enabledItems) {
        // Check array sizes
        if(items.length != checkedItems.length || checkedItems.length != enabledItems.length) {
            throw new RuntimeException("All arrays must be the same size");
        }

        // Add all and store params
        this.mContext = context;
        this.mItems = items;
        this.mSelected = checkedItems;
        this.mEnabled = enabledItems;

    }

    @Override
    public int getCount() {
        return this.mItems.length;

    }

    @Override
    public String getItem(int i) {
        return this.mItems[i];

    }

    @Override
    public long getItemId(int i) {
        return this.getItem(i).hashCode();

    }

    @NonNull
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = convertView;

        // Create view if not provided to convert
        if(v == null) {
            v = LayoutInflater.from(this.mContext).inflate(R.layout.dialog_list_multiple_choise, parent, false);
        }

        // Prepare text view
        TextView tv = (TextView) v.findViewById(android.R.id.text1);
        tv.setText(this.getItem(position));
        tv.setEnabled(this.isEnabled(position));

        // Prepare checkbox
        CheckBox cb = (CheckBox) v.findViewById(android.R.id.checkbox);
        cb.setTag(position);
        cb.setChecked(this.mSelected[position]);
        cb.setEnabled(this.isEnabled(position));
        cb.setOnCheckedChangeListener(this);

        // Return view
        return v;

    }

    @Override
    public boolean isEnabled(int position) {
        return this.mEnabled[position];

    }

    @Override
    public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
        this.mSelected[(Integer) compoundButton.getTag()] = b;

    }
}

And use it like this:

@Override
public void onClick(View view) {
    // Tell what string should be shown
    String[] entries = this.getResources().getStringArray(R.array.reminder_notifications);

    // Tell what entries should be already selected
    final boolean[] selectedEntries = new boolean[entries.length];
    selectedEntries[2] = true;

    // Tell what entries should be enabled
    boolean[] enabledEntries = new boolean[entries.length];
    enabledEntries[0] = true;
    enabledEntries[1] = true;
    enabledEntries[2] = true;
    enabledEntries[3] = true;

    // Create the adapter
    ReminderNotificationsAdapter a = new ReminderNotificationsAdapter(this, entries, selectedEntries, enabledEntries);

    // Create and show the dialog
    new AlertDialog.Builder(this)
            .setTitle("Add notification")
            .setAdapter(a, null)
            .setPositiveButton("Set", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    // Do what you want to do with the selected entries
                    Toast.makeText(MainActivity.this, Arrays.toString(selectedEntries), Toast.LENGTH_SHORT).show();

                }
            })
            .setNegativeButton("Dismiss", null)
            .show();

}

I simply used a boolean array to tell what entries should be enabled and selected, you can do something more elegant there if you want. The selection of the user is updated in the array provided to the adapter's constructor. The AlertDialog looks like this:

Screenshot

See the full example app here.