本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/57c7ff5d53bbcffd68c64411java
做者:黄进——QQ音乐团队android
相信每个Android开发者,在接触“Hello World”的时候,就造成了一个观念:Android UI布局是经过layout目录下的XML文件定义的。使用XML定义布局的方式,有着结构清晰、可预览等优点,于是极为通用。但是,恰恰在某些场景下,布局是须要根据运行时的状态变化的,没法使用XML预先定义。这时候,咱们只能经过JavaCode控制,在程序运行时,动态的实现对应的布局。git
因此,做为入门,将从给三个方面给你们介绍一些动态布局相关的基础知识和经验。github
NinePatchChunk
,解析如何实现后台下发.9图片给客户端使用。这一步,顾名思义,就是把咱们要的View添加到界面上去。这是动态布局中最基础最经常使用的步骤。数组
Android开发中,咱们用到的Button
、ImageView
、RelativeLayout
、LinearLayout
等等元素最终都是继承于View
这个类的。按照我本身的理解,能够将它们分为两类,控件和容器(这两个名字纯属做者本身编的,并不是官方定义)。Button
、ImageView
这类直接继承于View
的就是控件,控件通常是用来呈现内容和与用户交互的;RelativeLayout
、LinearLayout
这类继承于ViewGroup
的就是容器,容器就是用来装东西的。Android是嵌套式布局的设计,所以,容器装的既能够是容器,也能够是控件。微信
更直接的,仍是经过一段demo代码来看吧。app
首先,由于不能setContentView(R.layout.xxx)
了,咱们须要先添加一个root
做为整个的容器,ide
RelativeLayout root = new RelativeLayout(this); root.setBackgroundColor(Color.WHITE); setContentView(root, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
而后,咱们尝试在屏幕正中间添加一个按钮,工具
Button button1 = new Button(this); button1.setId(View.generateViewId()); button1.setText("Button1"); button1.setBackgroundColor(Color.RED); LayoutParams btnParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); btnParams.addRule(RelativeLayout.CENTER_IN_PARENT, 1); root.addView(button1, btnParams);
到这里能够发现,只须要三步,就能够添加一个view(以按钮为例)到相应的容器root
里面了,布局
new Button(this)
,并初始化控件相关的属性。root
的类型,new LayoutParams
,这个参数主要用来描述要添加的view
在容器中的定位信息,包括高宽,居中对齐,margin等等属性。特别地,对于上面的例子,相对于父容器居中的实现是,btnParams.addRule(RelativeLayout.CENTER_IN_PARENT, 1)
,这里对应XML的代码则是android:centerInParent='true'
。root.addView(button1, btnParams)
就好了。接下来,搞的稍微复杂点,继续在按钮的右下方添加一个线性布局,向其中添加一个TextView
和Button
,并且各自占的宽度比例为2:3(对于android:layout_weight
属性),demo代码以下,
// 在按钮右下方添加一个线性布局 LinearLayout linearLayout = new LinearLayout(this); linearLayout.setOrientation(LinearLayout.HORIZONTAL); LayoutParams lParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); lParams.addRule(RelativeLayout.BELOW, button1.getId()); lParams.addRule(RelativeLayout.RIGHT_OF, button1.getId()); root.addView(linearLayout, lParams); // 在线性布局中,添加一个TextView和一个Button,宽度按2:3的比例 TextView textView = new TextView(this); textView.setText("TextView"); textView.setTextSize(28); textView.setBackgroundColor(Color.BLUE); LinearLayout.LayoutParams tParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); tParams.weight = 2; // 定义宽度的比例 linearLayout.addView(textView, tParams); Button button2 = new Button(this); button2.setText("Button2"); button2.setBackgroundColor(Color.RED); LinearLayout.LayoutParams bParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); bParams.weight = 3; // 定义宽度的比例 linearLayout.addView(button2, bParams);
须要注意的是,上面代码中的lParams.addRule(RelativeLayout.BELOW, button1.getId())
(XML
对应android:layout_below
)
规则若是定义的是一个view相对于另外一个view的,必定要初始化另外一个view(button1
)的id不为0,不然规则会失效。一般,为了防止id重复,建议使用系统方法来生成id,也就是第二段代码中的button1.setId(View.generateViewId())
。
最终,这一段代码执行下来,咱们获得的效果就是,
可是,添加view做者也遇到过一个小小坑。
以下图左边部分,做者曾经遇到一个场景,须要在RelativeLayout
右边添加一个ImageView
,同时,这个ImageView
的右边部分在RelativeLayout
的外面。
一开始,做者的代码以下,却只能获得上图右边的效果,
ImageView imageView = new ImageView(this); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height); params.leftMargin = x; // 到左边的距离 params.topMargin = y; // 到上边的距离 parent.addView(imageView, params);
后来本人猜想,这是由于onMeasure
和onLayout
的时候,受到了rightMargin
默认为0的限制。
后来,通过本人验证,要跳过这个坑,加一行params.rightMargin = -1*width
就能够了。(有兴趣的同窗能够去看看源码,这里就不详解了)
上一节,咱们只是摆脱了layout目录的XML文件。但是还有一类XML文件,频繁的被layout目录的XML文件引用,那就是drawable目录的XML文件。drawable目录的下文件,一般是定义了一些,selector
,shape
等等。但是,考虑到一个场景:selector
里面引用的图片,不是打包时res目录的资源,而是后台下发的图片呢?相似场景下,咱们能不能摆脱这类XML文件呢?
根据上一节的经验,要相信,XML
定义能实现的,Java代码必定可以实现。从drawable
的目录名就能够看出,不论是selector
,shape
或是其余,总归都应该是drawable
。所以,在Java代码中,总应该有一个Drawable
的子类来对应他们。下面,就介绍几个经常使用的Drawable
的子类给你们。
StateListDrawable:对应selector
,主要用来描述按钮等的点击态。
StateListDrawable selector = new StateListDrawable(); btnSelectorDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePress); btnSelectorDrawable.addState(new int[]{android.R.attr.state_enabled}, drawableEnabel); btnSelectorDrawable.addState(new int[]{android.R.attr.state_selected}, drawableSelected); btnSelectorDrawable.addState(new int[]{android.R.attr.state_focused}, drawableFocused); btnSelectorDrawable.addState(new int[]{}, drawableNormal);
GradientDrawable:对应渐变色
。
GradientDrawable drawable = new GradientDrawable(); drawable.setOrientation(Orientation.TOP_BOTTOM); //定义渐变的方向 drawable.setColors(colors); //colors为int[],支持2个以上的颜色
最后,说一个比较复杂的Drawable,是进度条相关的。
LayerDrawable:对应Seekbar android:progressDrawable
一般,咱们用XML定义一个进度条的ProgressDrawable是这样的,
<!--ProgressDrawable--> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="[@android](http://my.oschina.net/asia):id/background" android:drawable="@drawable/background"/> <item android:id="[@android](http://my.oschina.net/asia):id/secondaryProgress" android:drawable="@drawable/secondary_progress"/> <item android:id="[@android](http://my.oschina.net/asia):id/progress" android:drawable="@drawable/progress"/> </layer-list>
而对于其中的,@drawable/progress
和@drawable/secondary_progress
也不是普通的drawable,
<!--@drawable/progress 定义--> <clip xmlns:android="http://schemas.android.com/apk/res/android" android:clipOrientation="horizontal" android:drawable="@drawable/progress_drawable" android:gravity="left" > </clip>
也就是说,经过XML要定义进度条的ProgressDrawable
,咱们须要定义多个XML文件的,仍是比较复杂的。那么JavaCode实现呢?
其实,理解了XML实现的方式,下面的JavaCode就很好理解了。
LayerDrawable layerDrawable = (LayerDrawable) getProgressDrawable(); //背景 layerDrawable.setDrawableByLayerId(android.R.id.background, backgroundDrawable); //进度条 ClipDrawable clipProgressDrawable = new ClipDrawable(progressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL); layerDrawable.setDrawableByLayerId(android.R.id.progress, clipProgressDrawable); //缓冲进度条 ClipDrawable clipSecondaryProgressDrawable = new ClipDrawable(secondaryProgressDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL); layerDrawable.setDrawableByLayerId(android.R.id.secondaryProgress, clipSecondaryProgressDrawable);
更多的Drawable
的子类,你们能够根据本身需求去官方文档上查询就好了。
.9.png
图片对Android开发来讲,都不陌生。一般状况下,咱们对于.9.png
图片的使用,只须要简单的放到resource目录下,而后,当作普通图片来用就能够了。然而,以本人的经验,若是要动态下发'.9.png'图片给客户端使用就很蛋疼了。
一开始,当我想固然觉得能够直接加载本地.9.png
图片,用的飞起的时候,发现了Android Nine Patch的一个大坑!!!
“说好的自动拉升了???”(隐隐约约感受到某需求的工做量又少评估了一天。。。。。。。)
经过查阅资料发现,原来,工程里面用的.9.png
在打包的时候,通过了aapt
的处理,成为了一张包含有特殊信息的.png
图片。而不是直接加载的.9.png
这种图片。
那么第一个思路就来了(参考引用),首先,咱们先对.9.png
执行一个aapt
命令。
aapt.exe s -i xx.9.png -o xx.png
而后,后台下发这种处理过的.png
,客户端经过以下代码,就能够加载这张图片,获得一个有局部拉伸效果的NinePatchDrawable
了。
Bitmap bitmap = BitmapFactory.decodeFile(filePath); NinePatchDrawable npd = new NinePatchDrawable(context.getResource(), bitmap, bitmap.getNinePatchChunk(), new Rect(), null);
但是,这个初级方式并非太完美,每次后台配置新的图片,都须要aapt
处理一遍,后台须要针对iOS和Android区分平台下发不一样图片。总之,不太科学!那么有没有更加完全的方式呢?
完全理解.9.png
回顾NinePatchDrawable
的构造方法第三个参数bitmap.getNinePatchChunk()
,做者猜测,aapt
命令其实就是在bitmap图片中,加入了NinePatchChunk
的信息,那么咱们是否是只要能本身构造出这个东西,就可让任何图片按照咱们想要的方式拉升了呢?
但是查了一堆官方文档,彷佛并找不到相应的方法来得到这个byte[]
类型的chunk
参数。
既然没法知道这个chunk
如何生成,那么能不能从解析的角度逆向得出这个NinePatchChunk
的生成方法呢?
下面就须要从源码入手了。
public static NinePatchChunk deserialize(byte[] data) { ByteBuffer byteBuffer = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); byte wasSerialized = byteBuffer.get(); if (wasSerialized == 0) return null; 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; }
其实从这部分解析byte[] chunk
的源码,咱们已经能够反推出来大概的结构了。以下图,
按照上图中的猜测以及对.9.png
的认识,直觉感觉到,mDivX
,mDivY
,mColor
这三个数组是最关键的,可是具体是什么,就要继续看源码了。
/** * This chunk specifies how to split an image into segments for * scaling. * * There are J horizontal and K vertical segments. These segments divide * the image into J*K regions as follows (where J=4 and K=3): * * F0 S0 F1 S1 * +-----+----+------+-------+ * S2| 0 | 1 | 2 | 3 | * +-----+----+------+-------+ * | | | | | * | | | | | * F2| 4 | 5 | 6 | 7 | * | | | | | * | | | | | * +-----+----+------+-------+ * S3| 8 | 9 | 10 | 11 | * +-----+----+------+-------+ * * Each horizontal and vertical segment is considered to by either * stretchable (marked by the Sx labels) or fixed (marked by the Fy * labels), in the horizontal or vertical axis, respectively. In the * above example, the first is horizontal segment (F0) is fixed, the * next is stretchable and then they continue to alternate. Note that * the segment list for each axis can begin or end with a stretchable * or fixed segment. * /
正如源码中,注释的同样,这个NinePatch Chunk
把图片从x轴和y轴分红若干个区域,F区域表明了固定,S区域表明了拉伸。mDivX
,mDivY
描述了全部S区域的位置起始,而mColor
描述了,各个Segment的颜色,一般状况下,赋值为源码中定义的NO_COLOR = 0x00000001
就好了。就以源码注释中的例子来讲,mDivX
,mDivY
,mColor
以下:
mDivX = [ S0.start, S0.end, S1.start, S1.end]; mDivY = [ S2.start, S2.end, S3.start, S3.end]; mColor = [c[0],c[1],...,c[11]]
对于mColor
这个数组,长度等于划分的区域数,是用来描述各个区域的颜色的,而若是咱们这个只是描述了一个bitmap的拉伸方式的话,是不须要颜色的,即源码中NO_COLOR = 0x00000001
说了这么多,咱们仍是经过一个简单例子来讲明如何构造一个按中心点拉伸的NinePatchDrawable
吧,
Bitmap bitmap = BitmapFactory.decodeFile(filepath); int[] xRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1}; int[] yRegions = new int[]{bitmap.getWidth() / 2, bitmap.getWidth() / 2 + 1}; int NO_COLOR = 0x00000001; int colorSize = 9; int bufferSize = xRegions.length * 4 + yRegions.length * 4 + colorSize * 4 + 32; ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize).order(ByteOrder.nativeOrder()); // 第一个byte,要不等于0 byteBuffer.put((byte) 1); //mDivX length byteBuffer.put((byte) 2); //mDivY length byteBuffer.put((byte) 2); //mColors length byteBuffer.put((byte) colorSize); //skip byteBuffer.putInt(0); byteBuffer.putInt(0); //padding 先设为0 byteBuffer.putInt(0); byteBuffer.putInt(0); byteBuffer.putInt(0); byteBuffer.putInt(0); //skip byteBuffer.putInt(0); // mDivX byteBuffer.putInt(xRegions[0]); byteBuffer.putInt(xRegions[1]); // mDivY byteBuffer.putInt(yRegions[0]); byteBuffer.putInt(yRegions[1]); // mColors for (int i = 0; i < colorSize; i++) { byteBuffer.putInt(NO_COLOR); } return byteBuffer.array();
后来也在github上找到了一个现成的Library,有兴趣的同窗能够直接去学习和使用。
参考资料:
https://android.googlesource.com/platform/pac kages/apps/Gallery2/+/jb-dev/src/com/android/gallery3d/ui/NinePatchChunk.java
https://android.googlesource.com/platform/frameworks/base/+/master/include/androidfw/ResourceTypes.h
http://stackoverflow.com/questions/5079868/create-a-ninepatch-ninepatchdrawable-in-runtime
更多精彩内容欢迎关注bugly的微信公众帐号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!