安卓view绘制_appui界面设计图

Android (3) 2024-09-10 21:23

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
安卓view绘制_appui界面设计图,希望能够帮助你!!!。

Android中View体系概览

在Android应用中所有的用户界面元素都是由View和ViewGroup对象构建而成。View对象用于绘制屏幕中可以和用户交互的内容。ViewGroup用于存储其他View对象和ViewGroup对象,从而构成界面的布局。

就如图中所示,用户界面的布局就是通过View和ViewGroup的结构层次定义的。View和ViewGroup组成了用户界面的View树,在View树中可以清晰的展示View的层次关系。

安卓view绘制_appui界面设计图_https://bianchenghao6.com/blog_Android_第1张

View发生绘制的时机

在Android应用中,View的第一次绘制是伴随这个Activity启动开始的。当Activity生命周期执行到onCreate时,我们都知道这时候会调用setContentView方法。View的绘制就是从这里开始。

除此之外,当View树中的视图发生变化时,会开始View的绘制;或者主动调用View的绘制方法,比如invalidate方法。这个都会发起View的绘制。

View的工作流程

View的工作流程指的就是View的measure、layout以及draw。其中measure是测量View的大小,layout是确定确定View的位置,draw是绘制View的内容。

这里将从setContentView开始分析View的工作流程。在setContentView函数中我们可以看到Activity通过getWindow方法加载View。这个getWindow是返回当前的Window对象。

这里先分析下Window对象怎么来的,以便更加清晰的理解整个View的加载过程。我们知道ActivityThread作为Android主线程的启动类,它承担了Activity的生命周期管理。当一个Activity开始工作时,会执行Activity的生命周期函数。这个里我们看到在performLaunchActivity方法中调用了Activity的attatch方法。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ...... activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); // ....... 此处代码省略 } 复制代码

在Activity的attach方法中创建了Window对象。我们可以看到这个Window对象时PhoneWindow类实例化的。

final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback) { attachBaseContext(context); mWindow = new PhoneWindow(this, window, activityConfigCallback); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); // ....... 此处代码省略 } 复制代码

View工作流程开始

通过在Activity中调用setContentView方法开始View的加载。这个过程是通过Window对象加载的。我们可以再PhoneWindow中找到这个方法。在这个方法中可以看到一个installDecor的方法。这个方法的作用就是初始化顶级View,也就是DecorView(这里不再介绍DecorView的创建过程,想了解的同学可以自行阅读代码)。之后View的工作流程就是从DecorView开始的,这个后面再讲。

public 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(); //创建顶级View(DecorView) } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } //......此处省略部分代码 } 复制代码

到这里只是先将顶级View初始化,还没有开始View的绘制。接着往下看,ActivityThread继续执行Activity的生命周期。在ActivityThread执行到handleResumeActivity方法时,这里调用了Activity的生命周期函数onResume。接着在通过WindowManager添加了DecorView,然后才开始了View的工作流程。这里也解释了为什么在Activity在执行完onResume的时候用户才可以跟App交互。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { //......此处省略部分代码 r = performResumeActivity(token, clearHide, reason); //调用Activity的onResume方法 if (r != null) { final Activity a = r.activity; if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); //获取Window对象 View decor = r.window.getDecorView(); //获取DecorView对象 decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); //获取WindowManager WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (r.mPreserveWindow) { a.mWindowAdded = true; r.mPreserveWindow = false; ViewRootImpl impl = decor.getViewRootImpl(); //获取ViewRootImpl对象 if (impl != null) { impl.notifyChildRebuilt(); } } if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); //添加DecorView } else { a.onWindowAttributesChanged(l); } } } else if (!willBeVisible) { r.hideForNow = true; } //......此处省略部分代码 } 复制代码

在上面的代码中DecorView添加进了WindowManager中。这个WindowManager的实现类是WindowManagerImpl。接下来看下WindowManagerImpl的addView实现。

@Override public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyDefaultToken(params); //private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); //mGlobal是WindowManagerGlobal单例类 mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); } 复制代码

在上面代码中可以看到,WinowManager最终通过WindowManagerGlobal添加了DecorView。

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { //......此处省略部分代码 final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params; ViewRootImpl root; //实现View和WindowManager所需要的协议 View panelParentView = null; synchronized (mLock) { //... root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); //通过ViewRootImpl添加DecorView } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. if (index >= 0) { removeViewLocked(index, true); } throw e; } } } 复制代码

在上面的代码中创建了ViewRootImpl的实例,ViewRootImpl的作用就是沟通View和WindowManager,实现两者所需要的协议,它管理着View的工作流程。所以到这一步,决定着View的measure、layout、draw的流程还没开始。继续往下看。

private void performTraversals() { //.......此处省略部分代码 if (!mStopped || mReportNextDraw) { int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //measure view } //... if (didLayout) { performLayout(lp, mWidth, mHeight); //layout view } if (!cancelDraw && !newSurface) { if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); } mPendingTransitions.clear(); } performDraw(); //draw 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的作用

在分析View的measure流程之前,先来看一个View的内部类MeasureSpec。这个类封装了View的规格尺寸参数,包括View的宽高以及测量模式。

public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; //未指定模式 public static final int UNSPECIFIED = 0 << MODE_SHIFT; //最大模式 public static final int EXACTLY = 1 << MODE_SHIFT; //精确模式 public static final int AT_MOST = 2 << MODE_SHIFT; @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } //...... } 复制代码

从MeasureSpec中可以看到,它的高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有关。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } 复制代码

在这个方法中可以看到子View的MeasureSpec生成时,首先判断父布局MeasureSpec的测量模式,然后在根据子View的尺寸数据决定子View的大小。

measure流程

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; //提取宽度信息 widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); //提取高度信息 heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); //view中的标志位计算 final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // 执行真正的测量工作 onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } //...... mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; } 复制代码

从上面的代码中,可以看到View的measure流程,首先是提取View的宽高信息,然后计算View的标志位,最后完成View的测量。

measure方法是final类型的,所以子类无法重写。真正的测量工作在onMeasure中完成,这也是所有子类需要重写的方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } 复制代码

可以看到onMeasure方法很简单,它内部有调用了setMeasureDimension方法。同时参数是由getDefaultSize方法获取的。接下来我们分别分析。

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; } protected int getSuggestedMinimumWidth() { //如果背景不为null,返回最小尺寸和背景最小尺寸的最大值;否则返回最小尺寸 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } 复制代码

可以看到,getDefaultSize返回View的默认大小。如果是UNSPECIFIED(未指定模式)就返回提供的大小(由getSuggestedMinimumWidth方法返回)。否则返回经过测量的大小。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); } private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; //对当前宽度属性赋值 mMeasuredHeight = measuredHeight; //对当前高度属性赋值 mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; } 复制代码

在上面的代码中,可以看到最终将测量后的大小赋值给View中的尺寸属性。

以上是View的measure流程。ViewGroup的measure与View不同,主要差别是ViewGroup需要递归的测试子View的尺寸,然后再测量自身。这里不再展开分析,想要了解的同学可以自行看源码。

View的layout流程

View的layout就是要确定自身的位置。位置是由left、top、right、bottom四个位置参数决定。它们代表View从左、上、右、下相对于父容器的距离。

public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //这里确定View的位置 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); //用于子View确定位置 //........ } 复制代码

从上面的代码可以看到,在layout方法中通过setFrame或者setOptionFrame确定位置。setOptionFrame最终会调用setFrame。这里看下setFrame方法,在setFrame方法中传入了4个参数l、t、r、b。这四个参数就是View中mLeft、mTop、mRight、mBottom的值。通过这四个值确定了View在父容器中的位置。

protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; if (DBG) { Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bit int drawn = mPrivateFlags & PFLAG_DRAWN; int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); mPrivateFlags |= PFLAG_HAS_BOUNDS; if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } //...... return changed; } 复制代码

在执行完setFrame方法后,会继续调用有个onLayout的方法,这个方法时一个空方法,和onMeasure类似。onLayout通常被View的子类重写。比如ViewGroup中会通过onLayout来遍历子View从而确定子View的位置。ViewGroup的layout过程这里也不再展开。

View的draw流程

View的工作流程最后一步就是绘制View的内容。通过调用draw方法实现。View的绘制流程可以概括为以下几步:

  1. 如果需要,则绘制背景;
  2. 保存当前canvas层;
  3. 绘制View的内容;
  4. 绘制子View,重写dispatchDraw方法;
  5. 如果需要绘制View的褪色边缘,类似于阴影效果;
  6. 绘制装饰,比如滚动条。调用onDrawForeground方法;

同样的在View的draw流程中,有回调方法来实现绘制子View。就是dispatchDraw方法。这个方法在ViewGroup中实现来绘制子View。

总结

以上我们通过Activity启动时来看View的整个工作流程。只从View的角度来看的话,View的工作流程主要分为测量(measure)、布局(layout)、绘制(draw)。一个View只有经过了这三步才能正确的展示在用户面前。

通过分析View的工作流程,可以更加清楚View在应用程序中运行原理,而且对于我们使用View也有帮助,比如说:自定义View。

作者:Codeing_ls
链接:https://juejin.im/post/

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

发表回复