November 10, 2011

NinePatches, backgrounds, paddings, relayouts and some headache


UPDATE: I've moved my whole blog to a new domain. That's why the comments section is closed here. The new URL for this post is http://www.leonardofischer.com/ninepatches-backgrounds-paddings-relayouts-and-some-headache/. If you have any question, post it there.

Hi,

A few days ago I got stuck into a so called "bug". A developed a small NinePatch to use in an Android app. I wanted it to highlight a ViewGroup object, and then remove the background later. But the first time I set the NinePatch, the view appears to move a little bit. Weird!


Let's start from the beginning: what is a NinePatch?

It is a special image file. In the Android SDK, it is a file with the extension .9.png that you can open in any image editor. What makes it special is a border around the image that has a special meaning for the Android system. If you ask to draw this image with a different size than its width*height dimensions, the image will stretch only in some pre-defined areas and the others will keep their original size. Why you would use this?? A practical way to explain this is to show you a NinePatch in use:


There is a NinePatch on the left (with a lot of zoom, and a yellow line to the actual NinePatch dimensions). On the right, two buttons with different dimensions. You got it? The NinePatch is very stretched on the big button, but still look very good! There is a lot of material explaining how to get this effect on the web. I recommend you the official Android SDK for this (which also is the source of the image with the buttons, modified by me). The Android SDK also have a very simple tool that let's you generate these NinePatches.


The Symptom

So far, so good, the NinePatch works pretty well. Until you put it behind a view as its background. What happens? Let's see.



After I set the NinePatch as the background of the FrameLayout root_layout, the text is moved some pixels down (I drew a blue line so you can see that it moved exactly 4 pixels). Not too much, but it should move 0 pixels, no more, no less!


The Research for the Cure of the Headache

Well, actually the "bug" is not a "bug", but a feature! After researching the Android source a little bit, I understood that the optional padding that you should set in the NinePatch also gets into account when you set the ViewGroup's background. Let's look at the source of the method View.setBackgroundDrawable(Drawable d).

public void setBackgroundDrawable(Drawable d) {
    //some other code
    if (d != null) {
        Rect padding = sThreadLocal.get();
        //more intermediate code
        if (d.getPadding(padding)) {
            setPadding(padding.left, padding.top, 
                padding.right, padding.bottom);
        }
        //more code
    }
    //and the finishing code
}


The Medicine

As you can see, the view literally gets the padding that you set into the NinePatch and sets onto the view, changing its dimension. That is why you set the background and the view changes its position. If you do not set the optional padding, the padding of the NinePatch will be computed from the stretchable area, and will mess with your beautiful layout.

Optionally, you can get the padding before set the background, set your custom background, and then set the old padding. Example:

//backup the old padding
Rect padding = new Rect();
padding.left = rootLayout.getLeft();
padding.top = rootLayout.getTop();
padding.right = rootLayout.getRight();
padding.bottom = rootLayout.getPaddingBottom();
//set the new background
rootLayout.setBackgroundResource(R.drawable.nine_patch);
//restore the old padding
rootLayout.setPadding(padding.left, padding.top, 
    padding.right, padding.bottom);

This one is not as good as just set the padding on the NinePatch, as you need to run some extra code every time you set a background onto a view. But this hack may let you do something that I did not though of yet  ツ


The Side-Effects

Finally, I want to show you a side effect of using a NinePatch. Let's go back to the Android source code:

public void setBackgroundDrawable(Drawable d) {
    boolean requestLayout = false;
    //some initial code
    if (d != null) {
        // some other code, including the one presented above
        if (mBGDrawable == null || 
                mBGDrawable.getMinimumHeight() != d.getMinimumHeight() ||
                mBGDrawable.getMinimumWidth() != d.getMinimumWidth()) {
            requestLayout = true;
        }
        // more code
    }
    // pre-requestLayout code
    if (requestLayout) {
        requestLayout();
    }
    //finishing code
}

As you can see, if the previous background has different minimum width or height from the new one, the method will force a requestLayout() call. This is ok if you set the background during the application initialization. But if you start to swap your view's background, then you need to take care of these properties too. If not, your application may suffer from "hiccups" from the re-layout of your views.


Finishing, this is the source code that I developed for this post. It contains the "Hello World" example you saw above. If you click on the text, the background is added, so you can see for yourself this Android feature.

UPDATE: I've moved my whole blog to a new domain. That's why the comments section is closed here. The new URL for this post is http://www.leonardofischer.com/ninepatches-backgrounds-paddings-relayouts-and-some-headache/. If you have any question, post it there.

No comments: