一篇文章搞懂 Android 自定义 viewgroup 的难点,又是一年金九银十
首先明确一个结论:
对于完全自定义的 view,完全自己写的 onMeasure 方法来说,你保存的宽高必须要符合父 view 的限制,否则会发生 bug, 保存父 view 对子 view 的限制的方法也很简单直接调用 resolveSize 方法即可。
![](https://static001.geekbang.org/infoq/6b/6b8b92661b13f0593a7865b9bb559963.png)
![](https://static001.geekbang.org/infoq/6b/6b8b92661b13f0593a7865b9bb559963.png)
所以对于完全自定义的 view onMeasure 方法也不难写了,
先算自己想要的宽高,比如你画了个圆,那么宽高就肯定是半径的两倍大小, 要是圆下面还有字, 那么高度肯定除了半径的两倍还要有字体的大小。对吧。很简单。这个纯看你自定义 view 是啥样的
算完自己想要的宽高以后 直接拿 resolveSize 方法处理一下 即可。
最后 setMeasuredDimension 保存。
范例:
public class LoadingView extends View {
//圆形的半径 int radius;
//圆形外部矩形 rect 的起点 int left = 10, top = 30;
Paint mPaint = new Paint();
public LoadingView(Context context) {super(context);}
public LoadingView(Context context, AttributeSet attrs) {super(context, attrs);TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = left + radius * 2;int height = top + radius * 2;
//一定要用 resolveSize 方法来格式化一下你的 view 宽高噢,否则遇到某些 layout 的时候一定会出现奇怪的 bug 的。//因为不用这个 你就完全没有父 view 的感受了 最后强调一遍 width = resolveSize(width, widthMeasureSpec);height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);}
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);
RectF oval = new RectF(left, top,left + radius * 2, top + radius * 2);mPaint.setColor(Color.BLUE);canvas.drawRect(oval, mPaint);//先画圆弧 mPaint.setColor(Color.RED);mPaint.setStyle(Paint.Style.STROKE);mPaint.setStrokeWidth(2);canvas.drawArc(oval, -90, 360, false, mPaint);}}
布局文件:
<LinearLayoutandroid:layout_width="200dp"android:layout_height="200dp"android:background="#000000"android:orientation="horizontal">
<com.example.a16040657.customviewtest.LoadingViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/dly"app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
<com.example.a16040657.customviewtest.LoadingViewandroid:layout_marginLeft="10dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@mipmap/dly"app:radius="200"></com.example.a16040657.customviewtest.LoadingView></LinearLayout>
最后效果:
![](https://static001.geekbang.org/infoq/6b/6b8b92661b13f0593a7865b9bb559963.png)
自定义一个 viewgroup
这个其实也就是稍微复杂了一点,但是还是有迹可循的,只是稍微需要一点额外的耐心。
自定义一个 viewgroup 需要注意的点如下:
一定是先重写 onMeasure 确定子 view 的宽高和自己的宽高以后 才可以继续写 onlayout 对这些子 view 进行布局噢~~
viewgroup 的 onMeasure 其实就是遍历自己的 view 对自己的每一个子 view 进行 measure,绝大多数时候对子 view 的 measure 都可以直接用 measureChild()这个方法来替代,简化我们的写法,如果你的 viewgroup 很复杂的话 无法就是自己写一遍 measureChild 而不是调用 measureChild 罢了。
计算出 viewgroup 自己的尺寸并且保存,保存的方法还是哪个 setMeasuredDimension 不要忘记了 4. 逼不得已要重写 measureChild 方法的时候,其实也不难无非就是对父 view 的测量和子 view 的测量 做一个取舍关系而已, 你看懂了基础的 measureChild 方法,以后就肯定会写自己的复杂的 measureChild 方法了。
下面是一个极简的例子,一个很简单的 flowlayout 的实现,没有对 margin paddding 做处理,也假设了每一个 tag 的高度 是固定的,可以说是极为简单了,但是麻雀虽小 五脏俱全,足够你们好好理解自定义 viewgroup 的关键点了。
/**
写一个简单的 flowlayout 从左到右的简单 layout,如果宽度不够放 就直接另起一行 layout
这个类似的开源控件有很多,有很多写的出色的,我这里只仅仅实现一个初级的 flowlayout
也是最简单的,目的是为了理解自定义 viewgroup 的关键核心点。
<p>
比方说这里并没有对 padding 或者 margin 做特殊处理,你们自己写 viewgroup 的时候 记得把这些属性的处理都加上
否则一旦有人用了这些属性 发现没有生效就比较难看了。。。。。。*/public class SimpleFlowLayout extends ViewGroup {public SimpleFlowLayout(Context context) {super(context);}
public SimpleFlowLayout(Context context, AttributeSet attrs) {super(context, attrs);}
public SimpleFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}
/**
layout 的算法 其实就是 不够放剩下一行 那另外放一行 这个过程一定要自己写一遍才能体会,
个人有个人的写法,说不定你的写法比开源的项目还要好
其实也没什么夸张的,无法就是前面 onMeasure 结束以后 你可以拿到所有子 view 和自己的 测量宽高 然后就算呗
@param changed
@param l
@param t
@param r
@param b*/
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int childTop = 0;int childLeft = 0;int childRight = 0;int childBottom = 0;
//已使用 widthint usedWidth = 0;
//customlayout 自己可使用的宽度 int layoutWidth = getMeasuredWidth();Log.v("wuyue", "layoutWidth==" + layoutWidth);for (int i = 0; i < getChildCount(); i++) {View childView = getChildAt(i);//取得这个子 view 要求的宽度和高度 int childWidth = childView.getMeasuredWidth();int childHeight = childView.getMeasuredHeight();
//如果宽度不够了 就另外启动一行 if (layoutWidth - usedWidth < childWidth) {childLeft = 0;usedWidth = 0;childTop += childHeight;childRight = childWidth;childBottom = childTop + childHeight;childView.layout(0, childTop, childRight, childBottom);usedWidth = usedWidth + childWidth;childLeft = childWidth;continue;}childRight = childLeft + childWidth;childBottom = childTop + childHeight;childView.layout(childLeft, childTop, childRight, childBottom);childLeft = childLeft + childWidth;usedWidth = usedWidth + childWidth;
}}
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先取出 SimpleFlowLayout 的父 view 对 SimpleFlowLayout 的测量限制 这一步很重要噢。//你只有知道自己的宽高 才能限制你子 view 的宽高 int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);
评论