本文主要关注View的测量、布局、绘制三个步骤,讨论这三个步骤的执行流程。本文暂不涉及View和Window之间的交互以及Window的管理。再论述完这三个步骤以后,文末以自定义TagGroup为例,讲述如何自定义ViewGroup。java
View树的绘图流程是由核心类:ViewRootImpl 来处理的,ViewRootImpl做为整个控件树的根部,它是控件树正常运做的动力所在,控件的测量、布局、绘制以及输入事件的派发处理都由ViewRootImpl触发。android
这里我主要讲几个Handler:git
这是ViewRootImpl调度的核心,其处理的消息事件主要有: MSG_INVALIDATE、MSG_INVALIDATE_RECT、MSG_RESIZED、MSG_DISPATCH_INPUT_EVENT、MSG_CHECK_FOCUS、MSG_DISPATCH_DRAG_EVENT、MSG_CLOSE_SYSTEM_DIALOGS、MSG_UPDATE_CONFIGURATION等github
主要有如下几类:View绘制相关、输入焦点等用户交互相关、系统通知相关。bash
有经验的同窗确定遇到过这样的场景:动态建立一个View以后,想要直接获取measureWidth 和 measureHeight每每取不到,这个时候咱们会经过view.postDelayed()方法去获取。那么,问题来了,为何这样就能取到呢?app
答案就在ViewRootImpl中的ViewRootHandler,view.post--> attachInfo.mHandler.post --> ViewRootImpl ViewRootHandler. 这个Handler保证了当你post的runable被执行到时,view早就测量好了。ide
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
复制代码
Choreographer这个类来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI操做,这里不得不提一下Choreographer.FrameHandler目的就在于ViewRootImpl中涉及到到的View绘制流程,是经过Choreographer.FrameHandler来进行调度的。具体的调度过程以下:函数
一、 ViewRootImpl.scheduleTraversalsoop
这个方法会往Choreographer注册类型为Choreographer.CALLBACK_TRAVERSAL的Callback。源码分析
// ViewRootImpl.scheduleTraversals 注册callback
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
复制代码
二、 Choreographer.FrameHandler
Choreographer.FrameHandler源码以下,主要处理三个信号: MSG_DO_FRAME:开始渲染下一帧的操做 MSG_DO_SCHEDULE_VSYNC:请求Vsync信号 MSG_DO_SCHEDULE_CALLBACK:请求执行callback
对于这三个信号,Choreographer是有一个调度过程的,最终callback的回调执行都是落实到doFrame()方法上面的。
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}
复制代码
doFrame执行回调有一个顺序的,顺序依次以下: Choreographer.CALLBACK_INPUT Choreographer.CALLBACK_ANIMATION Choreographer.CALLBACK_TRAVERSAL Choreographer.CALLBACK_COMMIT
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
复制代码
关于Choreographer,读者能够参考下这篇文章,讲的很是详细:Android Choreographer 源码分析
这里简单说一个小窍门,经过Choreographer.getInstance().postFrameCallback() 注册回调,并计算先后两帧的时间差,咱们能够测算出APP的掉帧数,从而动态检测APP 卡顿。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
long currentFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
}
currentFrameTimeNanos = frameTimeNanos;
long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS);
long droppedCount = 0;
if (diffMs > 100) {
droppedCount = (int) (diffMs / 16.6);
String anrLog = collectAnrLog(applicationContext);
DjLog.e("Block occur, droppedCount: " + droppedCount + ", anrLog: " + anrLog);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
复制代码
整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所作 的工做可简单概况为是否须要从新计算视图大小(measure)、是否须要从新安置视图的位置(layout)、以及是否须要重绘(draw),流程图以下:
更详细的图示以下:
performTraversals 方法很是庞大,整个源码在800行左右,看起来会让人吐血。这个方法主要的过程有四个:
预测量阶段 这是进入performTraversals()方法后的第一个阶段,它会对控件树进行第一次测量。测量结果能够经过mView. getMeasuredWidth()/Height()得到。在此阶段中将会计算出控件树为显示其内容所需的尺寸,即指望的窗口尺寸。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次获得回调。
布局窗口阶段 根据预测量的结果,经过IWindowSession.relayout()方法向WMS请求调整窗口的尺寸等属性,这将引起WMS对窗口进行从新布局,并将布局结果返回给ViewRootImpl。
最终测量阶段 预测量的结果是控件树所指望的窗口尺寸。然而因为在WMS中影响窗口布局的因素不少(参考第4章),WMS不必定会将窗口准确地布局为控件树所要求的尺寸,而迫于WMS做为系统服务的强势地位,控件树不得不接受WMS的布局结果。所以在这一阶段,performTraversals()将以窗口的实际尺寸对控件进行最终测量。在这个阶段中,View及其子类的onMeasure()方法将会沿着控件树依次被回调。
布局控件树阶段 完成最终测量以后即可以对控件树进行布局了。测量肯定的是控件的尺寸,而布局则是肯定控件的位置。在这个阶段中,View及其子类的onLayout()方法将会被回调。
绘制阶段 这是performTraversals()的最终阶段。肯定了控件的位置与尺寸后,即可以对控件树进行绘制了。在这个阶段中,View及其子类的onDraw()方法将会被回调。
那问题来了,这个方法何时会被触发,或者说Android系统何时会对整个View树进行一次全量的操做呢?从源码中,咱们能够看到如下几个核心的方法会触发:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void invalidate() {
...
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
复制代码
有几点注意: • invalidate/postInvalidate 只会触发 draw; • requestLayout,会触发 measure、layout 和 draw 的过程; • 它们都是走的 scheduleTraversals -> performTraversals,用不一样的标记位来进行区分; • resume 会触发 invalidate; • dispatchDraw 是用来绘制 child 的,发生在本身的 onDraw 以后,child 的 draw 以前 Measure 和 Layout 的具体过程
关于Measure过程,不得不详细提一下MeasureSpec。MeasureSpec是一个复合整型变量(32bit),用于指导控件对自身进行测量,它有两个份量:前两位表示SPEC_MODE,后30位表示SPEC_SIZE。SPEC_MODE的取值取决于此控件的LayoutParams.width/height的设置,SPEC_SIZE则是父视图给定的指导大小。
SPEC_MODE有三种模式,具体的计算以下:
MeasureSpec.UNSPECIFIED: 表示控件在进行测量时,能够无视SPEC_SIZE的值。控件能够是它所指望的任意尺寸。
MeasureSpec.EXACTLY: 表示子控件必须为SPEC_SIZE所制定的尺寸。当控件的LayoutParams.width/height为一肯定值,或者是MATCH_PARENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。
MeasureSpec.AT_MOST: 表示子控件能够是它所指望的尺寸,可是不得大于SPEC_SIZE。当控件的LayoutParams.width/height为WRAP_CONTENT时,对应的MeasureSpec参数会使用这个SPEC_MODE。
讲了这么多,下面咱们来实操一下。
需求:自定义一个TagGroup,用来显示一系列标签元素。要求标签样式彻底能够自定义,标签间距可在xml中指定,要有最多显示多少行的控制,显示不全时要展现“更多 ...”
在attrs.xml中协定样式:
<declare-styleable name="DjTagGroup">
<attr name="tag_horizontalSpacing" format="dimension" />
<attr name="tag_verticalSpacing" format="dimension" />
<attr name="max_row" format="integer"/>
</declare-styleable>
复制代码
协定接口,用来提供具体的标签元素:
public interface TagViewHolder {
View getView();
}
复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
int row = 0; // The row counter.
int rowWidth = 0; // Calc the current row width.
int rowMaxHeight = 0; // Calc the max tag height, in current row.
if (moreTagHolder != null) {
moreTagMeasureWidth = moreTagHolder.getView().getMeasuredWidth();
moreTagMeasureHeight = moreTagHolder.getView().getMeasuredHeight();
}
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
if (child.getVisibility() != GONE) {
// judge the max_row
if (row + 1 >= maxRow && rowWidth + childWidth > widthSize) {
break;
}
rowWidth += childWidth;
if (rowWidth > widthSize) { // Next line.
rowWidth = childWidth; // The next row width.
height += rowMaxHeight + verticalSpacing;
rowMaxHeight = childHeight; // The next row max height.
row++;
} else { // This line.
rowMaxHeight = Math.max(rowMaxHeight, childHeight);
}
rowWidth += horizontalSpacing;
}
}
// Account for the last row height.
height += rowMaxHeight;
// Account for the padding too.
height += getPaddingTop() + getPaddingBottom();
// If the tags grouped in one row, set the width to wrap the tags.
if (row == 0) {
width = rowWidth;
width += getPaddingLeft() + getPaddingRight();
} else {// If the tags grouped exceed one line, set the width to match the parent.
width = widthSize;
}
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
复制代码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int parentLeft = getPaddingLeft();
final int parentRight = r - l - getPaddingRight();
final int parentTop = getPaddingTop();
final int parentBottom = b - t - getPaddingBottom();
int childLeft = parentLeft;
int childTop = parentTop;
int row = 0;
int rowMaxHeight = 0;
boolean showMoreTag = false;
final int count = getChildCount();
int unTagCount = count;
if (moreTagHolder != null) {
unTagCount--;
}
for (int i = 0; i < unTagCount; i++) {
final View child = getChildAt(i);
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
if (child.getVisibility() != GONE) {
if (row + 1 >= maxRow && childLeft + width + (horizontalSpacing + moreTagMeasureWidth) > parentRight) {
// 预留一个空位放置moreTag
showMoreTag = true;
break;
}
if (childLeft + width > parentRight) { // Next line
childLeft = parentLeft;
childTop += rowMaxHeight + verticalSpacing;
rowMaxHeight = height;
row++;
} else {
rowMaxHeight = Math.max(rowMaxHeight, height);
}
// this is point
child.layout(childLeft, childTop, childLeft + width, childTop + height);
childLeft += width + horizontalSpacing;
}
}
if (showMoreTag) {
final View child = getChildAt(count - 1);
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
复制代码
在xml中直接引用
<com.xud.tag.DjTagGroup
android:id="@+id/dj_tag_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
app:tag_horizontalSpacing="8dp"
app:tag_verticalSpacing="8dp"
app:max_row="4"/>
复制代码
定义本身的TagViewHolder
public class DjTagViewHolder implements DjTagGroup.TagViewHolder {
public String content;
public View rootView;
public TextView tagView;
public DjTagViewHolder(View itemView, String content) {
this.rootView = itemView;
tagView = itemView.findViewById(R.id.tag);
tagView.setText(content);
tagView.setOnClickListener(v -> Toast.makeText(context, "点击了:" + content, Toast.LENGTH_SHORT).show());
}
@Override
public View getView() {
return rootView;
}
}
复制代码
往DjTagGroup直接设置tags
private void initDjTags() {
String[] tags = TagGenarator.generate(10, 6);
List<DjTagGroup.TagViewHolder> viewHolders = new ArrayList<>();
for (String tag: tags) {
DjTagViewHolder viewHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false),
tag);
viewHolders.add(viewHolder);
}
DjTagViewHolder moreHolder = new DjTagViewHolder(LayoutInflater.from(context).inflate(R.layout.view_tag, djTagGroup, false),
"更多 ...");
djTagGroup.setTags(viewHolders, moreHolder);
}
复制代码
实际的效果
源码地址: Github: 自定义View辑录DjCustomView
参考文章