NinePatchDrawable does not get padding from chunk
Asked Answered
S

4

9

I need help with NinePatchDrawable:

My app can download themes from the network. Almost all things work fine, except 9-Patch PNGs.

final Bitmap bubble = getFromTheme("bubble");
if (bubble == null) return null;

final byte[] chunk = bubble.getNinePatchChunk();
if (!NinePatch.isNinePatchChunk(chunk)) return null;

NinePatchDrawable d = new NinePatchDrawable(getResources(), bubble, chunk, new Rect(), null);
v.setBackgroundDrawable(d);

d = null;
System.gc();

getFromTheme() loads the Bitmap from the SD card. The 9-Patch PNGs are already compiled, that means they include the required chunk.

The way how I convert the Bitmap to a NinePatchDrawable object seems to be working, because the image is stretchable as well as I drew it.

The only thing that doesn't work is the padding. I already tried to set the padding to the view like this:

final Rect rect = new Rect();   // or just use the new Rect() set
d.getPadding(rect);             // in the constructor
v.setPadding(rect.left, rect.top, rect.right, rect.bottom);

d.getPadding(rect) should fill the variable rect with the padding got from the chunk, shouldn't it? But it doesn't.

Result: The TextView (v) does not show the text in the content area of the 9-Patch image. The paddings are set to 0 in each coordinate.

Thanks for reading.

Sickle answered 16/6, 2012 at 18:27 Comment(0)
S
17

Finally, I did it. Android wasn't interpreting the chunk data correctly. There might be bug. So you have to deserialize the chunk yourself to get the padding data.

Here we go:

package com.dragonwork.example;

import android.graphics.Rect;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

class NinePatchChunk {

    public static final int NO_COLOR = 0x00000001;
    public static final int TRANSPARENT_COLOR = 0x00000000;

    public final Rect mPaddings = new Rect();

    public int mDivX[];
    public int mDivY[];
    public int mColor[];

    private static void readIntArray(final int[] data, final ByteBuffer buffer) {
        for (int i = 0, n = data.length; i < n; ++i)
            data[i] = buffer.getInt();
    }

    private static void checkDivCount(final int length) {
        if (length == 0 || (length & 0x01) != 0)
            throw new RuntimeException("invalid nine-patch: " + length);
    }

    public static NinePatchChunk deserialize(final byte[] data) {
        final ByteBuffer byteBuffer =
            ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());

        if (byteBuffer.get() == 0) return null; // is not serialized

        final NinePatchChunk chunk = new NinePatchChunk();
        chunk.mDivX = new int[byteBuffer.get()];
        chunk.mDivY = new int[byteBuffer.get()];
        chunk.mColor = new int[byteBuffer.get()];

        checkDivCount(chunk.mDivX.length);
        checkDivCount(chunk.mDivY.length);

        // skip 8 bytes
        byteBuffer.getInt();
        byteBuffer.getInt();

        chunk.mPaddings.left = byteBuffer.getInt();
        chunk.mPaddings.right = byteBuffer.getInt();
        chunk.mPaddings.top = byteBuffer.getInt();
        chunk.mPaddings.bottom = byteBuffer.getInt();

        // skip 4 bytes
        byteBuffer.getInt();

        readIntArray(chunk.mDivX, byteBuffer);
        readIntArray(chunk.mDivY, byteBuffer);
        readIntArray(chunk.mColor, byteBuffer);

        return chunk;
    }
}

Use the class above as following:

final byte[] chunk = bitmap.getNinePatchChunk();
if (NinePatch.isNinePatchChunk(chunk)) {
    textView.setBackgroundDrawable(new NinePatchDrawable(getResources(),
          bitmap, chunk, NinePatchChunk.deserialize(chunk).mPaddings, null));
}

And it will work perfectly!

Sickle answered 18/6, 2012 at 16:23 Comment(3)
Raise an issue on b.android.com, also mention the device used and the Android version.Caprification
Sorry, I was so excited to see the answer, I forgot to upvote. It's also perfectly acceptable to accept your own answer :)Gothar
I would have never thought the bug was in API's. Thanks!Tibbs
B
4

It's actually slightly more complicated than that, but what it boils down to is pretty simple:

The padding rect is returned by BitmapFactory.decodeStream(InputStream, Rect, Options). There is no version of decodeByteArray() which can return the padding rect.

The whole nine-patch API is a bit silly:

  • decodeByteArray() calls nativeDecodeByteArray(), which is presumably more efficient than nativeDecodeStream() on a ByteArrayInputStream, but obviously the devs never expected you to want to decode a nine-patch from memory.
  • The padding rect is only used by nine-patches, so it makes more sense for it to be part of NinePatch instead of BitmapFactory. Sadly, NinePatch.java is not much more than a wrapper that passes the bitmap and nine-patch chunk to drawing methods (and most of the NinePatch.draw() calls aren't thread-safe due to the call to mRect.set(location)).
  • NinePatchDrawable doesn't offer a way to take a NinePatch and a padding rect, which makes NinePatch somewhat useless in application code (unless you want to do the padding yourself). There is no NinePatchDrawable.getNinePatch() or NinePatch.getBitmap().

This comment sums it up pretty well:

ugh. The decodeStream contract is that we have already allocated the pad rect, but if the bitmap does not had a ninepatch chunk, then the pad will be ignored. If we could change this to lazily alloc/assign the rect, we could avoid the GC churn of making new Rects only to drop them on the floor.

My fix is fairly simple:

public final class NinePatchWrapper {
  private final Bitmap mBitmap;
  private final Rect mPadding;
  /**
  * The caller must ensure that that bitmap and padding are not modified after
  * this method returns. We could copy them, but Bitmap.createBitmap(Bitmap)
  * does not copy the nine-patch chunk on some Android versions.
  */
  public NinePatchWrapper(Bitmap bitmap, Rect padding) {
    mBitmap = bitmap;
    mPadding = padding;
  }
  public NinePatchDrawable newDrawable(Resources resources) {
    return new NinePatchDrawable(mBitmap, mBitmap.getNinePatchChunk(), mPadding, null);
  }
}

...

public NinePatchWrapper decodeNinePatch(byte[] byteArray, int density) {
  Rect padding = new Rect();
  ByteArrayInputStream stream = new ByteArrayInputStream(byteArray);
  Bitmap bitmap = BitmapFactory.decodeStream(stream, padding, null);
  bitmap.setDensity(density);
  return new NinePatchWrapper(bitmap, padding);
}

Untested, since it's greatly simplified. In particular, you might want to check that the nine-patch chunk is valid.

Beggary answered 5/4, 2013 at 18:24 Comment(0)
P
1

I've never seen an example where the Padding isn't included as part of the 9-patch like so:

enter image description here

To do this you should first construct a NinePatch and then create you're Drawable from it:

NinePatch ninePatch = new NinePatch(bitmap, chunk, srcName);
NinePatchDrawable d = new NinePatchDrawable(res, ninePatch);

However, you seem to be constructing your Drawable with an empty rectangle:

NinePatchDrawable d = new NinePatchDrawable(getResources(), bubble, chunk, new Rect(), null);

If you want to programatically specify the padding try this:

Rect paddingRectangle = new Rect(left, top, right, bottom);
NinePatchDrawable d = new NinePatchDrawable(getResources(), bubble, chunk, paddingRectangle, null);
Pronuba answered 18/6, 2012 at 14:34 Comment(6)
But my question is: How to get the variables left, top, right and bottom from the image itself?Sickle
The first example show's you how to correctly populate a NinePatchDrawable to include the padding in the chunk. Once this has been constructed you can do getPadding(rect)Pronuba
The first example should do it. Construct the NinePatch, then the NinePatchDrawable from that NinePatch. getPadding() should then return the correct values.Katharinekatharsis
can you post both the 9 patch and the xml for the View?Pronuba
There is no xml for the view. It's a programmatically created TextView, thats appended to a Table as a TableRow.Sickle
I can only suggest then that you're chunk data at that point is missing. If you can compare the chunk data from the file with the chunk data at that point (perhaps a get bubble.getNinePatchChunk();)Pronuba
E
1

A bit late to the party, but here is how I solved it:

I use the decoder method that NinePatchDrawable provides, it reads the padding correctly:

     var myDrawable = NinePatchDrawable.createFromStream(sr, null);
Expertize answered 22/10, 2015 at 14:21 Comment(1)
This is the easiest way to do this, I tried it ,and it worked~Mozart

© 2022 - 2024 — McMap. All rights reserved.