Android自定义View基础及绘制流程
Android自定义View工作原理关于measure、layout、draw详解
前面两篇文章介绍了android关于View的绘制流程和原理,下面会列举一些简单的自定义View帮助大家更容易理解掌握。
通过前面的介绍自定义ViewGroup一般重写onMeausre和onLayout两个方法。这里回忆一下为何重写这两个方法?
当measure事件和layout事件传递到我们布局的ViewGroup时,会调用onMeasure和onLayout方法,这两个方法交由具体的实现类实现。参考LinearLayout,其onMeasure和onLayout循环遍历了子元素,并且又调用了子元素的measure和layout方法,这样来完成ViewTree的遍历。
我们在Android自定义View工作原理关于measure、layout、draw详解中介绍了自定义ViewGroup的以下结论:
- onMeasure 测量本身和测量子元素,测量子View根据实际情况调用不同方法。一般调用系统测量子元素的方法比如measureChildWithMargins、measureChild,如果子元素只有一层也可以调用view.measure
- onLayout 子元素的位置计算,本身的位置计算已在layout当中完成。
那么,我们根据以上结论来实现一个简单的流式布局
先实现一个简单的效果,不考虑margin,padding,换行等。
每个控件换一行,向右移动相对上一个控件宽度的距离。
xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.mygroupview.MyViewGroup
android:background="@color/design_default_color_background"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="我是控件1"
android:background="@color/design_default_color_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginTop="20dp"
android:text="我是控件2111111111111"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件33333333"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件4444444"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件555555555555555555"
android:layout_marginLeft="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.example.mygroupview.MyViewGroup>
</LinearLayout>
实现步骤:
1.重写onMeasure和onLayout,
2.onMeasure里面测量子元素。遍历出所有子元素,根据ViewGroup的宽高测量规格(也就是子元素的父控件),和子元素的layoutParams调用子元素的measure。
3.测量自身尺寸。如果自身有具体尺寸,返回具体尺寸。否则宽返回所有子元素宽之和,高返回所有子元素高之和。
4.重写onLayout方法,确定子元素位置。
package com.example.mygroupview;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//初步测量自身 主要是为了初始化 一些东西避免后续报错
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
//测量子View尺寸
for(int i =0;i<childCount;i++){
View view = getChildAt(i);
ViewGroup.LayoutParams lp = view.getLayoutParams();
int childWidthSpec = getChildMeasureSpec(widthMeasureSpec,0,lp.width);
int childHeightSpec = getChildMeasureSpec(heightMeasureSpec,0,lp.height);
view.measure(childWidthSpec,childHeightSpec);
}
//测量自身尺寸
int width = 0;
int height = 0;
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
width += getChildAt(i).getMeasuredWidth();
}
break;
default:
break;
}
switch (heightMode){
case MeasureSpec.EXACTLY:
height =heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
height += child.getMeasuredHeight();
}
break;
default:
break;
}
//保存自身的尺寸
setMeasuredDimension(width,height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
int childCount = getChildCount();
for(int i = 0; i < childCount; i++){
View child = getChildAt(i);
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left,top,right,bottom);
left +=child.getMeasuredWidth();
top += child.getMeasuredHeight();
}
}
}
其实列举这个,主要是大家要思考为何要重写这两个方法?这两个方法要做什么事情?调用子元素的测量方法的时候我们需要如何传值?
再回忆下Android自定义View工作原理关于measure、layout、draw详解
系统通过performTraversals依次调用performMeasure、performLayout、performDraw方法,最终会调用顶级View的measure,layout,draw完成顶级View的测量。
顶级View就是decorview,为一个FrameLayout,一般情况下里面是一个LinearLayout,这个LinearLayout又包含两个FrameLayout,分别显示标题和内容,其内容就是我们的setContentView。
measure,layout方法如何从顶级decorview–FrameLayout传递到LinearLayout再传递到我们的View?FrameLayout的measure会调用onMeasure,onMeasure里面会测量自身并遍历子元素,调用子元素的measure方法测量子View。所以就调用到了LinearLayout的meausre方法,而LinearLayout.measure()又会调用LinearLayout.onMeasure方法,在其onMeasure里面又会测量自身并遍历子元素调用子元素的measure方法。这样measure方法就从顶级View传递到了我们的View,对于layout基本也是这个过程。
- ViewGroup的measure和layout方法为final不可重写,它们分别调用了onMeasure和onLayout方法,所以重写onMeasure,和onLayout方法。LinearLayout和FrameLayout都是ViewGroup,它们是如何重写onMeasure和onLayout的?其实总结起来就是我们本文开篇所说的,测量自身和子元素,为子元素确定位置。我们自己重写这两个方法要完成的事情也是这样。
接着上面的例子,扩展下如果加入margin和padding值考虑下换行,实现下面简单的流式布局
还是按照上述思路去思考,需要先确定子元素和本身宽高,再确定子元素的位置。子元素的测量要考虑margin等值,ViewGroup的宽高计算不一样了,对于每个子元素的layout的四个点也需要考虑换行。计算过程确实复杂了很多,但是原理是相通的。其实真正看源码明白了onMeasure,measure,onLayout,layout做的事情后,对于ViewGroup这两个方法的重写基本都是大同小异,无非就是计算过程根据实际需求的复杂度不同。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.mygroupview.MyViewGroup1
android:background="@color/design_default_color_background"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="我是控件1"
android:layout_marginLeft="10dp"
android:paddingLeft="15dp"
android:background="@color/design_default_color_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件2"
android:layout_marginLeft="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件3"
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件4"
android:layout_marginLeft="12dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件5"
android:layout_marginLeft="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件2111111111111"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件33333333"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件4444444"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="我是控件555555555555555555"
android:layout_marginLeft="15dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.example.mygroupview.MyViewGroup1>
</LinearLayout>
package com.example.mygroupview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class MyViewGroup1 extends ViewGroup {
private String TAG = "MyViewGroup1";
public MyViewGroup1(Context context) {
super(context);
}
public MyViewGroup1(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private int lineWidth;//每一行的宽
private int viewGroupWidth;//ViewGroup的宽
private int lineHeight;
private int viewGroupHeight;
private List<List<View>> views = new ArrayList<>();
private List<View> lineViews = new ArrayList<>();//每一行的View
private List<Integer> heights = new ArrayList<>();//记录每行的高度
private void init() {
views.clear();
lineViews.clear();
lineWidth = 0;
lineHeight = 0;
viewGroupWidth = 0;
heights.clear();
viewGroupHeight = 0;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//初步测量自身 主要是为了初始化 一些东西避免后续报错
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
init();
// 计算子View限制信息
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//测量子View宽高
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
//计算每行的View主要是为了测量父View宽高
int childWidth = childView.getMeasuredWidth();
int childheight = childView.getMeasuredHeight();
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin > widthSize - getPaddingRight() - getPaddingLeft()) {
//换行的时候清空记录每行的list,并把它加入两层list的views
views.add(lineViews);
lineViews = new ArrayList<>();
viewGroupWidth = Math.max(viewGroupWidth, lineWidth);
viewGroupHeight += lineHeight;
heights.add(lineHeight);
lineHeight = 0;
lineWidth = 0;
}
// 不换行
lineViews.add(childView);
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
lineHeight = Math.max(lineHeight, childheight + lp.topMargin + lp.bottomMargin);
if (i == childCount - 1) {
viewGroupWidth = Math.max(viewGroupWidth, lineWidth);
viewGroupHeight += lineHeight;
heights.add(lineHeight);
views.add(lineViews);
}
}
//测量自身尺寸
int width = 0;
int height = 0;
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
width = viewGroupWidth;
break;
default:
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
height = viewGroupHeight;
break;
default:
break;
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int top = 0;
for (int i = 0; i < views.size(); i++) {
List<View> lineViews = views.get(i);
lineHeight = heights.get(i);//行高
// 遍历当前行
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
// 该child的LayoutParams
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//上一个child的LayoutParams
MarginLayoutParams lastLp = null;
View lastChild = null;
if (j != 0) {
lastChild = lineViews.get(j - 1);
lastLp = (MarginLayoutParams) lastChild.getLayoutParams();
}
if (j == 0) {
left = lp.leftMargin;
} else {
//上一次的左边+上个的宽+上一个的右边距+这一个的左边距
left = left + lastChild.getMeasuredWidth() + lastLp.rightMargin + lp.leftMargin;
}
Log.d(TAG, "第" + i + "行第" + j + "个 left:" + left + ",right:" + (left + child.getMeasuredWidth()) + ",top:" + (top + lp.topMargin )+ ",buttom:" + (top + lp.topMargin + child.getMeasuredHeight()));
child.layout(left, top + lp.topMargin, left + child.getMeasuredWidth(), top + lp.topMargin + child.getMeasuredHeight());
}
top += lineHeight;
left = 0;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new MyLayoutParams(lp);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
public static class MyLayoutParams extends MarginLayoutParams {
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public MyLayoutParams(int width, int height) {
super(width, height);
}
public MyLayoutParams(LayoutParams lp) {
super(lp);
}
}
}
关于layoutParams的坑
这里注意下有个layoutParams的坑,计算子元素宽高需要考虑margin的情况下,我们可以调用系统给我们提供的measureChildWithMargins。传入的值是子元素父控件的宽高规格和子元素的layoutParams。那么这个layoutParams还能不能用ViewGroup的layoutParams呢?
遗憾的是这样传入会报错,需要传入MarginLayoutParams。
我们来看看measureChildWithMargins源码:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
通过上述可以看到,需要传入的是MarginLayoutParams 。如果我们不重写
generateLayoutParams方法,通过View的添加过程addView可以看到,默认传入的是ViewGroup.LayoutParams。
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException(
"generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
以上只是一个简单的流式布局,里面还有很多需要优化的地方,比如超出屏幕如果没有放在scrollView里面无法滑动等。可结合滑动冲突系列文章处理Android滑动冲突解决方案内外部拦截法及原理
本文主要是帮助大家加深理解View的measure和layout的过程,这里就不做介绍了。
最后这里记录一个github上面的一个流式布局地址: https://github.com/google/flexbox-layout