什么是自定义View?
在Android开发中,系统提供给我们的UI控件是有限的,当我们需要使用一些特殊的控件的时候,只靠系统提供的控件,可能无法达到我们想要的效果,这时,就需要我们自定义一些控件,来完成我们想要的效果了。下面,我就来讲讲自定义控件的那些事。
首先,我来讲讲Android的控件架构。Android的控件可以被分为两类,分别是ViewGroup和View。在ViewGroup中可以包含多个View,并且管理他们。控件树就是有这两个部分组成的,控件树的上层负责的是下层控件的绘制和测量以及交互。我们在Activity中使用的findViewById()方法,就是在控件树中用深度遍历的方法搜索到对应的ID的。每一颗控件树的顶部,都有个ViewParent对象,他是整棵树的核心,负责调度所有的交互事件。在Activity中,我们是使用setContentView()来加载布局的。每个Activity都是包含着一个Window对象的,在Android中通常是PhoneWindow,他将一个DecorView作为整个窗口的根View,将要显示的内容呈现在window上。DecorView又分为两个部分,一个是TitleView,一个是ContentView。ContentView是一个ID为content的Framelayout,布局文件就是设置在这里面的。而TitleView就是我们看到topbar标题栏。这就是activity加载布局文件的过程了。
View什么时候发生绘制
在Android应用中,View的第一次绘制是伴随这个Activity启动开始的。当Activity生命周期执行到onCreate时,我们都知道这时候会调用setContentView方法。View的绘制就是从这里开始。
除此之外,当View树中的视图发生变化时,会开始View的绘制;或者主动调用View的绘制方法,比如invalidate方法。这个都会发起View的绘制。
setContentView之后发生了什么?
通过在Activity中调用setContentView方法开始View的加载。这个过程是通过Window对象加载的。我们可以再PhoneWindow中找到这个方法。在这个方法中可以看到一个installDecor的方法。这个方法的作用就是初始化顶级View,也就是DecorView(这里不再介绍DecorView的创建过程,想了解的同学可以自行阅读代码)。之后View的工作流程就是从DecorView开始的,这个后面再讲。
setContentView只是先将顶级View初始化,还没有开始View的绘制。接着往下看,ActivityThread继续执行Activity的生命周期。在ActivityThread执行到handleResumeActivity方法时,这里调用了Activity的生命周期函数onResume。接着在通过WindowManager添加了DecorView,然后才开始了View的工作流程。这里也解释了为什么在Activity在执行完onResume的时候用户才可以跟App交互。
最后创建了ViewRootImpl的实例,ViewRootImpl的作用就是沟通View和WindowManager,实现两者所需要的协议,它管理着View的工作流程。 ViewRootImpl中的performTraversals方法中可以看到执行了3个方法。分别是performMeasure、performLayout、performDraw,从名称上也可以想到,着3个方法执行了View的measure、layout、draw方法
View怎么测量大小?
View通过measure来确定大小 measure的作用就是决定View到底有多大。在整个View树种是由View和ViewGroup组成。而measure也分为着两种绘制方式。View的measure只测试自身大小。ViewGroup除了测量自身大小,还负责测量子View的大小。
MeasureSpec的作用
MeasureSpec封装了View的规格尺寸参数,包括View的宽高以及测量模式。
它的高2位代表测量模式(通过mode & MODE_MASK计算),低30位代表尺寸。其中测量模式总共有3中。
- UNSPECIFIED:未指定模式不对子View的尺寸进行限制。
- AT_MOST:最大模式对应于wrap_content属性,父容器已经确定子View的大小,并且子View不能大于这个值。
- EXACTLY:精确模式对应于match_parent属性和具体的数值,子View可以达到父容器指定大小的值。
对于每一个View,都会有一个MeasureSpec属性来保存View的尺寸规格信息。在View测量的时候,通过makeMeasureSpec来保存宽高信息,通过getMode获取测量模式,通过getSize获取宽或高。
MeasureSpec是如何产生的
MeasureSpec相当于View测量过程中的一个规格,在View开始测量前需要先生成MeasureSpec来指导View以何种方式测量。
MeasureSpec生成是由父布局决定的,同时对于顶级ViewDecorView来说是由LayoutParams决定的。
在上面分析View工作流程开始的时候,在ViewRootImpl中开始工作流程前,有一个方法measureHierarchy(),这个方法就是生成DecorView的方式。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
...
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
}
...
}
在代码中可以看到通过getRootMeasureSpec()方法获取了DecorView的MeasureSpec。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
getRootMeasureSpec()也不复杂,在方法中可以看出如果是LayoutParams.MATCH_PARENT,那么DecorView的大小就是Window的大小;如果是LayoutParams.WRAP_CONTENT,那么DecorView的大小不确定。
对于普通的View,MeasureSpec来自于父布局(ViewGroup)生成。
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);
}
在这里可以看到生成子View的MeasureSpec时与父布局的MeasureSpec以及padding相关,同时也与View本身的margin有关。
MeasureSpec中UNSPECIFIED的用途
UNSPECIFIED主要在一线父View不限制子View宽高的情况下使用,比如ScrollView
1.UNSPECIFIED会在ScrollView的measure方法里传给子View
2.子View收到UNSPECIFIED,会根据自己的实际内容大小来决定高度
3.UNSPECIFIED与AT_MOST的区别就是,它没有最大size限定这也说明UNSPECIFIED在ScrollView里很实用,因为ScrllView不需要限定子View的大小,它可以滚动嘛
如何自定义FlowLayout
实现自定义View主要需要解决以下3个问题
1.自定义控件的大小,也就是宽和高分别设置多少;
2.如果是 ViewGroup,如何合理安排其内部子 View 的摆放位置。
3.如何根据相应的属性将 UI 元素绘制到界面;
以上 3 个问题依次在如下 3 个方法中得到解决:
onMeasure,onLayout,onDraw
FlowLayout的onMeasure方法
因为自定义的控件是一个容器,onMeasure 方法会更加复杂一些。因为 ViewGroup 在测量自己的宽高之前,需要先确定其内部子 View 的所占大小,然后才能确定自己的大小。
如下所示:
//测量控件的宽和高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获得宽高的测量模式和测量值
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获得容器中子View的个数
int childCount = getChildCount();
//记录每一行View的总宽度
int totalLineWidth = 0;
//记录每一行最高View的高度
int perLineMaxHeight = 0;
//记录当前ViewGroup的总高度
int totalHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//对子View进行测量
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
//获得子View的测量宽度
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//获得子View的测量高度
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (totalLineWidth + childWidth > widthSize) {
//统计总高度
totalHeight += perLineMaxHeight;
//开启新的一行
totalLineWidth = childWidth;
perLineMaxHeight = childHeight;
} else {
//记录每一行的总宽度
totalLineWidth += childWidth;
//比较每一行最高的View
perLineMaxHeight = Math.max(perLineMaxHeight, childHeight);
}
//当该View已是最后一个View时,将该行最大高度添加到totalHeight中
if (i == childCount - 1) {
totalHeight += perLineMaxHeight;
}
}
//如果高度的测量模式是EXACTLY,则高度用测量值,否则用计算出来的总高度(这时高度的设置为wrap_content)
heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight;
setMeasuredDimension(widthSize, heightSize);
}
上述 onMeasure 方法的主要目的有 2 个:
1.调用 measureChild 方法递归测量子 View;
2.通过叠加每一行的高度,计算出最终 FlowLayout 的最终高度 totalHeight。
FlowLayout的onLayout方法
上面的 FlowLayout 中的 onMeasure 方法只是计算出 ViewGroup 的最终显示宽高,但是并没有规定某一个子 View 应该显示在何处位置。要定义 ViewGroup 内部子 View 的显示规则,则需要复写并实现 onLayout 方法。
onLayout是一个抽象方法,也就是说每一个自定义 ViewGroup 都必须主动实现如何排布子 View,具体就是遍历每一个子 View,调用 child.(l, t, r, b) 方法来为每个子 View 设置具体的布局位置。四个参数分别代表左上右下的坐标位置,一个简易的 FlowLayout 实现如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mPerLineMaxHeight.clear();
//存放每一行的子View
List<View> lineViews = new ArrayList<>();
//记录每一行已存放View的总宽度
int totalLineWidth = 0;
//记录每一行最高View的高度
int lineMaxHeight = 0;
/****遍历所有View,将View添加到List<List<View>>集合中**********/
//获得子View的总个数
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (totalLineWidth + childWidth > getWidth()) {
mAllViews.add(lineViews);
mPerLineMaxHeight.add(lineMaxHeight);
//开启新的一行
totalLineWidth = 0;
lineMaxHeight = 0;
lineViews = new ArrayList<>();
}
totalLineWidth += childWidth;
lineViews.add(childView);
lineMaxHeight = Math.max(lineMaxHeight, childHeight);
}
//单独处理最后一行
mAllViews.add(lineViews);
mPerLineMaxHeight.add(lineMaxHeight);
/************遍历集合中的所有View并显示出来************/
//表示一个View和父容器左边的距离
int mLeft = 0;
//表示View和父容器顶部的距离
int mTop = 0;
for (int i = 0; i < mAllViews.size(); i++) {
//获得每一行的所有View
lineViews = mAllViews.get(i);
lineMaxHeight = mPerLineMaxHeight.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View childView = lineViews.get(j);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int leftChild = mLeft + lp.leftMargin;
int topChild = mTop + lp.topMargin;
int rightChild = leftChild + childView.getMeasuredWidth();
int bottomChild = topChild + childView.getMeasuredHeight();
//四个参数分别表示View的左上角和右下角
childView.layout(leftChild, topChild, rightChild, bottomChild);
mLeft += lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin;
}
mLeft = 0;
mTop += lineMaxHeight;
}
}
一道滴滴面试题
之前在面试滴滴时碰到了这样一首题目,这个问题如果你如果理解了,相信你已经充分掌握了自定义View的measure过程
Activity内根布局LinearLayout,背景颜色为红色,宽高为wrap_content
内部包含View背影颜色为蓝色,宽高也为wrap_content
求界面颜色
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/red"
xmlns:android="http://schemas.android.com/apk/res/android">
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/blue"
/>
</LinearLayout>
答案是蓝色
在下当时想当然的认为,既然都是wrap_content,界面颜色应该是白色。但是正确答案是蓝色
下面就来分析下具体原因
LinearLayout的onMeasure()
onMeasure()中比较简单,但是这里我们需要明确一下,这个方法的参数是什么含义:
MeasureSpec就不用多说了,记录当前View的尺寸和测量模式
另外明确一点,这里的MeasureSpec是父View的
/**
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这里咱们就选measureVertical()追进去,方法里的边界条件非常的多,但其中对于子View的测量过程比较的简单,遍历所有的子View,挨个调用measureChildBeforeLayout()方法,而这个方法最终会走到ViewGroup中的measureChildWithMargins():
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 这个方法主要就是做了一件事情:通过子View的LayoutParams和父View的MeasureSpec来决定子View的MeasureSpec
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);
}
生成子View的MeasureSpec
这部分逻辑主要在getChildMeasureSpec()方法中,我们直接追进去就好了:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 省略部分初始化代码
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这部分代码,就是Google定的规则,也没什么好说的。总结起来就是《Android开发艺术探索》中的那张图:
看了这个,咱们就可以思考一下咱们开篇遇到的问题:父View(LinearLayout)是wrap_content,子View是wrap_parent,那么子View的MeasureSpec是什么样子?
有了上边的分析,我们很容易得出答案:parentSize + AT_MOST。因此咱们就知道这种场景下,子View的wrap_parent意味自己的宽高就是父View的宽高。那么此时父View的宽高是多少呢?
由于这里的父View已经是根View了,那么它的外边便是DecorView,而DecorView的MeasureSpec相对简单些,直接基于Window的宽高和自身的LayoutParams进行计算。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
因此这种场景下,DecorView的MeasureSpec是屏幕宽高 + EXACTLY,那么父View(LinearLayout)的宽高就很明确了:parentSize + AT_MOST。
1.子View(TextView)的MeasureSpec是parentSize + AT_MOST
2.父View(LinearLayout)的MeasureSpec是parentSize + AT_MOST
3.DecorView的MeasureSpec是屏幕的size + EXACTLY
执行子View的measure()方法
接下来咱们去看一看子View的measure()方法,
上述的部分我们已经知道measureChildWithMargins()方法中会基于父View的MeasureSpec和子View的LayoutParams计算子View的MeasureSpec,
然后调用子View的measure():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从代码中可以看出,View的宽度即为父View传过来的宽高,即屏幕宽高。
因此最终效果为全屏显示蓝色。
小编暂且就写到这了,后续还会更新更多干货请关注我哦!!!
相关学习资料
Android 核心知识点之自定义ViewAndroid 知识点大全之自定义View
想要获取这份相关学习资料或更多Android 进阶资料,请点击【Github地址】查看获取方式!
网友评论