11

I am creating a settings menu for a free version of my app. I have a ListPreference displaying many different options. However, only some of these options are to be made available in the free version (I would like all options to be visible - but disabled, so the user knows what they are missing!).

I'm struggling to disable certain rows of my ListPreference. Does anybody know how this can be achieved?

Adam Smith
  • 539
  • 1
  • 6
  • 16

4 Answers4

8

Solved it.

I made a custom class extending ListPreference. I then used a custom ArrayAdapter and used methods areAllItemsEnabled() and isEnabled(int position).

public class CustomListPreference extends ListPreference {

    public CustomListPreference (Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    protected void onPrepareDialogBuilder(Builder builder) {
        ListAdapter listAdapter = new CustomArrayAdapter(getContext(), R.layout.listitem, getEntries(), resourceIds, index);

        builder.setAdapter(listAdapter, this);
        super.onPrepareDialogBuilder(builder);
    }
}

and

public class CustomArrayAdapter extends ArrayAdapter<CharSequence> {

public CustomArrayAdapter(Context context, int textViewResourceId,
        CharSequence[] objects, int[] ids, int i) {
    super(context, textViewResourceId, objects);

}

   public boolean areAllItemsEnabled() {
        return false;
    }

    public boolean isEnabled(int position) {
        if(position >= 2)
            return false;
        else
            return true;
    }

public View getView(int position, View convertView, ViewGroup parent) {
             ...
    return row;
}
haxpor
  • 2,431
  • 3
  • 27
  • 46
Adam Smith
  • 539
  • 1
  • 6
  • 16
0

I searched through and through all over the web, and couldn't find a way to achieve this. The answer above did not help me. I found the entire "ArrayAdapter" method very unintuitive , unhelpful, and hard to implement.

Finally, I actually had to look inside the source code for "ListPreference", to see what they did there, and figure out how to override the default behavior cleanly and efficiently.

I'm sharing my solution below. I made the class "SelectiveListPreference" to inherit the behavior of "ListPreference", but add a positive button, and prevent closing when an option is pressed. There is also a new xml attribute to specify which options are available in the free version.

My trick is not to call ListPreference's version of onPrepareDialogBuilder, but instead implement my own, with a custom click handler. I did not have to write my own code for persisting the selected value, since I used ListPreference's code (that's why I extended "ListPreference" and not "Preference").

The handler looks for the boolean resource "free_version" and if it's true, it only allows the options specified in "entry_values_free" xml attribute. If "free_version" is false, all options are allowed. There's also an empty method for inheritors, if something should happen when an option is chosen.

Enjoy,

Tal

public class SelectiveListPreference extends ListPreference
{
    private int mSelectedIndex;
    private Collection<CharSequence> mEntryValuesFree;
    private Boolean mFreeVersion;


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

    //CTOR: load members - mEntryValuesFree & mFreeVersion
    public SelectiveListPreference(Context context, AttributeSet attrs)
    {
        super(context, attrs);

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.SelectiveListPreference);

        try
        {
            CharSequence[] entryValuesFree = a
                    .getTextArray(R.styleable.SelectiveListPreference_entryValuesFree);

            mEntryValuesFree = new ArrayList<CharSequence>(
                    Arrays.asList(entryValuesFree));
        } 
        finally
        {
            a.recycle();
        }

        Resources resources = context.getResources();
        mFreeVersion = resources.getBoolean(R.bool.free_version);
    }

    //override ListPreference's implementation - make our own dialog with custom click handler, keep the original selected index
    @Override
    protected void onPrepareDialogBuilder(android.app.AlertDialog.Builder builder)
    {
        CharSequence[] values = this.getEntries();

        mSelectedIndex = this.findIndexOfValue(this.getValue());

        builder.setSingleChoiceItems(values, mSelectedIndex, mClickListener)
                .setPositiveButton(android.R.string.ok, mClickListener)
                .setNegativeButton(android.R.string.cancel, mClickListener);
    };

    //empty method for inheritors
    protected void onChoiceClick(String clickedValue)
    {
    }

    //our click handler
    OnClickListener mClickListener = new OnClickListener()
    {
        public void onClick(DialogInterface dialog, int which)
        {
            if (which >= 0)//if which is zero or greater, one of the options was clicked
            {
                String clickedValue = (String) SelectiveListPreference.this
                        .getEntryValues()[which]; //get the value

                onChoiceClick(clickedValue);

                Boolean isEnabled;

                if (mFreeVersion) //free version - disable some of the options
                {
                    isEnabled = (mEntryValuesFree != null && mEntryValuesFree
                            .contains(clickedValue));
                } 
                else //paid version - all options are open
                {
                    isEnabled = true;
                }

                AlertDialog alertDialog = (AlertDialog) dialog;

                Button positiveButton = alertDialog
                        .getButton(AlertDialog.BUTTON_POSITIVE);

                positiveButton.setEnabled(isEnabled);

                mSelectedIndex = which;//update current selected index
            } 
            else //if which is a negative number, one of the buttons (positive or negative) was pressed.
            {
                if (which == DialogInterface.BUTTON_POSITIVE) //if the positive button was pressed, persist the value.
                {
                    SelectiveListPreference.this.setValueIndex(mSelectedIndex);

                    SelectiveListPreference.this.onClick(dialog,
                            DialogInterface.BUTTON_POSITIVE);
                }

                dialog.dismiss(); //close the dialog
            }
        }
    };
}

EDIT: we also need to override the implemented onDialogClosed from ListPreference (and do nothing), otherwise, things valued do not get persisted. Add:

protected void onDialogClosed(boolean positiveResult) {}
TalL
  • 1,661
  • 16
  • 16
0

Maybe you can do it by overrding default getView:

Steps:

  1. Extend ListPreference

  2. Override onPrepareDialogBuilder and replace mBuilder in DialogPreference with ProxyBuilder

  3. Handle getView in ProxyBuilder->AlertDialog->onShow->getListView->Adapter

Code samples are in custom row in a listPreference?

Community
  • 1
  • 1
galex
  • 593
  • 7
  • 10
0

Having the same problem I found a solution (maybe "hack" is more appropriate). We can register an OnPreferenceClickListener for the ListPreference. Inside this listener we can get the dialog (since the preference was clicked we are pretty safe that it is not null). Having the dialog we can set a OnHierarchyChangeListener on the ListView of the dialog where we are notified when a new child view is added. With the child view at hand we can disable it. Assuming that the ListView entries are created in the same order as the entry values of the ListPreference we can even get the entry value.

I hope somebody finds this helpful.

public class SettingsFragment extends PreferenceFragment {


  private ListPreference devicePreference;
  private boolean hasNfc;

  @Override
  public void onCreate(android.os.Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // load preferences
    addPreferencesFromResource(R.xml.preferences);

    hasNfc = getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC);

    devicePreference = (ListPreference) getPreferenceScreen().findPreference(getString(R.string.pref_device));

    // hack to disable selection of internal NFC device when not available
    devicePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {

        public boolean onPreferenceClick(Preference preference) {
            final ListPreference listPref = (ListPreference) preference;
            ListView listView = ((AlertDialog)listPref.getDialog()).getListView();
            listView.setOnHierarchyChangeListener(new OnHierarchyChangeListener() {

                // assuming list entries are created in the order of the entry values
                int counter = 0;

                public void onChildViewRemoved(View parent, View child) {}

                public void onChildViewAdded(View parent, View child) {
                    String key = listPref.getEntryValues()[counter].toString();
                    if (key.equals("nfc") && !hasNfc) {
                        child.setEnabled(false);
                    }
                    counter++;
                }
            });
            return false;
        }
    });
  }
}
Simikolon
  • 317
  • 3
  • 9