【问题标题】:Create a NinePatch/NinePatchDrawable in runtime在运行时创建 NinePatch/NinePatchDrawable
【发布时间】:2011-02-22 15:07:04
【问题描述】:

我对我的 Android 应用程序有一个要求,即图形上的部分应该是可定制的,方法是从服务器端检索新的颜色和图像。其中一些图像是九块图像。

我找不到创建和显示这些九个补丁图像(已通过网络检索到)的方法。

九个补丁图像被检索并作为位图保存在应用程序中。为了创建NinePatchDrawable,您需要相应的NinePatch 或NinePatch 的块(byte[])。 NinePatch 不能从资源中加载,因为图像在/res/drawable/ 中不存在。此外,为了创建 NinePatch,您需要 NinePatch 的块。所以,这一切都深入到块。
那么问题来了,如何从现有的位图(包含 NinePatch 信息)中格式化/生成块?

我搜索了 Android 源代码和网络,但似乎找不到任何示例。更糟糕的是,所有对 NinePatch 资源的解码似乎都是在本地完成的。

有人遇到过此类问题吗?

如果这很重要,我的目标是 API 级别 4。

【问题讨论】:

    标签: android networking runtime nine-patch


    【解决方案1】:

    getNinePatchChunk 工作得很好。它返回 null 是因为您给 Bitmap 一个“源”九补丁。它需要一个“已编译”的 Ninepatch 图像。

    Android 世界中有两种类型的 Ninepatch 文件格式(“源”和“编译”)。源版本是您在各处添加 1px 透明边框的地方——稍后当您将应用程序编译为 .apk 时,aapt 会将您的 *.9.png 文件转换为 Android 期望的二进制格式。这是 png 文件获取其“块”元数据的地方。 (read more)

    好的,现在开始(你正在听 DJ kanzure)。

    1. 客户端代码,类似这样:

      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. 服务器端,您需要准备图像。您可以使用Android Binary Resource Compiler。这将一些痛苦从创建一个新的 Android 项目中解放出来,只是为了将一些 *.9.png 文件编译成 Android 原生格式。如果您要手动执行此操作,您实际上将创建一个项目并放入一些 *.9.png 文件(“源”文件),将所有内容编译为 .apk 格式,解压缩 .apk 文件,然后找到 *. 9.png 文件,这是您发送给客户的文件。

    另外:我不知道BitmapFactory.decodeStream 是否知道这些 png 文件中的 npTc 块,因此它可能会或可能不会正确处理图像流。 Bitmap.getNinePatchChunk 的存在表明 BitmapFactory 可能——你可以在上游代码库中查找它。

    如果它不知道 npTc 块并且您的图像被严重搞砸了,那么我的答案会有所改变。

    您无需将编译后的 Ninepatch 图像发送到客户端,而是编写一个快速的 Android 应用程序来加载编译后的图像并输出 byte[] 块。然后,您将此字节数组与常规图像一起传输给您的客户端——没有透明边框,不是“源”九补丁图像,不是“编译”九补丁图像。您可以直接使用块来创建您的对象。

    另一种方法是使用对象序列化将九个补丁图像 (NinePatch) 发送到您的客户端,例如使用 JSON 或内置序列化程序。

    编辑如果你真的,真的需要构建你自己的块字节数组,我会先查看 ResourceTypes.cpp 中的 do_9patchisNinePatchChunkRes_png_9patchRes_png_9patch::serialize() .还有一个来自 Dmitry Skiba 的自制 npTc 块阅读器。我不能发布链接,所以如果有人可以编辑我的答案会很酷。

    do_9补丁: 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

    德米特里斯基巴的东西:http://code.google.com/p/android4me/source/browse/src/android/graphics/Bitmap.java

    【讨论】:

    • 感谢 kanzure 的精彩回答!我不敢相信我没有想到将编译后的图像发送给客户。我认为您自己没有尝试过BitmapFactory.decodeStream() 的解决方案,对吧!?这就是我所做的:用 abrc“编译”图像并将编译后的图像发送到客户端。我使用BitmapFactory.decodeStream() 来解码编译的位图,但bitmap.getNinePatchChunk() 返回null。我在模拟器和几个目标(各种 API 级别)上都试过了。好像BitmapFactory 无法正确解码ninepatch 图像... =(
    • 嗯。我能够在编译的 Ninepatch 上成功使用BitmapFactory.decodeStream()。此外,它成功地为我返回了块。话虽如此,我不知道为什么我用那一行写了我的答案,关于decodeStream 是否知道该块——当然知道,否则我自己的尝试不会奏效。但它确实让我担心它不适合你。你能确认你编译的九个补丁确实被编译了吗?您可以使用diff 来检查前后是否相同。
    • 你是对的!确实可以通过使用BitmapFactory.decodeStream() 解码来显示“已编译”的 9 补丁!我通过设置一个仅通过网络检索“已编译”的 9-patch 图像并将其显示在 UI 中的小型示例项目来使其工作。一定是其他东西弄乱了我原来的应用程序。我使用可能是问题的图像(磁盘)缓存......我现在就看看!等我找到原因,我会告诉你的!谢谢,kanzure!
    • 无法正确显示已编译的 9-patch 图像的原因是,当我将检索到的图像存储到磁盘(作为缓存功能的一部分)时,PNG 图像会丢失它们的9 补丁块 (npTc) 信息。我使用Bitmap.compress(),它似乎无法正确处理已编译的 9-patch PNG 的编码。我得出这个结论,因为持久化的 PNG 文件不包含 npTc 块 ... =(
    • 呃,好吧,你应该把块字节存储在另一个文件中,然后再读回来。您也可以直接通过 InputStream 将图像直接存储到文件中。
    【解决方案2】:

    如果您需要即时创建 9Patches,请查看我制作的以下要点:https://gist.github.com/4391807

    你将任何位图传递给它,然后给它类似于 iOS 的 cap insets。

    【讨论】:

    • 这正是我想知道的事情,所以感谢您分享您的代码 sn-p。
    • 这对我来说是完美的,因为我正在尝试完全复制跨平台位图的 iOS 功能,非常感谢!
    • 使用提供的代码时,我在定义“左/右/..”应该是什么时遇到了问题。我尝试了明显的,使用 UIEdgeInsets 的值,但它没有用。因此,作为最后的手段,我尝试使用这些值:左、右宽、上、高-下。不幸的是,虽然它再次不起作用 - 只有左上角和上部它起作用了。既然这确实是结构,知道如何正确使用它吗?位图应该是特殊形式吗?
    • @Panayotis 首先解码您的位图(使用 BitmapFactory.Options.inJustDecodeBounds = true)并获取位图的宽度/高度。然后使用完全相同的 iOS 值将参数作为createNinePathWithCapInsets(res, bm, top, left, width - right, height - bottom); 传递。
    • 这个:stackoverflow.com/a/16676419/580677 .. 对我来说更可靠。 (链接的要点大约失败了 1/10 次,创建了一个带有意想不到的黑色中心补丁的九个补丁。)
    【解决方案3】:

    我创建了一个工具来从(未编译的)NinePatch 位图创建 NinePatchDrawable。 见https://gist.github.com/knight9999/86bec38071a9e0a781ee

    方法 NinePatchDrawable createNinePatchDrawable(Resources res, Bitmap bitmap) 帮助你。

    例如,

        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 );
    

    在哪里

    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;
    }
    

    在此示例中,my_nine_patch_image.9.png 位于资产目录下。

    【讨论】:

    • 感谢分享这个有抱负的攻击。但是,这种方法仍然会破坏 NinePatch 位图背景使用 xml 比较设置背景。
    • 您的 NinePatchBitmapFactory 正是我的问题所需要的:stackoverflow.com/questions/35203989/…。似乎工作正常。可以免费使用吗?
    • @zionpi 是的,真正的 NinePatch 位图更复杂。
    • @user2331454 我的代码是免费的。但我的代码是一个分叉版本。我不知道原始代码是否免费。 (我认为可能是免费的)请访问 briangriffey 的原始存储库。 gist.github.com/briangriffey/4391807
    • @KNaito 你的 NinePatch 代码很好。很抱歉我没有清楚地描述我的问题。我想使用代码设置九个可绘制的补丁作为背景以减少内存消耗,which 有与BitmapFactory.Options有关。所以我认为BitmapFactory.decodeStream使螺丝不是你的NinePatch代码。
    【解决方案4】:

    无需使用Android Binary Resource Compiler准备编译的9patch pngs,在android-sdk中使用aapt即可,命令行如下:
    aapt.exe c -v -S /path/to/project -C /path/to/destination

    【讨论】:

    • 这确实应该是公认的答案。构建 APK 时,9 个 Patch 可绘制对象具有由 aapt(通常使用 Eclipse)添加的 9 个补丁块。 draw9patch 生成的 9 补丁 PNG 不包含其中的块,并且确实在 PNG 中添加了黑线。
    【解决方案5】:

    所以你基本上想根据需要创建一个NinePatchDrawable,不是吗?我尝试了以下代码,也许它对你有用:

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

    我认为这应该可行。您只需更改第一行即可从网络获取 InputStream。根据文档,getNinePatchChunk() 不打算从开发人员那里调用,并且将来可能会中断。

    【讨论】:

    • 那是我的错误,我只是查看了方法概述,他们忽略了。
    • 感谢您的回复,mreichelt!
    • 是的,我想创建一个NinePatchDrawable。您是否真的使用上面的代码在目标设备上创建了NinePatchDrawable?我试了一下,Drawable.createFromStream(InputStream, String) 返回一个BitmapDrawable(扩展Drawable)而不是NinePatchDrawable(扩展Drawable)。我已经尝试使用远程图像的输入流(通过 HTTP)和本地图像文件的输入流。在这两种情况下都会返回 BitmapDrawable。我已经在三星 Galaxy S 和 HTC Desire(均运行 2.2)上对此进行了测试。还有其他建议吗!?任何帮助表示赞赏!
    • 我的一位同事将我指向AAPT src code,但我是否真的必须移植此代码才能创建NinePatchDrawable(从通过网络检索的图像)!?这对我来说似乎有点矫枉过正......
    • 有一个bug 用于使用Drawable.createFrom... 方法加载NinePatchDrawable。看到这个SO-thread。所以这种方法可能行不通......所以剩下的唯一选择是找出如何格式化块,并使用构造函数创建“NinePatch”。
    【解决方案6】:

    工作和测试 - 运行时 NINEPATCH 创建

    这是我实现的android Ninepatch Builder,你可以通过这个类和下面的代码示例在运行时创建NinePatches,通过提供任何位图

    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;
        }
    }
    

    现在我们可以使用 Ninepatch builder 来创建 NinePatch 或 NinePatchDrawable 或用于创建 NinePatch Chunk。

    例子:

    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();
    

    只需将 NinePatchBuilder 类代码复制粘贴到 java 文件中,然后使用示例在应用运行时动态创建 NinePatch,分辨率不限。

    【讨论】:

      【解决方案7】:

      Bitmap 类提供了一个方法来做到这一点yourbitmap.getNinePatchChunk()。我从未使用过它,但它似乎就是您要找的东西。

      【讨论】:

      • 正如 mreichelt 所说,文档说 “返回一个可选的私有数据数组,供 UI 系统用于某些位图。不打算由应用程序调用。”。所以这个方法不能用。还是谢谢!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-13
      • 1970-01-01
      • 2012-01-29
      • 2011-05-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多