Search code examples
androidnine-patch

Creating and combining NinePatchDrawables at runtime


I am creating a compound custom control. The control consists of a single element across the bottom and an unknown number of elements across the top. The background for each of these elements is a StateListDrawable defining the NinePatchDrawables to use for each state of that element.

As the individual top elements change their width to suit their content, the region of the bottom element directly below needs to also change width to suit. The top elements may also have their positions shifted left or right.

control layout mock-up

The images above and below are just quick mock-ups to convey the layout and behavior I need to achieve. I am not responsible for designing the actual background images to be used, but they will not be a uniform flat grey.

desired stretch behavior

I managed to solve the first part of this re-sizing problem by:

  • Creating a custom class which extends Drawable.
  • Passing the class an array of NinePatchDrawables defining various potential regions of the Bottom View (eg left_end, right_end, middle_below_control, middle_below_gap).
  • Changing the bounds of each to match the relevant view above during the custom class' onBoundChange().
  • Drawing my array of re-sized NinePatches to the Bottom View canvas in onDraw().
  • Setting a combined padding for the Bottom View based on the padding of the NinePatches in the array.

This solution raises a number of problems though:

  • You need StateListDrawables for each of the potential regions of the Bottom View.
  • You need individual NinePatch images for each of the states in each of these StateListDrawables.
  • You have to design an image for the Bottom View which is restricted to being very uniform across it's width.
  • You need ear defenders and a thick skin to deal with all the shouting from the graphic designers.

What I really need to achieve is:

  • Have a single StateListDrawable for the Bottom View.
  • Extract the single image for each state and slice it up into the required sections.
  • Create a new NinePatchDrawable for each slice, applying the data Chunk from the view above for width only. Not height.
  • Then re-combine them as per my previous solution above.

Unfortunately as we know, you can't really play with the data Chunk. It's generated at compile time from the source image to create the NinePatch binary.

One of the better answers on this sort of topic can be found here. Based on this, a possible compromise solution might be:

  • For each of the top element types, have the graphic designers supply an additional blank dummy NinePatch source with the identical height and width dimensions, identical width padding and patch definitions, but with the height padding and patch definitions as they want for the bottom view.
  • Use getNinePatchChunk() on the relevant dummy NinePatch compiled resource.
  • Combine the resulting data Chunk with the relevant Bottom View bitmap slice to create a new NinePatch using NinePatch(Bitmap bitmap, byte[] chunk, String srcName).
  • Put up with dirty looks from the graphic designers, but at least they've stopped shouting.

This seems like it would be a bit of a clunky way of doing things. Is there a better, more efficient way of going about this to achieve all the required goals?

Note: There are likely to be many instances of this custom control in a scroll view, when used in an app, so nested layouts need to be kept to a minimum.


Solution

  • I have finally come up with a way to avoid using a blank dummy NinePatch source as necessary my compromise solution above.

    Based on this excellent solution to a similar problem, I have written a utility method to extract the padding info from a NinePatch resource. Using this, I can extract the necessary padding from the adjacent NinePatch regions, and apply them to my new slice. Hope this is helpful to others.

    public static Rect getNinePatchPadding(Context context, int drawableId) {
        Rect mPadding = new Rect();
        Bitmap bm = BitmapFactory.decodeResource(context.getResources(), drawableId);
        final byte[] chunk = bm.getNinePatchChunk();
        if (!NinePatch.isNinePatchChunk(chunk)) return null;
        ByteBuffer byteBuffer = ByteBuffer.wrap(chunk).order(ByteOrder.nativeOrder());
    
        // Skip to padding
        for (int i = 0; i < 12; i++) {
            byteBuffer.get();
        }
    
        mPadding.left = byteBuffer.getInt();
        mPadding.top = byteBuffer.getInt();
        mPadding.right = byteBuffer.getInt();
        mPadding.bottom = byteBuffer.getInt();
    
        return mPadding;
    }