116

I have an ImageView that has a drawable image resource set to a selector. How do I programmatically access the selector and change the images of the highlighted and non-highlighted state?

Here is a code of selector:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/iconSelector">
  <!-- pressed -->
  <item android:state_pressed="true" android:drawable="@drawable/btn_icon_hl" />
  <!-- focused -->
  <item android:state_focused="true" android:drawable="@drawable/btn_icon_hl" />
  <!-- default -->
  <item android:drawable="@drawable/btn_icon" />
</selector>

I want to be able to replace btn_icon_hl and btn_icon with other images.

Keavon
  • 6,837
  • 9
  • 51
  • 79
dropsOfJupiter
  • 6,763
  • 12
  • 47
  • 59

2 Answers2

246

As far as I've been able to find (I've tried doing something similar myself), there's no way to modify a single state after the StateListDrawable has already been defined. You can, however, define a NEW one through code:

StateListDrawable states = new StateListDrawable();
states.addState(new int[] {android.R.attr.state_pressed},
    getResources().getDrawable(R.drawable.pressed));
states.addState(new int[] {android.R.attr.state_focused},
    getResources().getDrawable(R.drawable.focused));
states.addState(new int[] { },
    getResources().getDrawable(R.drawable.normal));
imageView.setImageDrawable(states);

And you could just keep two of them on hand, or create a different one as you need it.

Kevin Coppock
  • 133,643
  • 45
  • 263
  • 274
  • 1
    I wasn't able to add this to an image view. setState is not available on it. – dropsOfJupiter Jan 16 '11 at 04:32
  • 2
    actually I found it, its setImageDrawable() Thank you very much it worked and saved me about 40 xml files! – dropsOfJupiter Jan 16 '11 at 04:43
  • 2
    So I have another related note to this. I was hoping you can answer. I have this selector set on the ImageView that is inside the Custom Control. Custom control also has a selector as a background. So the selector of the whole control works, while the ImageView selector does not. Is there something I am doing wrong? Is there a sequence? – dropsOfJupiter Jan 16 '11 at 07:15
  • 1
    You're welcome! Yeah I don't know why I put setState, should be setImageDrawable, you're right. As per your other question, I'd suggest posting a new question with the code for your custom control and its selector, I'm not sure on the answer to that. – Kevin Coppock Jan 16 '11 at 15:04
  • 1
    here is the question. http://stackoverflow.com/questions/4651441/several-controls-in-one-with-drawable-background-gradient – dropsOfJupiter Jan 17 '11 at 03:47
  • 3
    i am using the same code. always the image which i have specified in ----> new int[]{} state remains. where i was wrong ?? – KK_07k11A0585 Jun 19 '12 at 12:34
  • @dropsOfJupiter , kcoppock : I'm trying to do something similar in a listview with alternating background colors, created a "darkBgStateList" and "lightBgStateList", however the same color is applied to all the even or odd views every time a even/odd is selected, as if they shared. Can't different views use the same selector ? – Rafael Nobre Jan 29 '13 at 20:13
  • 1
    I imagine you're doing something in your "set selected" logic or your getView() logic that's settings all odds or evens. Best to start a new question for this. – Kevin Coppock Jan 29 '13 at 20:23
  • @KK_07k11A0585 Thats because you need to implement the onclick, else it sees it as a non pressable imageview – Daan Oerlemans Dec 04 '14 at 10:22
  • 1
    Be careful, the order in which you add the states is important! – Jleuleu Mar 30 '15 at 10:26
  • cant we have this on a Button rather an ImageView? – Adam Jun 09 '16 at 02:39
  • Thank you so much. I was tired of creating so many drawables from xml. This snippet helps making a global method that I can call from anywhere. Btw you can make this with color too instead of drawable states.addState(new int[]{android.R.attr.state_activated}, ContextCompat.getDrawable(context,R.color.black)); – Alex Jul 16 '16 at 02:39
  • 1
    Just for your information, the order of calling `addState` is extremely important. – Cheok Yan Cheng Apr 14 '18 at 16:31
6

I had the same problem and went a step further to solve it. The only problem however is you can not specify the NavStateListDrawable in xml, so you have to set the background of your UI element through code. The onStateChange method must then be overriden to ensure that every time the level of the main drawable is changed, that you also update the level of the child level list.

When constructing the NavStateListDrawable you have to pass in the level of the icon you wish to display.

public class NavStateListDrawable extends StateListDrawable {

    private int level;

    public NavStateListDrawable(Context context, int level) {

        this.level = level;
        //int stateChecked = android.R.attr.state_checked;
        int stateFocused = android.R.attr.state_focused;
        int statePressed = android.R.attr.state_pressed;
        int stateSelected = android.R.attr.state_selected;

        addState(new int[]{ stateSelected      }, context.getResources().getDrawable(R.drawable.nav_btn_pressed));
        addState(new int[]{ statePressed      }, context.getResources().getDrawable(R.drawable.nav_btn_selected));
        addState(new int[]{ stateFocused      }, context.getResources().getDrawable(R.drawable.nav_btn_focused));

        addState(new int[]{-stateFocused, -statePressed, -stateSelected}, context.getResources().getDrawable(R.drawable.nav_btn_default));


    }

    @Override
    protected boolean onStateChange(int[] stateSet) {

        boolean nowstate = super.onStateChange(stateSet);

        try{
            LayerDrawable defaultDrawable = (LayerDrawable)this.getCurrent();


            LevelListDrawable bar2 =  (LevelListDrawable)defaultDrawable.findDrawableByLayerId(R.id.nav_icons);
            bar2.setLevel(level);
        }catch(Exception exception)
        {

        }

        return nowstate;
    }
}

For all of the different navigation button drawable states i have something like the following.

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

   <item android:drawable="@drawable/top_bar_default" >

   </item>

    <item android:id="@+id/nav_icons" android:bottom="0dip">
        <level-list xmlns:android="http://schemas.android.com/apk/res/android">
            <item android:maxLevel="0" >
                <bitmap
                    android:src="@drawable/top_bar_icon_back"
                    android:gravity="center" />
            </item>
            <item android:maxLevel="1" >
                <bitmap
                    android:src="@drawable/top_bar_icon_nav"
                    android:gravity="center" />
            </item>
            <item android:maxLevel="2" >
                <bitmap
                    android:src="@drawable/top_bar_icon_settings"
                    android:gravity="center" />
            </item>
            <item android:maxLevel="3" >
                <bitmap
                    android:src="@drawable/top_bar_icon_search"
                    android:gravity="center" />
            </item>
        </level-list>

    </item>

</layer-list>

I was going to post this as a question and answer, but seeing as you've asked the very question, here you go. Note, this saves you a hell of a lot of xml file definitions. i went down from about 50-100 xml definitions down to about 4!.

Emile
  • 11,451
  • 5
  • 50
  • 63
  • The navstatelistdrawable code effectively makes the selector xml redundant. – Emile Jan 15 '11 at 04:40
  • Hi emilie, Is there a possibility that drawables as a button background wont show up the first time for any reason. Currently I am getting the problem where I need to touch the area of the button for the background to show up, or switch out and switch back to the activity. (This only happens on a hdpi screen, but works fine on my mdpi). I believe others may have this problem too. Is your code tested for all screen densities? – shecodesthings Oct 30 '12 at 13:29
  • Hi, no, this was written a fair while back and was only for one device at the time. I'm not sure what kind of issue might arrise though as far as i'm aware multiple screen densities and layouts shouldn't present a problem. – Emile Nov 02 '12 at 12:12
  • Thanks I don't quite know what I was doing wrong but in the end I just had the following : buttonStates = new StateListDrawable(); buttonStates.addState(new int[]{statePressed}, ApplicationConstants.moduleImageLoader.findImageByName(drawable_pressed)); buttonStates.addState(new int[]{-stateFocused, -statePressed, -stateSelected}, ApplicationConstants.moduleImageLoader.findImageByName(drawable_normal)); – shecodesthings Nov 09 '12 at 20:03
  • 1
    This is the first place where I see that negative values have to be used for states set to false. The documentation is not very clear about it. Thanks for the tip! – eocanha Oct 02 '13 at 13:34