Blogue de Macadamian

Le contenu du blogue est disponible uniquement en anglais.

Android - Theming the Unthemable

If you are relatively new to Android, we highly recommend you check out our guide: Your First Android Release - It Could Go Well (Or Really, Really Badly)

“What we've got here is... failure to theme. Some views you just can't reach. So you get what we had here last project, which is the way Android wants it... well, it gets it. I don't like it any more than you men.” – Captain, Road Prison 36

Some of you might recognize the previous paragraph as the introduction of Guns ‘N Roses’ Civil War or from the movie Cold Hand Luke starring Paul Newman. This is the feeling I get when I try to create a custom theme for an application on Android. The Android SDK does permit some level of theming, which is not really well documented to start with. Other things are hard-coded, “so you get what we had here last project”. Now, one of the things your application will most likely use is the Options menu, which is the menu you see when you press the hard menu key. It is kind of... orange. In our last project, we had to completely remove the orange in favour of our customer’s color scheme, which is on the blue side.

I couldn’t find a way to change the menu item background using the theme.xml and styles.xml files of our own theme. Furthermore, most blogs and articles I found told me that this was impossible. Inconceivable! So I went looking at Android’s source code. Thank [insert favourite deity or Darwin] for open source!

I started from the layout files for the platform I was using, in this case Android 1.5, in /platforms/android-1.5/data/res/layout. There, I found the file icon_menu_item_layout.xml and in that file I saw that it uses a custom item of class com.android.internal.view.menu.IconMenuItemView.

That class can be easily found in the Android source code (IconMenuItemView.java). Once I opened the file, I noticed that it doesn’t do much in regards of the background. So I went to the class that creates and controls it, IconMenuView.

Now, in that file, I did see that android resets the background of menu items every time it adds an item to the menu. That background is indeed hard-coded to an internal drawable file. So what can we do?

Enter the LayoutInflater.Factory class (again…).

The IconMenuItemView is just a derived class from the basic View class. And as such we can call setBackgroundResource method on it. So, in our LayoutInflater factory class, I simply detected the name “com.android.internal.view.menu.IconMenuItemView”, asked my inflater to create the view and kind of (more on this later) applied my own background.

public View onCreateView(String name, Context context, AttributeSet attrs) { // Do you own inflater stuff here // Check for menu items (Options menu AKA menu key) if (name.equalsIgnoreCase("com.android.internal.view.menu.IconMenuItemView")) { try { // Ask our inflater to create the view View view = inflater_.createView(name, null, attrs); // Kind of apply our own background view.setBackgroundResource(myBackground); return view; } catch (InflateException e) { } catch (ClassNotFoundException e) { } } return null; }

The only problem with that previous code is that Android will go on and set the default background right after this. So we simply need to post our background change as a Runnable to the UI thread, which will be executed after Android sets the default background. Just replace the bold line in the code above with the following: new Handler().post(new Runnable() { public void run() { view.setBackgroundResource(myBackground); } });

Finally, every time your application is about to display the options menu, you need to re-apply the background. Android might re-use or re-create the menu item and push its default background. So in your factory, you just need to add the menu items in a list and create a function that sets your own background to the list items. Remember to post to the UI thread once again!

About the Author

no picture
Sébastien Tardif


Visit Website

+ Comments

#1
Dimka
Jan 20, 2010
08:27

Wow, thanks, i love my Android )) Spica from samsung...

#2
Nayan
Feb 17, 2010
11:00

For a long time, i have been trying to change the background color of the menu but i failed. Today i found your post but alas! i'm a beginner in android and understood nothing you described. would you please send me a piece of code/a project source code implementing this? please help me. thank you

#3
Hemaraj
Apr 21, 2010
06:51

Hi, Tried using above code but I am getting error for attributes. can you please guide me which attributes i need to read here. Is it from my defined XML file or from com.android.internal.view.menu.IconMenuItemView ?

#4
Ricky
Apr 26, 2010
05:35

Hi, thanks you for your tip. Can you post a complete example of the code? I don't understand very well how to integrate it in my app. Thanks you again.

#5
Hemaraj
Apr 29, 2010
11:55

Hi Ricky,

here is the example code to set the background color to the menu view. In your activity onCreate method

/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);

    LayoutInflater inflater = getLayoutInflater();
    if (inflater.getFactory() == null)
    {
        Factory factory = new MyLayoutInflate(getLayoutInflater());
        inflater.setFactory(factory);
    }
}

override OnCreateOptionsMenu() and onOptionsItemSelected

MyLayoutInflate is a seperate class which implements LayoutInflater.Factory

public class MyLayoutInflate implements LayoutInflater.Factory { LayoutInflater lminflater; public MyLayoutInflate(LayoutInflater inflat) { // TODO Auto-generated constructor stub lminflater = inflat; }

    @Override
    public View onCreateView(String name, Context context,
            AttributeSet attrs) {
     if (name.equalsIgnoreCase

("com.android.internal.view.menu.IconMenuItemView")) { System.out.println("IF CONDITION"); try { // Ask our inflater to create the view
final View view = lminflater.createView(name, null,attrs); // // Kind of apply our own background

                 new Handler().post(new Runnable()
                    {

                     public void run()
                        {
                            view.setBackgroundColor(Color.GREEN);
                        }
                    });
                 return view;  
                 }catch (InflateException e)  {   
                 } catch (ClassNotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }   
         }
        return null;
    }

}

I hope this code helps you

#6
Dimaseg
Jun 03, 2010
03:51

But HTC Desire is the best )) Android RULES

#7
Matthew
Jul 07, 2010
04:32

I didn't understand why should we return to that factory..now everything is clear! If you don't want to purchase cv - you may use the ideas behind...I hope,you know what I mean, if no - just PM me.

#8
Brian
Jul 29, 2010
01:32

Thanks for this very much.

I'm trying to use a select dialog after a long press. The class for that is com.android.internal.app.AlertController$RecycleListView which extends ListView. I wanted to cast the view received from the Inflater and get a child and change it's background. But the children are put into the list after the code you provided.

You mentioned something for manipulating the list items in the list. You said "So in your factory, you just need to add the menu items in a list and create a function that sets your own background to the list items."

What did you mean by that?

Thanks Brian

#9
Brian
Jul 29, 2010
02:59

I realize I can use the class name to look for other elements of the What if I wanted to add something unique to a view? For instance I'd like to add a close button off to the upper right of the dialog. I'd like to get a hold of the header view and add the button off to the right of that for a custom close option.

Thanks

Brian
Jul 29, 2010
05:16

I used this to get at "topPanel" that the dialog uses to place the header title in. Then I added my own view with image or button.

    if (name.equalsIgnoreCase("LinearLayout")) {
        try {
            final LinearLayout ll = (LinearLayout) lminflater.createView(name, null, attrs);
            if (ll.getId() == com.android.internal.R.id.topPanel)
            {
                final LinearLayout linear = new LinearLayout(context);
                TextView text = new TextView(context);
                text.setText("Detail Header");
                ImageView image = new ImageView(context);
                image.setImageDrawable(context.getResources().getDrawable(com.android.mystuff.R.drawable.mylogo));
                linear.addView(text);
                linear.addView(image);
                int id = ll.getId();

                new Handler().post(new Runnable() {

                    public void run() {
                        ll.addView(linear);
                    }
                });
            }
            return ll;
        } catch (InflateException e) {
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
Laissez un commentaire:

Votre nom içi :)

Avez-vous un site web? Placez-le içi.

On vous promet que nous n'allons pas partager votre addresse courriel.


Tapez votre commentaire içi... les dimensions de cette boîte s'ajustera automatiquement!

NB: Les liens seront automatiquement convertits!

S'il vous plaît tapez le mot que vous voyer par dessus.



* - indique un champ requis

Here's a preview of your comment:

macadamian
English
Communiquez avec nous: 1-877-779-6336 or Envoyez-nous un courriel