点九图简介
Android为了使用同一张图作为不同数量文字的背景,设计了一种可以指定区域拉伸的图片格式“.9.png”,这种图片格式就是点九图。
注意:这种图片格式只能被使用于Android开发。在ios开发中,可以在代码中指定某个点进行拉伸,而在Android中不行,所以在Android中想要达到这个效果,只能使用点九图(下文会啪啪打脸,其实是可以的,只是很少人这样使用,兼容性不知道怎么样,点击跳转)
点九图实质
点九图的本质实际上是在图片的四周各增加了1px的像素,并使用纯黑(#FF000000)的线进行标记,其它的与原图没有任何区别。可以参考以下图片:
点九图在 Android 中的应用
点九图在 Android 中主要有三种应用方式
直接放在 res 目录中的 drawable 或者 mipmap 目录中
放在 assert 目录中
从网络下载
第一种方式是我们最常用的,直接调用setBackgroundResource
或者setImageResource
方法,这样的话图片及可以做到自动拉伸。
而对于第二种或者第三种方式,如果我们直接去加载 .9.png,你会发现图片或者图片背景根本无法拉伸。纳尼,这是为甚么呢。下面,且听老衲慢慢道来。
Android 并不是直接使用点九图,而是在编译时将其转换为另外一种格式,这种格式是将其四周的黑色像素保存至Bitmap类中的一个名为mNinePatchChunk
的 byte[] 中,并抹除掉四周的这一个像素的宽度;接着在使用时,如果 Bitmap 的这个mNinePatchChunk
不为空,且为 9patch chunk,则将其构造为NinePatchDrawable
,否则将会被构造为 BitmapDrawable,最终设置给 view。
因此,在 Android 中,我们如果想动态使用网络下载的点九图,一般需要经过以下步骤:
使用 sdk 目录下的 aapt 工具将点九图转化为 png 图片
解析图片的时候,判断是否含有 NinePatchChunk,有的话,转化为 NinePatchDrawable
2if(file.exists()){ 3Bitmapbitmap=BitmapFactory.decodeFile(file.getAbsolutePath()); 4byte[]chunk=bitmap.getNinePatchChunk(); 5if(NinePatch.isNinePatchChunk(chunk)){ 6NinePatchDrawablepatchy=newNinePatchDrawable(view.getResources(),bitmap,chunk,newRect(),null); 7view.setBackground(patchy); 8} 9 10} 11}1publicstaticvoidsetNineImagePatch(Viewview,Filefile,Stringurl){
点九图上传服务器流程
aapt 转换命令
单个图片文件转换
1./aapts-ixxx.9.png-oxxx.png
批量转换
2./aaptc-SinputDir-CoutputDir 3#inputDir为原始.9图文件夹,outputDir为输出文件夹1#批量转换
执行成功实例
2CrunchingPNGFilesinsourcedir:/Users/junxu/Desktop/一期气泡/气泡需求整理 3Todestinationdir:/Users/junxu/Desktop/一期气泡/output1jundeMacBook-Pro:一期气泡junxu$./aaptc-S/Users/junxu/Desktop/一期气泡/气泡需求整理-C/Users/junxu/Desktop/一期气泡/output
注意:
若不是标准的点九图,在转换的过程会报错,这时候请设计重新提供新的点九图
实际开发当中遇到的问题
小屏手机适配问题
刚开始,我们的切图是按照 2 倍图切的,这样在小屏幕手机上会手机气泡高度过大的问题。
原因分析:
该现象的本质是点九图图片的高度大于单行文本消息的高度。
解决方案一(暂时不可取):
我尝试去压缩点九图,但最终再部分手机上面显示错乱,不知道是不是压缩点九图的方法错了。
解决方案二
对于低分辨率的手机和高分辨的手机分别下发不同的图片 url,我们尝试过得方案是当density < 2
的时候,采用一倍图图片,density >= 2
采用二倍图图片。
解决方案三
可能有人会有这样的疑问呢,为什么要采用一倍图,两倍图的解决方案呢?直接让 UI 设计师给一套图,点九图图片的高度适中不就解决了。是啊,我们也是这样想得,但他们说对于有一些装饰的点九图,如果缩小高度,一些装饰图案他们不太好切。比如下面图片中的星星。
小结
说到底,方案二,方案三其实都是折中的一种方案,如果直接能够做到点九图缩放,那就完美解决了。而 Android 中 res 目录中的 drawable 或者 mipmap 的点九图确实能做到,去看了相关的代码,目前也没有发现什么好的解决方案,如果你有好的解决方案话,欢迎留言交流。
点九图的 padding 在部分手机上面失效
这个是部分 Android 手机的 bug,解决方法见:/questions/11065996/ninepatchdrawable-does-not-get-padding-from-chunk
2 3privatestaticfinalStringTAG="NinePatchChunk"; 4 5publicfinalRectmPaddings=newRect(); 6 7publicintmDivX[]; 8publicintmDivY[]; 9publicintmColor[]; 10 11privatestaticfloatdensity=IMO.getInstance().getResources().getDisplayMetrics().density; 12 13privatestaticvoidreadIntArray(finalint[]data,finalByteBufferbuffer){ 14for(inti=0,n=data.length;i15data[i]=buffer.getInt(); 16} 17 18privatestaticvoidcheckDivCount(finalintlength){ 19if(length==0||(length&0x01)!=0) 20thrownewIllegalStateException("invalidnine-patch:"+length); 21} 22 23publicstaticRectgetPaddingRect(finalbyte[]data){ 24NinePatchChunkdeserialize=deserialize(data); 25if(deserialize==null){ 26returnnewRect(); 27} 28} 29 30publicstaticNinePatchChunkdeserialize(finalbyte[]data){ 31finalByteBufferbyteBuffer= 32ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); 33 34if(byteBuffer.get()==0){ 35returnnull;//isnotserialized 36} 37 38finalNinePatchChunkchunk=newNinePatchChunk(); 39chunk.mDivX=newint[byteBuffer.get()]; 40chunk.mDivY=newint[byteBuffer.get()]; 41chunk.mColor=newint[byteBuffer.get()]; 42 43try{ 44checkDivCount(chunk.mDivX.length); 45checkDivCount(chunk.mDivY.length); 46}catch(Exceptione){ 47returnnull; 48} 49 50 51//skip8bytes 52byteBuffer.getInt(); 53byteBuffer.getInt(); 54 55 56chunk.mPaddings.left=byteBuffer.getInt(); 57chunk.mPaddings.right=byteBuffer.getInt(); 58chunk.mPaddings.top=byteBuffer.getInt(); 59chunk.mPaddings.bottom=byteBuffer.getInt(); 60 61 62//skip4bytes 63byteBuffer.getInt(); 64 65readIntArray(chunk.mDivX,byteBuffer); 66readIntArray(chunk.mDivY,byteBuffer); 67readIntArray(chunk.mColor,byteBuffer); 68 69returnchunk; 70} 71} 72 73NinePatchDrawablepatchy=newNinePatchDrawable(view.getResources(),bitmap,chunk,NinePatchChunk.getPaddingRect(chunk),null); 74view.setBackground(patchy);1publicclassNinePatchChunk{
动态下载点九图会导致聊天气泡闪烁
这里我们采取的方案是预下载(预下载 10 个)
聊天气泡采用内存缓存,磁盘缓存,确保 RecyclerView 快速滑动的时候不会闪烁
理解点九图
以下内容参考腾讯音乐的Android动态布局入门及NinePatchChunk解密
回顾NinePatchDrawable的构造方法第三个参数bitmap.getNinePatchChunk(),作者猜想,aapt命令其实就是在bitmap图片中,加入了NinePatchChunk的信息,那么我们是不是只要能自己构造出这个东西,就可以让任何图片按照我们想要的方式拉升了呢?
可是查了一堆官方文档,似乎并找不到相应的方法来获得这个byte[]类型的chunk参数。
既然无法知道这个chunk如何生成,那么能不能从解析的角度逆向得出这个NinePatchChunk的生成方法呢?
下面就需要从源码入手了。
NinePatchChunk.java
2ByteBufferbyteBuffer= 3ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); 4bytewasSerialized=byteBuffer.get(); 5if(wasSerialized==0)returnnull; 6NinePatchChunkchunk=newNinePatchChunk(); 7chunk.mDivX=newint[byteBuffer.get()]; 8chunk.mDivY=newint[byteBuffer.get()]; 9chunk.mColor=newint[byteBuffer.get()]; 10checkDivCount(chunk.mDivX.length); 11checkDivCount(chunk.mDivY.length); 12//skip8bytes 13byteBuffer.getInt(); 14byteBuffer.getInt(); 15chunk.mPaddings.left=byteBuffer.getInt(); 16chunk.mPaddings.right=byteBuffer.getInt(); 17chunk.mPaddings.top=byteBuffer.getInt(); 18chunk.mPaddings.bottom=byteBuffer.getInt(); 19//skip4bytes 20byteBuffer.getInt(); 21readIntArray(chunk.mDivX,byteBuffer); 22readIntArray(chunk.mDivY,byteBuffer); 23readIntArray(chunk.mColor,byteBuffer); 24returnchunk; 25}1publicstaticNinePatchChunkdeserialize(byte[]data){
其实从这部分解析byte[] chunk的源码,我们已经可以反推出来大概的结构了。如下图,
按照上图中的猜想以及对.9.png的认识,直觉感受到,mDivX,mDivY,mColor这三个数组是最关键的,但是具体是什么,就要继续看源码了。
ResourceTypes.h
2*Thischunkspecifieshowtosplitanimageintosegmentsfor 3*scaling. 4* 5*ThereareJhorizontalandKverticalsegments.Thesesegmentsdivide 6*theimageintoJ*Kregionsasfollows(whereJ=4andK=3): 7* 8*F0S0F1S1 9*+-----+----+------+-------+ 10*S2|0|1|2|3| 11*+-----+----+------+-------+ 12*||||| 13*||||| 14*F2|4|5|6|7| 15*||||| 16*||||| 17*+-----+----+------+-------+ 18*S3|8|9|10|11| 19*+-----+----+------+-------+ 20* 21*Eachhorizontalandverticalsegmentisconsideredtobyeither 22*stretchable(markedbytheSxlabels)orfixed(markedbytheFy 23*labels),inthehorizontalorverticalaxis,respectively.Inthe 24*aboveexample,thefirstishorizontalsegment(F0)isfixed,the 25*nextisstretchableandthentheycontinuetoalternate.Notethat 26*thesegmentlistforeachaxiscanbeginorendwithastretchable 27*orfixedsegment. 28*/1/**
正如源码中,注释的一样,这个NinePatch Chunk把图片从x轴和y轴分成若干个区域,F区域代表了固定,S区域代表了拉伸。mDivX,mDivY描述了所有S区域的位置起始,而mColor描述了,各个Segment的颜色,通常情况下,赋值为源码中定义的NO_COLOR = 0x00000001就行了。就以源码注释中的例子来说,mDivX,mDivY,mColor如下:
2mDivY=[S2.start,S2.end,S3.start,S3.end]; 3mColor=[c[0],c[1],...,c[11]]1mDivX=[S0.start,S0.end,S1.start,S1.end];
对于mColor这个数组,长度等于划分的区域数,是用来描述各个区域的颜色的,而如果我们这个只是描述了一个bitmap的拉伸方式的话,是不需要颜色的,即源码中NO_COLOR = 0x00000001
说了这么多,我们还是通过一个简单例子来说明如何构造一个按中心点拉伸的 NinePatchDrawable 吧,
2int[]xRegions=newint[]{bitmap.getWidth()/2,bitmap.getWidth()/2+1}; 3int[]yRegions=newint[]{bitmap.getWidth()/2,bitmap.getWidth()/2+1}; 4intNO_COLOR=0x00000001; 5intcolorSize=9; 6intbufferSize=xRegions.length*4+yRegions.length*4+colorSize*4+32; 7 8ByteBufferbyteBuffer=ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder()); 9//第一个byte,要不等于0 10byteBuffer.put((byte)1); 11 12//mDivXlength 13byteBuffer.put((byte)2); 14//mDivYlength 15byteBuffer.put((byte)2); 16//mColorslength 17byteBuffer.put((byte)colorSize); 18 19//skip 20byteBuffer.putInt(0); 21byteBuffer.putInt(0); 22 23//padding先设为0 24byteBuffer.putInt(0); 25byteBuffer.putInt(0); 26byteBuffer.putInt(0); 27byteBuffer.putInt(0); 28 29//skip 30byteBuffer.putInt(0); 31 32//mDivX 33byteBuffer.putInt(xRegions[0]); 34byteBuffer.putInt(xRegions[1]); 35 36//mDivY 37byteBuffer.putInt(yRegions[0]); 38byteBuffer.putInt(yRegions[1]); 39 40//mColors 41for(inti=0;i42byteBuffer.putInt(NO_COLOR); 43} 44 45returnbyteBuffer.array();1Bitmapbitmap=BitmapFactory.decodeFile(filepath);
create-a-ninepatch-ninepatchdrawable-in-runtime
在 stackoverflow 上面也找到牛逼的类,可以动态创建点九图,并拉伸图片,啪啪打脸,刚开始说到 android 中无法想 ios 一样动态指定图片拉伸区域。
2intwidth,height; 3Bitmapbitmap; 4Resourcesresources; 5privateArrayListxRegions=newArrayList(); 6privateArrayListyRegions=newArrayList(); 7 8publicNinePatchBuilder(Resourcesresources,Bitmapbitmap){9width=bitmap.getWidth(); 10height=bitmap.getHeight(); 11this.bitmap=bitmap; 12this.resources=resources; 13} 14 15publicNinePatchBuilder(intwidth,intheight){16this.width=width; 17this.height=height; 18} 19 20publicNinePatchBuilderaddXRegion(intx,intwidth){21xRegions.add(x); 22xRegions.add(x+width); 23returnthis; 24} 25 26publicNinePatchBuilderaddXRegionPoints(intx1,intx2){27xRegions.add(x1); 28xRegions.add(x2); 29returnthis; 30} 31 32publicNinePatchBuilderaddXRegion(floatxPercent,floatwidthPercent){33intxtmp=(int)(xPercent*this.width); 34xRegions.add(xtmp); 35xRegions.add(xtmp+(int)(widthPercent*this.width)); 36returnthis; 37} 38 39publicNinePatchBuilderaddXRegionPoints(floatx1Percent,floatx2Percent){40xRegions.add((int)(x1Percent*this.width)); 41xRegions.add((int)(x2Percent*this.width)); 42returnthis; 43} 44 45publicNinePatchBuilderaddXCenteredRegion(intwidth){46intx=(int)((this.width-width)/2); 47xRegions.add(x); 48xRegions.add(x+width); 49returnthis; 50} 51 52publicNinePatchBuilderaddXCenteredRegion(floatwidthPercent){53intwidth=(int)(widthPercent*this.width); 54intx=(int)((this.width-width)/2); 55xRegions.add(x); 56xRegions.add(x+width); 57returnthis; 58} 59 60publicNinePatchBuilderaddYRegion(inty,intheight){61yRegions.add(y); 62yRegions.add(y+height); 63returnthis; 64} 65 66publicNinePatchBuilderaddYRegionPoints(inty1,inty2){67yRegions.add(y1); 68yRegions.add(y2); 69returnthis; 70} 71 72publicNinePatchBuilderaddYRegion(floatyPercent,floatheightPercent){73intytmp=(int)(yPercent*this.height); 74yRegions.add(ytmp); 75yRegions.add(ytmp+(int)(heightPercent*this.height)); 76returnthis; 77} 78 79publicNinePatchBuilderaddYRegionPoints(floaty1Percent,floaty2Percent){80yRegions.add((int)(y1Percent*this.height)); 81yRegions.add((int)(y2Percent*this.height)); 82returnthis; 83} 84 85publicNinePatchBuilderaddYCenteredRegion(intheight){86inty=(int)((this.height-height)/2); 87yRegions.add(y); 88yRegions.add(y+height); 89returnthis; 90} 91 92publicNinePatchBuilderaddYCenteredRegion(floatheightPercent){93intheight=(int)(heightPercent*this.height); 94inty=(int)((this.height-height)/2); 95yRegions.add(y); 96yRegions.add(y+height); 97returnthis; 98} 99100publicbyte[]buildChunk(){101if(xRegions.size()==0){102xRegions.add(0);103xRegions.add(width);104}105if(yRegions.size()==0){106yRegions.add(0);107yRegions.add(height);108}109110intNO_COLOR=1;//0x00000001;111intCOLOR_SIZE=9;//couldchange,maybe2or6or15-buthasnoeffectonoutput112intarraySize=1+2+4+1+xRegions.size()+yRegions.size()+COLOR_SIZE;113ByteBufferbyteBuffer=ByteBuffer.allocate(arraySize*4).order(ByteOrder.nativeOrder());114byteBuffer.put((byte)1);//wastranslated115byteBuffer.put((byte)xRegions.size());//divisionsx116byteBuffer.put((byte)yRegions.size());//divisionsy117byteBuffer.put((byte)COLOR_SIZE);//colorsize118119//skip120byteBuffer.putInt(0);121byteBuffer.putInt(0);122123//padding--always0--leftrighttopbottom124byteBuffer.putInt(0);125byteBuffer.putInt(0);126byteBuffer.putInt(0);127byteBuffer.putInt(0);128129//skip130byteBuffer.putInt(0);131132for(intrx:xRegions)133byteBuffer.putInt(rx);//regionsleftrightleftright...134for(intry:yRegions)135byteBuffer.putInt(ry);//regionstopbottomtopbottom...136137for(inti=0;i138byteBuffer.putInt(NO_COLOR);139140returnbyteBuffer.array();141}142143publicNinePatchbuildNinePatch(){144byte[]chunk=buildChunk();145if(bitmap!=null)146returnnewNinePatch(bitmap,chunk,null);147returnnull;148}149150publicNinePatchDrawablebuild(){151NinePatchninePatch=buildNinePatch();152if(ninePatch!=null)153returnnewNinePatchDrawable(resources,ninePatch);154returnnull;155}156}1publicclassNinePatchBuilder{
运行一下测试代码
2try{ 3InputStreamis=getAssets().open("sea.png"); 4Bitmapbitmap=BitmapFactory.decodeStream(is); 5for(inti=0;i5;i++){ 6NinePatchDrawableninePatchDrawable=NinePatchHelper.buildMulti(this,bitmap); 7TextViewtextView=newTextView(this); 8textView.setTextSize(25); 9textView.setPadding(20,10,20,10); 10textView.setText(strArray[i]); 11textView.setGravity(Gravity.CENTER_VERTICAL); 12LinearLayout.LayoutParamslayoutParams=newLinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT); 13layoutParams.leftMargin=20; 14layoutParams.rightMargin=20; 15textView.setLayoutParams(layoutParams); 16if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN){ 17textView.setBackground(ninePatchDrawable); 18} 19mLlRoot.addView(textView); 20} 21}catch(IOExceptione){ 22e.printStackTrace(); 23}1mLlRoot=findViewById(R.id.ll_root);
可以看到,我们的图片完美拉伸
参考文章
/developer/article/1168755?
https://mp./s/rZyt9ECypfwvNnXKt5xtqg
推荐阅读早就是优势!我每年得忽悠10万程序员上车为什么说80%的程序员都缺乏基本功?
编程·思维·职场
欢迎扫码关注