Create a NinePatch/NinePatchDrawable in runtime
Asked Answered
B

7

41

I have a requirement on my Android application that parts on the graphics should be customizable, by retrieving new colors and images from the server side. Some of these images are nine-patch images.

I can't find a way to create and display these nine-patch images (that have been retrieved over the network).

The nine-patch images are retrieved and kept in the application as Bitmaps. In order to create a NinePatchDrawable, you either need the corresponding NinePatch or the chunk (byte[]) of the NinePatch. The NinePatch can NOT be loaded from the Resources, since the images doesn't exist in /res/drawable/. Furthermore, in order to create the NinePatch, you need the chunk of the NinePatch. So, it all drills down to the chunk.
The question is then, how do one format/generate the chunk from an existing Bitmap (containing the NinePatch information)?

I've searched through the Android source code and the Web and I can't seem to find any examples of this. To make things worse, all decoding of a NinePatch resources seem to be done natively.

Have anyone had any experiences with this kind of issue?

I'm targeting API level 4, if that is of importance.

Broca answered 22/2, 2011 at 15:7 Comment(0)
R
56

getNinePatchChunk works just fine. It returned null because you were giving Bitmap a "source" ninepatch. It needs a "compiled" ninepatch image.

There are two types of ninepatch file formats in the Android world ("source" and "compiled"). The source version is where you add the 1px transparency border everywhere-- when you compile your app into a .apk later, aapt will convert your *.9.png files to the binary format that Android expects. This is where the png file gets its "chunk" metadata. (read more)

Okay, now down to business.

  1. Client code, something like this:

    InputStream stream = .. //whatever
    Bitmap bitmap = BitmapFactory.decodeStream(stream);
    byte[] chunk = bitmap.getNinePatchChunk();
    boolean result = NinePatch.isNinePatchChunk(chunk);
    NinePatchDrawable patchy = new NinePatchDrawable(bitmap, chunk, new Rect(), null);
    
  2. Server-side, you need to prepare your images. You can use the Android Binary Resource Compiler. This automates some of the pain away from creating a new Android project just to compile some *.9.png files into the Android native format. If you were to do this manually, you would essentially make a project and throw in some *.9.png files ("source" files), compile everything into the .apk format, unzip the .apk file, then find the *.9.png file, and that's the one you send to your clients.

Also: I don't know if BitmapFactory.decodeStream knows about the npTc chunk in these png files, so it may or may not be treating the image stream correctly. The existence of Bitmap.getNinePatchChunk suggests that BitmapFactory might-- you could go look it up in the upstream codebase.

In the event that it does not know about the npTc chunk and your images are being screwed up significantly, then my answer changes a little.

Instead of sending the compiled ninepatch images to the client, you write a quick Android app to load compiled images and spit out the byte[] chunk. Then, you transmit this byte array to your clients along with a regular image-- no transparent borders, not the "source" ninepatch image, not the "compiled" ninepatch image. You can directly use the chunk to create your object.

Another alternative is to use object serialization to send ninepatch images (NinePatch) to your clients, such as with JSON or the built-in serializer.

Edit If you really, really need to construct your own chunk byte array, I would start by looking at do_9patch, isNinePatchChunk, Res_png_9patch and Res_png_9patch::serialize() in ResourceTypes.cpp. There's also a home-made npTc chunk reader from Dmitry Skiba. I can't post links, so if someone can edit my answer that would be cool.

do_9patch: https://android.googlesource.com/platform/frameworks/base/+/gingerbread/tools/aapt/Images.cpp

isNinePatchChunk: http://netmite.com/android/mydroid/1.6/frameworks/base/core/jni/android/graphics/NinePatch.cpp

struct Res_png_9patch: https://scm.sipfoundry.org/rep/sipX/main/sipXmediaLib/contrib/android/android_2_0_headers/frameworks/base/include/utils/ResourceTypes.h

Dmitry Skiba stuff: http://code.google.com/p/android4me/source/browse/src/android/graphics/Bitmap.java

Rigi answered 1/4, 2011 at 23:2 Comment(8)
Thanks kanzure for a very impressive answer! I can't believe I didn't come to think of sending compiled images to the clients. I take it you haven't tried the solution with BitmapFactory.decodeStream() yourself, right!? Here's what I did: "compiled" the images with abrc and sent the compiled images to the client. I used BitmapFactory.decodeStream() to decode the compiled bitmaps, but nevertheless the bitmap.getNinePatchChunk() returns null. I tried it both on the emulator and on several targets (various API levels). Seems like BitmapFactory can't decode ninepatch images properly ... =(Broca
Hmm. I was able to use BitmapFactory.decodeStream() successfully on a compiled ninepatch. Also, it successfully returned the chunk for me. Having said that, I don't know why I wrote my answer with that line about whether or not decodeStream knows about the chunk-- of course it does, otherwise my own attempts wouldn't have been working. But it does worry me that it isn't working for you. Can you confirm that your compiled ninepatches are indeed compiled? You can use diff to check if the before/afters are the same.Rigi
You're right! It's indeed possible to show "compiled" 9-patches by decoding them with BitmapFactory.decodeStream()! I got it working by setting up a small sample project that only retrieves a "compiled" 9-patch image over the network and displays it in the UI. It must be something else that messes up my original app. I use an image (disk) cache that could be the problem ... I'll have a look at it now! As soon as I found the cause, I'll let you know! Thanks, kanzure!Broca
The reason why I couldn't get the compiled 9-patch images to be displayed correctly was that when I store the retrieved images to disk (as part of the cache functionality), the PNG images loose their 9-patch chunk (npTc) info. I use Bitmap.compress(), and it doesn't seem to handle encoding of compiled 9-patch PNGs properly. I conclude this since the persisted PNG files don't contain the npTc chunk ... =(Broca
Uh, well, off the top of my head you should just store the chunk bytes in another file and read them back later. You could also just store the images directly through an InputStream straight to file.Rigi
Hi kanzure, I am having the same issue too. for me the returned chunk is always null, even if the served image is a 9-patch (the same image in resource folder works fine). You're saying in your comment that we're giving a source bitmap instead of compiled one. How do we get the compiled image?Whiny
You would use the "Android Binary Resource Compiler" tool to make the 9patches. If that doesn't exist anymore, then you would make a simple Android project, put the image in the app's res/ folder somewhere (I forget where it goes), then compile the app, unzip the apk file, and take out the modified version of the file. That will be your compiled version of the image.Rigi
The Android Binary Resource Compiler is overkill for this. You just need to invoke AndroidSDK\build-tools\${version}\aapt s -i ${img}.png -o ${img}.9.pngCorell
L
25

If you need to create 9Patches on the fly check out this gist I made: https://gist.github.com/4391807

You pass it any bitmap and then give it cap insets similar to iOS.

Lanchow answered 27/12, 2012 at 20:49 Comment(6)
This is exactly the sort of thing I wanted to know if it was possible so thank you for sharing your code snippet.Willpower
This is perfect for me as I'm trying to exactly duplicate the iOS function for cross-platform bitmaps, thank you so much!Tetracycline
When using the provided code, I had problems defining what the "left/right/.." should be. I tried the obvious, using values of UIEdgeInsets, but it didn't work. So as a last resort I tried to use these values: left, width-right, top, height - bottom. Unfortunately though again it didn't work - only the upper left and upper part it worked. SInce indeed this is the structure, any idea how to properly use it? Should the Bitmap should be in special form maybe?Imitate
@Imitate Decode your bitmap first (using BitmapFactory.Options.inJustDecodeBounds = true) and get the width/height of your bitmap. Then pass the params as createNinePathWithCapInsets(res, bm, top, left, width - right, height - bottom); using exactly the same iOS values.Tetracycline
This: https://mcmap.net/q/392493/-building-a-9-patch-drawable-at-runtime .. is more reliable for me. (The linked gist fails about 1/10 times, created a nine patch with an unexpected black looking center patch.)Melaniamelanic
Thanks, this works great. Finallly after some many years i could figure it out. Thank you so much. I will post my implementation as an answer hereKura
P
7

I create a tool to create NinePatchDrawable from (uncompiled) NinePatch bitmap. See https://gist.github.com/knight9999/86bec38071a9e0a781ee .

The method NinePatchDrawable createNinePatchDrawable(Resources res, Bitmap bitmap) helps you.

For example,

    ImageView imageView = (ImageView) findViewById(R.id.imageview);
    Bitmap bitmap = loadBitmapAsset("my_nine_patch_image.9.png", this);
    NinePatchDrawable drawable = NinePatchBitmapFactory.createNinePatchDrawable(getResources(), bitmap);
    imageView.setBackground( drawable );

where

public static final Bitmap loadBitmapAsset(String fileName,Context context) {
    final AssetManager assetManager = context.getAssets();
    BufferedInputStream bis = null;
    try {
        bis = new BufferedInputStream(assetManager.open(fileName));
        return BitmapFactory.decodeStream(bis);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            bis.close();
        } catch (Exception e) {

        }
    }
    return null;
}

In this sample case, the my_nine_patch_image.9.png is under the assets directory.

Paternoster answered 23/4, 2015 at 13:0 Comment(6)
Thanks for sharing this aspiring attack.However,this approach will still screw NinePatch bitmap background comparing set background by using xml.Sloop
Your NinePatchBitmapFactory is exactly what I need for my problem: #35204489. Seems to work fine. Is it free for use?Hallow
@Sloop Yes, the real NinePatch bitmap is more complicated.Paternoster
@Hallow My code is free. But my code is a forked version. I don't know the original code is free or not. (May be free I think) Please visit briangriffey's original repository. gist.github.com/briangriffey/4391807Paternoster
@Sloop If you have an image file which brings some issue in my NinePatch emulation code, please tell me its url. I or someone may improve my NInePatch codes.Paternoster
@Paternoster Your NinePatch code is just fine.I'm sorry that I have not described my issue clearly.I want to set nine patch drawable as background using code to reduce memory consumption,which has something to do with BitmapFactory.Options.So I think the BitmapFactory.decodeStream makes the screw not your NinePatch code.Sloop
W
6

No need to use Android Binary Resource Compiler to prepare compiled 9patch pngs, just using aapt in android-sdk is ok, the command line is like this:
aapt.exe c -v -S /path/to/project -C /path/to/destination

Warmhearted answered 3/6, 2013 at 5:56 Comment(1)
This should really be the accepted answer. The 9 Patch drawables have their 9 patch chunks added by aapt (using Eclipse typically) when the APK is built. The 9 patch PNG produced by draw9patch doesn't include the chunk in it and does literally add black lines to PNG's.Yeast
S
4

So you basically want to create a NinePatchDrawable on demand, don't you? I tried the following code, maybe it works for you:

InputStream in = getResources().openRawResource(R.raw.test);
Drawable d = NinePatchDrawable.createFromStream(in, null);
System.out.println(d.getMinimumHeight() + ":" + d.getMinimumHeight());

I think this should work. You just have to change the first line to get the InputStream from the web. getNinePatchChunk() is not intended to be called from developers according to the documentation, and might break in the future.

Simplistic answered 22/2, 2011 at 15:59 Comment(9)
Thats correct my mistake I just looked at the method overview and they left that out.Furey
Yes, I want to create a NinePatchDrawable. Have you actually used the code above to create a NinePatchDrawable on a target device? I tried it out, and Drawable.createFromStream(InputStream, String) returns a BitmapDrawable (extends Drawable) and not a NinePatchDrawable (extends Drawable). I've tried it with an input stream to the remote image (over HTTP) and with an input stream to a local image file. In both cases a BitmapDrawable is returned. I've have tested this on a Samsung Galaxy S and a HTC Desire (both running 2.2). Any other suggestions!? Any help is appreciated!Broca
You can find one of the nine-patch images that I'm trying to load here. I'm pretty sure it's a valid nine-patch, since it works fine if I load it from /res/drawable/ ...Broca
OK, I made a small mistake here - the method createFromStream is inherited from the Drawable class, so this does not work. I also tried to decode the Bitmap using other methods, the ninepatch chunk is not decoded (getNinePatchChunk() returns null). I think you might want to give this a try: Decode a regular Bitmap, define the ninepatch chunk array manually and call the NinePatch constructor directly: developer.android.com/reference/android/graphics/… You might be able to learn from regular NinePatches.Simplistic
This was my initial approach, but I can't find a definition of the chunk format anywhere. I loaded the (linked) nine-patch from /res/drawable/ and had a look at the nine-patch chunk: [1, 2, 2, 6, 64, -128, -117, -66, 72, -128, -117, -66, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 80, -128, -117, -66, 6, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 63, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]. Based on this output, at least I can't see any logic on how to build up the chunk dynamically. Anybody knows how to create the chunk manually (from a nine-patch bitmap)?Broca
A colleague of mine pointed me to the AAPT src code, but should I really have to port this code to be able to create a NinePatchDrawable (from an image retrieved over the network)!? That's seems a bit overkill to me ...Broca
There is a bug filed for loading a NinePatchDrawable using the Drawable.createFrom... methods. See this SO-thread. So that approach will probably not work ... So the only option remaining is to find out how to format the chunk, and create the `NinePatch' using the constructor.Broca
You might want to have a look at the native file ResourceTypes.h - there is some documentation about the PNG Extensions and the struct Res_png_9patch: android.git.kernel.org/?p=platform/frameworks/…Simplistic
There is also an implementation of the serialize and deserialize methods here: android.git.kernel.org/?p=platform/frameworks/… Hope this helps.Simplistic
K
4

WORKING AND TESTED - RUNTIME NINEPATCH CREATION

This is my implementation of android Ninepatch Builder, you can create NinePatches on Runtime through this class and code examples below by supplying any Bitmap

public class NinePatchBuilder {
    int width,height;
    Bitmap bitmap;
    Resources resources;
    private ArrayList<Integer> xRegions=new ArrayList<Integer>();
    private ArrayList<Integer> yRegions=new ArrayList<Integer>();
    public NinePatchBuilder(Resources resources,Bitmap bitmap){
        width=bitmap.getWidth();
        height=bitmap.getHeight();
        this.bitmap=bitmap;
        this.resources=resources;
    }
    public NinePatchBuilder(int width, int height){
        this.width=width;
        this.height=height;
    }
    public NinePatchBuilder addXRegion(int x, int width){
        xRegions.add(x);
        xRegions.add(x+width);
        return this;
    }
    public NinePatchBuilder addXRegionPoints(int x1, int x2){
        xRegions.add(x1);
        xRegions.add(x2);
        return this;
    }
    public NinePatchBuilder addXRegion(float xPercent, float widthPercent){
        int xtmp=(int)(xPercent*this.width);
        xRegions.add(xtmp);
        xRegions.add(xtmp+(int)(widthPercent*this.width));
        return this;
    }
    public NinePatchBuilder addXRegionPoints(float x1Percent, float x2Percent){
        xRegions.add((int)(x1Percent*this.width));
        xRegions.add((int)(x2Percent*this.width));
        return this;
    }
    public NinePatchBuilder addXCenteredRegion(int width){
        int x=(int)((this.width-width)/2);
        xRegions.add(x);
        xRegions.add(x+width);
        return this;
    }
    public NinePatchBuilder addXCenteredRegion(float widthPercent){
        int width=(int)(widthPercent*this.width);
        int x=(int)((this.width-width)/2);
        xRegions.add(x);
        xRegions.add(x+width);
        return this;
    }
    public NinePatchBuilder addYRegion(int y, int height){
        yRegions.add(y);
        yRegions.add(y+height);
        return this;
    }
    public NinePatchBuilder addYRegionPoints(int y1, int y2){
        yRegions.add(y1);
        yRegions.add(y2);
        return this;
    }
    public NinePatchBuilder addYRegion(float yPercent, float heightPercent){
        int ytmp=(int)(yPercent*this.height);
        yRegions.add(ytmp);
        yRegions.add(ytmp+(int)(heightPercent*this.height));
        return this;
    }
    public NinePatchBuilder addYRegionPoints(float y1Percent, float y2Percent){
        yRegions.add((int)(y1Percent*this.height));
        yRegions.add((int)(y2Percent*this.height));
        return this;
    }
    public NinePatchBuilder addYCenteredRegion(int height){
        int y=(int)((this.height-height)/2);
        yRegions.add(y);
        yRegions.add(y+height);
        return this;
    }
    public NinePatchBuilder addYCenteredRegion(float heightPercent){
        int height=(int)(heightPercent*this.height);
        int y=(int)((this.height-height)/2);
        yRegions.add(y);
        yRegions.add(y+height);
        return this;
    }
    public byte[] buildChunk(){
        if(xRegions.size()==0){
            xRegions.add(0);
            xRegions.add(width);
        }
        if(yRegions.size()==0){
            yRegions.add(0);
            yRegions.add(height);
        }
        /* example code from a anwser above
        // The 9 patch segment is not a solid color.
        private static final int NO_COLOR = 0x00000001;
        ByteBuffer buffer = ByteBuffer.allocate(56).order(ByteOrder.nativeOrder());
        //was translated
        buffer.put((byte)0x01);
        //divx size
        buffer.put((byte)0x02);
        //divy size
        buffer.put((byte)0x02);
        //color size
        buffer.put(( byte)0x02);

        //skip
        buffer.putInt(0);
        buffer.putInt(0);

        //padding
        buffer.putInt(0);
        buffer.putInt(0);
        buffer.putInt(0);
        buffer.putInt(0);

        //skip 4 bytes
        buffer.putInt(0);

        buffer.putInt(left);
        buffer.putInt(right);
        buffer.putInt(top);
        buffer.putInt(bottom);
        buffer.putInt(NO_COLOR);
        buffer.putInt(NO_COLOR);

        return buffer;*/
        int NO_COLOR = 1;//0x00000001;
        int COLOR_SIZE=9;//could change, may be 2 or 6 or 15 - but has no effect on output 
        int arraySize=1+2+4+1+xRegions.size()+yRegions.size()+COLOR_SIZE;
        ByteBuffer byteBuffer=ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder());
        byteBuffer.put((byte) 1);//was translated
        byteBuffer.put((byte) xRegions.size());//divisions x
        byteBuffer.put((byte) yRegions.size());//divisions y
        byteBuffer.put((byte) COLOR_SIZE);//color size

        //skip
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);

        //padding -- always 0 -- left right top bottom
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);
        byteBuffer.putInt(0);

        //skip
        byteBuffer.putInt(0);

        for(int rx:xRegions)
            byteBuffer.putInt(rx); // regions left right left right ...
        for(int ry:yRegions)
            byteBuffer.putInt(ry);// regions top bottom top bottom ...

        for(int i=0;i<COLOR_SIZE;i++)
            byteBuffer.putInt(NO_COLOR);

        return byteBuffer.array();
    }
    public NinePatch buildNinePatch(){
        byte[] chunk=buildChunk();
        if(bitmap!=null)
            return new NinePatch(bitmap,chunk,null);
        return null;
    }
    public NinePatchDrawable build(){
        NinePatch ninePatch=buildNinePatch();
        if(ninePatch!=null)
            return new NinePatchDrawable(resources, ninePatch);
        return null;
    }
}

Now we can use ninepatch builder to create NinePatch or NinePatchDrawable or for creating NinePatch Chunk.

Example:

NinePatchBuilder builder=new NinePatchBuilder(getResources(), bitmap);
NinePatchDrawable drawable=builder.addXCenteredRegion(2).addYCenteredRegion(2).build();

//or add multiple patches

NinePatchBuilder builder=new NinePatchBuilder(getResources(), bitmap);
builder.addXRegion(30,2).addXRegion(50,1).addYRegion(20,4);
byte[] chunk=builder.buildChunk();
NinePatch ninepatch=builder.buildNinePatch();
NinePatchDrawable drawable=builder.build();

//Here if you don't want ninepatch and only want chunk use
NinePatchBuilder builder=new NinePatchBuilder(width, height);
byte[] chunk=builder.addXCenteredRegion(1).addYCenteredRegion(1).buildChunk();

Just copy paste the NinePatchBuilder class code in a java file and use the examples to create NinePatch on the fly during your app runtime, with any resolution.

Kura answered 13/6, 2016 at 9:2 Comment(0)
F
0

The Bitmap class provides a method to do this yourbitmap.getNinePatchChunk(). I've never used it but it seems like thats what your looking for.

Furey answered 22/2, 2011 at 15:46 Comment(1)
As mreichelt said, the doc says "Returns an optional array of private data, used by the UI system for some bitmaps. Not intended to be called by applications.". So that method can't be used. Thanks anyway!Broca

© 2022 - 2024 — McMap. All rights reserved.