前言:生命总是要有信仰,有梦想才能一直前行,哪怕走的再慢,也是在前行。
相关文章:
1、《FlowLayout详解(一)——onMeasure()与onLayout()》
2、《FlowLayout详解(二)——FlowLayout实现》
今天给大家讲讲有关自定义布局控件的问题,大家来看这样一个需求,你需要设计一个container,实现内部控件自动换行。即里面的控件能够根据长度来判断当前行是否容得下它,进而决定是否转到下一行显示。效果图如下
在上图中,所有的紫色部分是FlowLayout控件,明显可以看出,内部的每个TextView控件,可以根据大小自动排列。
效果图就是这样子了,第一篇先讲下预备知识。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)这里我们主要关注传进来的两个参数:int widthMeasureSpec, int heightMeasureSpec
//对应11000000000000000000000000000000;总共30位,前两位是1 int MODE_MASK = 0xc0000000; //提取模式 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } //提取数值 public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }相信大家看了代码就应该清楚模式和数值提取的方法了吧,主要用到了MASK的与、非运算,难度不大,如果有问题,自行谷歌一下与、非运算方法吧。
MeasureSpec.getMode(int spec) //获取MODE MeasureSpec.getSize(int spec) //获取数值另外MODE的取值为:
MeasureSpec.AT_MOST MeasureSpec.EXACTLY MeasureSpec.UNSPECIFIED通过下面的代码就可以分别获取widthMeasureSpec和heightMeasureSpec的MODE和数值
int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);其实大家通过查看代码可以知道,我们的实现就是MeasureSpec.getSize()和MeasureSpec.getMode()的实现代码。
例如,下面这个XML
<com.example.harvic.myapplication.FlowLayout android:layout_width="match_parent" android:layout_height="wrap_content"> </com.example.harvic.myapplication.FlowLayout>那FlowLayout在onMeasure()中传值时widthMeasureSpec的模式就是 MeasureSpec.EXACTLY,即父窗口宽度值。heightMeasureSpec的模式就是 MeasureSpec.AT_MOST,即不确定的。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); //经过计算,控件所占的宽和高分别对应width和height //计算过程,我们会在下篇细讲 ………… setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height); }
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);是一个抽象方法,说明凡是派生自ViewGroup的类都必须自己去实现这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
这个效果图主要有两点:
1、三个TextView竖直排列
2、背景的Layout宽度是match_parent,高度是wrap_content.
下面我们就看一下,代码上如何实现:
(1)、XML布局
首先我们看一下XML布局:(activity_main.xml)
<com.harvic.simplelayout.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff00ff" tools:context=".MainActivity"> <TextView android:text="第一个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:text="第二个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:text="第三个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </com.harvic.simplelayout.MyLinLayout>可见里面有三个TextView,然后自定义的MyLinLayout布局,宽度设为了match_parent,高度设为了wrap_content.
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); int height = 0; int width = 0; int count = getChildCount(); for (int i=0;i<count;i++) { //测量子控件 View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); //获得子控件的高度和宽度 int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); //得到最大宽度,并且累加高度 height += childHeight; width = Math.max(childWidth, width); } setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height); }首先,是从父类传过来的建议宽度和高度值:widthMeasureSpec、heightMeasureSpec
int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);接下来就是通过测量它所有的子控件来决定它所占位置的大小:
int height = 0; int width = 0; int count = getChildCount(); for (int i=0;i<count;i++) { //测量子控件 View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); //获得子控件的高度和宽度 int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); //得到最大宽度,并且累加高度 height += childHeight; width = Math.max(childWidth, width); }我们这里要计算的是整个VIEW当被设置成layout_width="wrap_content",layout_height="wrap_content"所占用的大小,因为我们是垂直排列其内部所有的VIEW,所以container所占宽度应该是各个TextVIew中的最大宽度,所占高度应该是所有控件的高度和。
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);前面我们讲过,模式与XML布局的对应关系:
<com.harvic.simplelayout.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff00ff" tools:context=".MainActivity">所以这里的measureWidthMode应该是MeasureSpec.EXACTLY,measureHeightMode应该是MeasureSpec.AT_MOST;所以在最后利用setMeasuredDimension(width,height)来最终设置时,width使用的是从父类传过来的measureWidth,而高度则是我们自己计算的height.即实际的运算结果是这样的:
setMeasuredDimension(measureWidth,height);
总体来讲,onMeasure()中计算出的width和height,就是当XML布局设置为layout_width="wrap_content"、layout_height="wrap_content"时所占的宽和高;即整个container所占的最小矩形
(3)、MyLinLayout实现:重写onLayout()函数
在这部分,就是根据自己的意愿把内部的各个控件排列起来。我们要完成的是将所有的控件垂直排列;protected void onLayout(boolean changed, int l, int t, int r, int b) { int top = 0; int count = getChildCount(); for (int i=0;i<count;i++) { View child = getChildAt(i); int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(0, top, childWidth, top + childHeight); top += childHeight; } }最核心的代码,就是调用layout()函数设置子控件所在的位置:
int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(0, top, childWidth, top + childHeight); top += childHeight;
在这里top指的是控件的顶点,那bottom的坐标就是top+childHeight,我们从最左边开始布局,那么right的坐标就肯定是子控件的宽度值了childWidth.
到这里,这个例子就讲完了,源码会在文章底部给出,下面来讲一个非常容易混淆的问题。
(4)、getMeasuredWidth()与getWidth()int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); child.layout(0, top, childWidth, top + childHeight);从代码中可以看到,我们使用child.layout(0, top, childWidth, top + childHeight);来布局控件的位置,其中getWidth()的取值就是这里的右坐标减去左坐标的宽度;因为我们这里的宽度是直接使用的child.getMeasuredWidth()的值,当然会导致getMeasuredWidth()与getWidth()的值是一样的。如果我们在调用layout()的时候传进去的宽度值不与getMeasuredWidth()相同,那必然getMeasuredWidth()与getWidth()的值就不再一样了。
前面我们说了,在派生自ViewGroup的container中,比如我们上面的MyLinLayout,在onLayout()中布局它所有的子控件。那它自己什么时候被布局呢?
当然是在他的父类中。在所有的控件的最顶部有一个ViewRoot,它才是所有控件的最终祖先结点。那让我们来看看它是怎么来做的吧。
在它布局里,会调用它自己的一个layout()函数(不能被重载,代码位于View.java):
/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent * @param b Bottom position, relative to parent */ public final void layout(int l, int t, int r, int b) { boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴 if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局 mPrivateFlags &= ~LAYOUT_REQUIRED; } mPrivateFlags &= ~FORCE_LAYOUT;在SetFrame(l,t,r,b)就是设置自己的位置,设置结束以后才会调用onLayout(changed, l, t, r, b)来设置内部所有子控件的位置。
在这部分,大家先不必纠结这个例子为什么要这么写,我会先简单粗暴的教大家怎么先获取到margin值,然后再细讲为什么这样写,他们的原理是怎样的。
如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
我们在上面MyLinLayout例子的基础上,添加上layout_margin参数;
(1)、首先,在XML中添加上layout_margin参数
<com.harvic.simplelayout.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ff00ff" tools:context=".MainActivity"> <TextView android:text="第一个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:background="#ff0000"/> <TextView android:text="第二个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:background="#00ff00"/> <TextView android:text="第三个VIEW" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="30dp" android:background="#0000ff"/> </com.harvic.simplelayout.MyLinLayout>我们在每个TextView中都添加了一layout_marginTop参数,而且值分别是10dp,20dp,30dp;背景也都分别改为了红,绿,蓝;
从图中可以看到,根本没作用!!!这是为什么呢?因为测量和布局都是我们自己实现的,我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果啦。需要特别注意的是,如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算container的大小时,也要加上margin,不然会导致container太小,而控件显示不全的问题。费话不多说,我们直接看代码实现。
(2)、重写generateLayoutParams()函数
重写代码如下:@Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); }在这里,我们重写了两个函数,一个是generateLayoutParams()函数,一个是generateDefaultLayoutParams()函数。直接返回对应的MarginLayoutParams()的实例。至于为什么要这么写,我们后面再讲,这里先把Margin信息获取到再说。
(3)、重写onMeasure()
让我们先看一下重写好的onMeasure()函数代码:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); int height = 0; int width = 0; int count = getChildCount(); for (int i=0;i<count;i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin; height += childHeight; width = Math.max(childWidth, width); } setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height); }最关键的地方是改了这句:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;通过 child.getLayoutParams()获取child对应的LayoutParams实例,将其强转成MarginLayoutParams;
(4)、重写onLayout()函数
同样,我们在布局时仍然将间距加到控件里就好了,完整代码如下:@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int top = 0; int count = getChildCount(); for (int i=0;i<count;i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin; child.layout(0, top, childWidth, top + childHeight); top += childHeight; } }在这里同样在布局子控件时,添加上子控件间的间距,具体就不讲了,很容易理解。
从效果图中可以明显的看到每个ITEM都添加上了间距了。
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();下面我们来看看这么做的原因。
/** *从指定的XML中获取对应的layout_width和layout_height值 */ public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } /* *如果要使用默认的构造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"对应的参数 */ protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }所以,如果我们还需要margin相关的参数就只能重写generateLayoutParams()函数了:
public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }由于generateLayoutParams()的返回值是LayoutParams实例,而MarginLayoutParams是派生自LayoutParam的;所以根据类的多态的特性,可以直接将此时的LayoutParams实例直接强转成MarginLayoutParams实例;
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();大家也可以为了安全起见利用instanceOf来做下判断,如下:
MarginLayoutParams lp = null if (child.getLayoutParams() instanceof MarginLayoutParams) { lp = (MarginLayoutParams) child.getLayoutParams(); ………… }所以整体来讲,就是利用了类的多态特性!下面来看看MarginLayoutParams和generateLayoutParams()都做了什么。
(1)generateLayoutParams()实现
首先,我们看看generateLayoutPararms()都做了什么吧,它是怎么得到布局值的:
//位于ViewGrop.java中 public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } public LayoutParams(Context c, AttributeSet attrs) { TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height); a.recycle(); } protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { width = a.getLayoutDimension(widthAttr, "layout_width"); height = a.getLayoutDimension(heightAttr, "layout_height"); }从上面的代码中明显可以看出,generateLayoutParams()调用LayoutParams()产生布局信息,而LayoutParams()最终调用setBaseAttributes()来获得对应的宽,高属性。
下面再来看看MarginLayoutParams的具体实现,其实通过上面的过程,大家也应该想到,它也是通过TypeArray来解析自定义属性来获得用户的定义值的(大家看到长代码不要害怕,先列出完整代码,下面会分段讲):
public MarginLayoutParams(Context c, AttributeSet attrs) { super(); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; } else { leftMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN); rightMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN); topMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED); startMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE); } a.recycle(); }这段代码分为两部分:
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; } else { ………… }在这段代码中就是通过提取layout_margin的值来设置上,下,左,右边距的。
leftMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN); rightMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN); topMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED); startMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE);这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。难度不大,也没什么好讲的了。
推荐文章:(推荐的文章,大家有时间一定要读一下,这里包含了本篇想讲但没有篇幅再讲的内容)
这篇文章是想要告诉大家:LayoutParams是控件生成给子控件使用的,而不是给自己使用的,自己的Layout布局参数是由父控件生成的!
《安卓冷知识:LayoutParams》
源码内容:
1、《SimpleLayout》:第三部分对应源码:MyLinLayout的初步实现
2、《SimpleLayoutAdvance》:第四部分对应源码:添加上margin值的获取方法
如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/8928283
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/47029169 谢谢