博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
高级UI-UI绘制流程
阅读量:5281 次
发布时间:2019-06-14

本文共 16786 字,大约阅读时间需要 55 分钟。

UI的绘制流程和事件分发,属于Android里面的重点内容,在做自定义UI的时候,更是应该了解UI的绘制流程是如何的,此篇文章就是说明UI的绘制流程,事件分发前面已经详细讲过了

UI绘制流程探索

这里分析Activity,而不是AppCompatActivity,后者做了兼容处理,前者更容易理清逻辑

要知道UI的绘制流程,就需要有一个入手点,而这个入手点就是onCreate(),也就是下面这句代码

setContentView(R.layout.activity_main);

接下来找到了这个方法

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID); initWindowDecorActionBar();}

这里先看``,有下面的源代码发现,这里只是设置了ActionBar,并没有UI的绘制

private void initWindowDecorActionBar() {
Window window = getWindow(); // Initializing the window decor can change window feature flags. // Make sure that we have the correct set before performing the test below. window.getDecorView(); if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
return; } mActionBar = new WindowDecorActionBar(this); mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp); mWindow.setDefaultIcon(mActivityInfo.getIconResource()); mWindow.setDefaultLogo(mActivityInfo.getLogoResource());}

那么其关注的重点就放在了第一个设置上了

getWindow()返回的是mWindow,仔细查找发现,mWindow实际上就是一个Windows的实现类:PhoneWindow

mWindow = new PhoneWindow(this, window);

那么也就是说要在PhoneWindow中寻找setContentView()方法

PhoneWindowInternal API,只能通过源代码查看(com.android.internal.policy)
在源代码中找到了相关代码

@Overridepublic void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) {
installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else {
mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) {
cb.onContentChanged(); } mContentParentExplicitlySet = true;}

这里的关注点在于installDecor()方法,这个方法

private void installDecor() {
mForceDecorInstall = false; if (mDecor == null) {
mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } ···}

这里又指向了generateDecor(),继续查看

protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity. Context context; if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext(); if (applicationContext == null) {
context = getContext(); } else {
context = new DecorContext(applicationContext, getContext().getResources()); if (mTheme != -1) {
context.setTheme(mTheme); } } } else {
context = getContext(); } return new DecorView(context, featureId, this, getAttributes());}

这里返回了一个DecorView,那么DecorView是什么呢,他是一个类继承了FrameLayout

/** @hide */public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
···}

那么回到installDecor(),在下面有这样的语句

private void installDecor() {
mForceDecorInstall = false; if (mDecor == null) {
mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else {
mDecor.setWindow(this); } if (mContentParent == null) {
mContentParent = generateLayout(mDecor); ··· } ···}

而这里的generateLayout()有一句注释

protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme. ··· mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);}

其作用是用来设置当前的主题数据,其中的上面一段代码很重要,在这个方法里面,渲染了一个根布局

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId(); if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded(); mBackdropFrameRenderer.onResourcesLoaded( this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable, mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState), getCurrentColor(mNavigationColorViewState)); } mDecorCaptionView = createDecorCaptionView(inflater); final View root = inflater.inflate(layoutResource, null); if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); } else {
// Put it below the color views. addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mContentRoot = (ViewGroup) root; initializeElevation();}

在根布局里面才是我们的用户布局,那么可以得出整个屏幕的层次结构应该是这样的

UI绘制流程-Activity加载显示基本流程
那么再回到setContentView(),下面有一段重要代码

mLayoutInflater.inflate(layoutResID, mContentParent);

这便是通过资源ID去寻找布局,然后进行用户布局的渲染

用户布局绘制

上面介绍完了外层布局的绘制,那么在用户布局这里是怎么进行绘制的呢,其绘制方法在View.java

向上面一样,这里首先要寻找的也是其布局绘制的入口,而这个绘制入口就是requestLayout()

@CallSuperpublic void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it, // not the views in its parent hierarchy ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return; } } mAttachInfo.mViewRequestingLayout = this; } mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null; }}

这里会不断地递归寻找父容器,直到找到DecorView

而在这之后,会执行到ViewRootImp类的performTranversal()方法
最后在performTranversal()方法中,会调用performMeasure(),performLayout()performDraw(),而这三个方法又会进一步调用,最终会调用到View的measure(测量),layout(摆放)draw(绘制)
其控制过程如下图
UI绘制流程-performTranversal方法控制

measure的测量过程

测量过程如下图

UI绘制流程-测量过程
因为在Android中,其屏幕规格尺寸千差万别,为了做自适应,就需要测量,measure过程会使用先序遍历遍历整颗View树,然后依次测量每一个View的真实的尺寸

在measure当中,有一个很重要的参数:MeasureSpec(测量规格)

这个参数是用int表示的,其前两位代表mode,后三十位代表值
mode:EXACTLY(精确值)AT_MOST(最值)UNSPECIFIED(不确定值)
value:宽高的值

经过大量测量以后,最终确定了自己的宽高,才会调用setMeasuredDimension(w,h),从而真正确定宽高值,只有这时候调用宽高获取,其获取到的值才是有效的

写自定义控件的时候,必须先经过measure,才能获得到宽高,不是getWidth(),而是getMeasuredWidth(),也就是当我们重写onMeasure()的时候,我们需要在里面调用child.measure()才能获取child的宽高

从规格当中获取mode和value

int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);

反过来将mode和value合成一个规格

MeasureSpec.makeMeasureSpec(resultSize, resultMode);

自定义控件的测量问题

  • 继承自View的子类
    只需要重写onMeasure测量好自己的宽高就可以了
    最终调用setMeasuredDimension()保存好自己的测量宽高。
int mode = MeasureSpec.getMode(widthMeasureSpec);int size = MeasureSpec.getSize(widthMeasureSpec);int viewSize = 0;switch(mode){
case MeasureSpec.EXACTLY: viewSize = size;//当前view的尺寸就为父容器的尺寸 break; case MeasureSpec.AT_MOST: viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。 break; case MeasureSpec.UNSPECIFIED: viewSize = getContentSize();//内容有多大,久设置多大尺寸。 break; default: break; } //setMeasuredDimension(width, height); setMeasuredDimension(size);
  • 继承自ViewGroup的子类:
    不但需要重写onMeasure测量自己,还要测量子控件的规格大小
    可以直接使用ViewGroup的工具方法来测量里面的子控件,也可以自己来实现这一套子控件的测量(比如:RelativeLayout)
//1.测量自己的尺寸ViewGroup.onMeasure();//1.1 为每一个child计算测量规格信息(MeasureSpec)ViewGroup.getChildMeasureSpec();//1.2 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸child.measure();//1.3 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了child.getChildMeasuredSize();//child.getMeasuredWidth()和child.getMeasuredHeight()//1.4ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸ViewGroup.calculateSelfSize();//2.保存自己的尺寸ViewGroup.setMeasuredDimension(size);

理解实例

自定义ViewGroup

public class CustomView extends ViewGroup {
private static final int OFFSET = 80; public CustomView(Context context, AttributeSet attrs) {
super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0; int right = 0; int top = 0; int bottom = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) {
View child = getChildAt(i); left = i * OFFSET; right = left + child.getMeasuredWidth(); bottom = top + child.getMeasuredHeight(); child.layout(left, top, right, bottom); top += child.getMeasuredHeight(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) {
View child = getChildAt(i); LayoutParams lp = child.getLayoutParams(); int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width); int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height); child.measure(childWidthSpec, childHeightSpec); } switch (widthMode) {
case MeasureSpec.EXACTLY: width = widthSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) {
View child = getChildAt(i); int widthAndOffset = i * OFFSET + child.getMeasuredWidth(); width = Math.max(width, widthAndOffset); } 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); }}

布局

效果

UI绘制流程-自定义ViewGroup

layout摆放过程

layout摆放子空间,其过程和measure十分类似,这里就不赘述了

其摆放位置只关注父容器,父容器又专注父容器···
一般不会重写layout方法,不然就需要专注分发问题
附上layout的过程
UI绘制流程-摆放过程

draw绘制过程

draw用于绘制,在这个方法里面,有一段简明的注释

/** Draw traversal performs several drawing steps which must be executed* in the appropriate order:**      1. Draw the background*      2. If necessary, save the canvas' layers to prepare for fading*      3. Draw view's content*      4. Draw children*      5. If necessary, draw the fading edges and restore layers*      6. Draw decorations (scrollbars for instance)*/

这段注释就说明了绘制的流程,其过程也类似于measure与layout

ViewGroup的onDraw方法默认是不会调用的,因为在ViewGroup构造方法里面就默认设置了

setFlags(WILL_NOT_DRAW, DRAW_MASK);

原因是因为ViewGroup本来就没东西显示,除了设置了背景,这样就是为了效率,如果需要它执行onDraw,可以设置背景或者setWillNotDraw(false);

自定义ViewGroup实现热门标签

自定义ViewGroup

public class HotTagLayout extends ViewGroup {
//记录每一行的高度 private List
lineHeights = new ArrayList<>(); private List
> views = new ArrayList<>(); public HotTagLayout(Context context, AttributeSet attrs) {
super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
views.clear(); lineHeights.clear(); //该行有多少个列数据 List
lineViews = new ArrayList<>(); //1.计算 int width = getMeasuredWidth();//容器宽 int lineWidth = 0; int lineHeight = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) {
View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width) {
//换行 lineHeights.add(lineHeight); views.add(lineViews); lineWidth = 0; lineViews = new ArrayList<>(); } lineWidth += childWidth + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin); lineViews.add(child); } lineHeights.add(lineHeight); views.add(lineViews); //2.摆放 int left = 0; int top = 0; int size = views.size(); for (int i = 0; i < size; i++) {
lineViews = views.get(i); lineHeight = lineHeights.get(i); for (int j = 0; j < lineViews.size(); j++) {
//遍历一行所有child View child = lineViews.get(j); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc = lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); child.layout(lc, tc, rc, bc); left += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; } left = 0; top += lineHeight; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); int width = 0; //所有行最宽的一行 int height = 0; //所有行的高度相加 int lineWidth = 0; int lineHeight = 0; //1.测量所有子控件大小 int childCount = getChildCount(); for (int i = 0; i < childCount; i++) {
View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //计算子控件真实占用的宽高 int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //当一行放不下时候换行 if (lineWidth + childWidth > sizeWidth) {
width = Math.max(lineWidth, width); lineWidth = childWidth; height += lineHeight; lineHeight = childHeight; } else {
lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); } //最后得到宽高 if (i == childCount - 1) {
width = Math.max(width, lineWidth); height += lineHeight; } } //2.测量并定义自身大小 int measuredWidth = (modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : width; int measuredHeight = (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height; setMeasuredDimension(measuredWidth, measuredHeight); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs); }}

布局

背景

效果

UI绘制流程-热门标签

转载于:https://www.cnblogs.com/cj5785/p/10664579.html

你可能感兴趣的文章
hdu 2807 The Shortest Path 矩阵
查看>>
熟悉项目需求,要知道产品增删修改了哪些内容,才会更快更准确的在该项目入手。...
查看>>
JavaScript 变量
查看>>
java实用类
查看>>
smarty模板自定义变量
查看>>
研究称90%的癌症由非健康生活习惯导致
查看>>
命令行启动Win7系统操作部分功能
查看>>
排序sort (一)
查看>>
Parrot虚拟机
查看>>
Teamcenter10 step-by-step installation in Linux env-Oracle Server Patch
查看>>
Struts2学习(三)
查看>>
Callable和Runnable和FutureTask
查看>>
GitHub 多人协作开发 三种方式:
查看>>
文本域添加编辑器
查看>>
Yum安装MySQL以及相关目录路径和修改目录
查看>>
java获取hostIp和hostName
查看>>
关于web服务器和数据库的各种说法(搜集到的)
查看>>
C# Stream 和 byte[] 之间的转换
查看>>
OMG: daily scrum nine
查看>>
redis与spring结合错误情况
查看>>