-
Android View的绘制流程
2016-01-18 15:15:19Android View的绘制流程本篇文章主要是在学习《Android开发艺术探索》时做的一些笔记,主要是对知识的总结(绝大部分知识来自于《Android开发艺术探索》)。
概述
View的绘制流程是从ViewRoot的performTraversals方法开始,经过measure丶layout和draw三个过程才能最终将一个View绘制出来。而ViewRoot是连接WindowManager和DecorView(其实就是一个FramLayout,View层的事件都先经过DecorView,然后才传递给我们的View)的纽带,而View的三大流程均是通过ViewRoot完成的。其中,measure负责测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw负责将View绘制在屏幕上。
View的测量宽高和最终宽高有什么区别?
答:measure过程决定了View的宽/高,Measure完成后可以通过getMeasuredWidth和getMeasuredHeight方法获取到View的宽和高,基本上测量的宽高就是最终的宽高。
layout中主要决定了View的四个顶点的坐标和实际的宽/高,完成以后可以通过getTop,getLeft,getBottom和getRight或者顶点位置,通过getWidth和getHeight获取最终宽/高。如何实现整个View树的遍历?
答:View的绘制流程是从ViewRoot的performTraversals方法开始,performTraversals会依次调用performMeasure丶performLayout丶performDraw三个方法,这三个方法主要是完成顶级View的measure丶layout和draw这三大流程。其中在performMeasure中会调用measure方法,在measure方法中又会去调用onMeasure方法,onMeasure方法中会对所有的子元素进行测量,这样measure流程九层父元素转到了子元素,就完成了一次measure过程。接着子元素再重复此流程就完成了对整个View树的遍历。
测量过程中的MeasureSpec
什么是MeasureSpec?
MeasureSpec对于一个View的尺寸规格有很大影响,可以理解为一种测量规格。MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(某种测量模式下的规格大小)。
SpecMode主要有三类:
EXACTLY(精确模式):父容器已经检测出View所需的精确大小,View的最终大小就是SpecSize所指定的值。对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST(最大模式):父容器仅提供一个可用大小的SpecSize,只要求View的大小不能大于这个值,具体是什么值,要看View自己的具体实现。对应于LayoutParams中的wrap_content。
UNSPECIFIED:父容器不对View有任何限制,一般不用理会。MeasureSpec对测量View的宽/高有什么作用?
在测量View的过程中,系统就是根据MeasureSpec来测量的View的宽/高。系统会将View的LayoutParams根据父容器所施加的规格转换成自己所对应的MeasureSpec,有了MeasureSpec就可以测量该View的宽/高。也就是说View自身的LayoutParams和父容器的MeasureSpec一起决定了View的MeasureSpec(其实也与View本身的margin和padding有关),即决定了View的宽和高。一旦MeasureSpec确定了,在onMeasure中就可以确定View的宽和高。
View的绘制流程
View绘制的三大流程就是:measure丶layout和draw。measure确定View的测量宽/高,layout确定View的最终宽高和四个顶点的位置,draw则将View绘制在屏幕上。
measure
对于measure过程,View和ViewGroup的测量过程是有区别的,如果只是View,那么通过measure就完成了其测量过程;如果是ViewGroup,除了完成自己的测量过程外,还要遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个流程。
- View的measure过程
主要由measure方法完成,measure是final类型,不能被重写。但是在measure方法中调用了onMeasure方法,onMeasure是可以被重写的,所以可以在onMeasure中完成我们的一些逻辑代码。
View的onMeasure方法源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
可以看出在onMeasure方法中主要是调用了setMeasuredDimension设置了View的测量值,下面是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; }
getDefaultSize的返回值是specSize,而specSize就是View测量后的大小。这里之所以说是测量后的大小,是因为View的最终大小是在layout阶段才确定了,所以现在设置的大小仅仅是一个参考值,但是大部分情况下View的测量大小和最终大小都是一样的。
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此没有重写onMeasure方法,但是它提供了一个measureChild的方法,思想就是取出子元素的LayoutParams,然后通过getChildMeasureSpec来创建子元素的MeasureSpec对象,接着将MeasureSpec直接传给View的measure方法进行测量。
但是需要注意的是ViewGroup中并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,它的onMeasure方法需要各个子类去具体实现,比如LinearLayout和RelativeLayout的onMeasure是不同的,都需要自己去实现。
获取View的宽高
在有些时候,系统可能需要多次进行measure,才能确定View的最终宽高,所以在onMeasure中获取View的宽/高可能是不准确的。所以最好在onLayout中获取View的测量宽高或者最终宽高。
还有一种情况就是,比如在Activity中的onCreat丶onStart或者onResume中获取View的宽高,但是由于View的Measure过程不是同步的,无法保证在onCreat丶onStart或者onResume中View已经测量过了,如果还未测量过,那么我们获取的宽高就是0。
- MainActivity
package com.wangjian.wjmeasuredemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.EditText; public class MainActivity extends AppCompatActivity { private EditText text; private int width = -1; private int height = -1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (EditText) findViewById(R.id.et); width = text.getMeasuredWidth(); height = text.getMeasuredHeight(); text.setText("宽 = " + width+" ; 高 = "+ height); } }
很简单,就是在onCreat方法中试图获取EditText的测量的宽和高。
* activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <EditText android:id="@+id/et" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="文本宽高" /> </LinearLayout>
运行结果如下:
获取到的宽高是0。想解决这个问题,有以下四种方式:
(1)Activity和View中提供的onWindowFocusChanged方法
在onWindowFocusChanged方法中获取View的宽高时,View已经初始化完毕,可以获取到正常的宽高。但是onWindowFocusChanged方法在很多情况下都会被调用,那么肯定会导致调用多次,所以应该加一些判断条件对调用次数加以控制。
(2)view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
(3)ViewTreeObserver
使用ViewTreeObserver的众多回调方法可以完成这个功能,比如OnGlobalLayoutListener这个接口,当View树发生发辫或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽高比较好的时机。不过,伴随着View树的状态改变等,onGlobalLayout会被多次调用,所以应该在被调用一次时移除该监听。
对于上面三种方法比较简单,代码中有比较详细的使用方法:
- MainActivity
package com.wangjian.wjmeasuredemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.ViewTreeObserver; import android.widget.EditText; public class MainActivity extends AppCompatActivity { private EditText text; private int width = -1; private int height = -1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); text = (EditText) findViewById(R.id.et); getWidthAndHeight(); } /** * 获取视图的宽高 */ private void getWidthAndHeight() { width = text.getMeasuredWidth(); height = text.getMeasuredHeight(); text.setText("宽 = " + width+" ; 高 = "+ height); } /** * 获取视图宽高的第一种方式 * @param hasFocus */ @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus){ // getWidthAndHeight(); } } @Override protected void onStart() { super.onStart(); /** * 获取视图宽高的第二种方式,其实在onCreat中也是可以获取到的 */ text.post(new Runnable() { @Override public void run() { // getWidthAndHeight(); } }); /** * * 获取视图宽高的第三种方式 */ ViewTreeObserver observer = text.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @SuppressWarnings("deprecation") @Override public void onGlobalLayout() { text.getViewTreeObserver().removeOnGlobalLayoutListener(this); // getWidthAndHeight(); } }); } }
运行结果如下:
(4)手动调用view.measure(int widthMeasureSpec, int heightMeasureSpec)进行测量
该方法与View本身的LayoutParams有关,主要分为以下三种:
①match_parent
无法获取出具体的宽和高。要测量View的宽高,就要得到View的MeasureSpec,但是这种模式必须知道parentSize,即父容器的剩余空间。而此时我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
②具体的数值(比如宽/高都是100px)
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); text.measure(widthMeasureSpec,heightMeasureSpec);
在进行了测量之后我们就可以获取它的宽高了。
③wrap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST); text.measure(widthMeasureSpec,heightMeasureSpec);
layout
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置确定后,它在onLayout中会遍历所有的子元素并调用子元素的layout方法,在layout方法中onLayout方法又会被调用。即layout方法用来确定本身的位置,onLayout方法用来确定所有子元素的位置,onLayout方法和onMeasure方法一样,和具体的布局有关,所以都没有真正的实现,需要根据自己需求实现。
要确定一个View在父容器的位置,主要通过View中的四个参数,mLeft,mRight,mTop和mBottom。常见问题:
View的测量宽高和最终宽高有什么区别?
答:其实就是getWidth与getMeasuredWidth的区别。其实很简单,测量宽高是在measure过程赋值的,而最终宽高是在layout过程赋值的,时机不同。但是一般情况下,两者都是相等的。什么时候测量宽高与最终宽高不同?
答:主要有两种情况:一种是重写View的layout方法,手动修改数值。一种是有的View需要测量多次才能确定自己的测量宽高,前几次的测量结果可能与最终结果不一致,但最终来说,测量宽高和最终宽高还是相同的。
draw
绘制过程比较简单,主要遵循以下几步:
(1)绘制背景background.draw(canvas).
(2)绘制自己(onDraw)。
(3)绘制children(dispatchDraw)。
(4)绘制装饰(onDrawScrollBars)。
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层的传递下去了。 -
Android View 的绘制流程
2019-08-08 10:53:51Android View 的绘制流程 https://www.jianshu.com/p/c151efe22d0dAndroid View 的绘制流程
https://www.jianshu.com/p/c151efe22d0d -
android View的绘制流程
2019-03-06 14:39:50View的绘制流程 View的绘制主要指measure、layout、draw三大流程,即测量、布局和绘制。其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的位置,draw则是将view绘制在屏幕上。 一、mesure过程...View的绘制流程
View的绘制主要指measure、layout、draw三大流程,即测量、布局和绘制。其中measure确定view的测量宽高,layout确定view的最终宽高和四个顶点的位置,draw则是将view绘制在屏幕上。
一、measure过程
measure过程要分开来看,如果是单纯的原始view,那么通过measure就可以完成其测量过程。如果是一个ViewGroup,除了完成自身的测量过程之外,还会去遍历调用所有的子元素的measure方法,各个子元素再递归去执行这个流程。
1.View的measure过程
首先我们来看一下View的measure方法:
/** * <p> * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * </p> * * <p> * The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. * </p> * * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ 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); } 、、、、、、 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }
我们发现这个方法在View中定义的是final的,意味着无法被子类重写,那么View的子类如何进行自身测量?注释中给出了结果:
* <p> * The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. * </p>
实际的测量工作是在onMeasure方法中进行的,因此子类必须自己去重写这个方法。那么我们先看一下View的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
这个方法很简洁,setMeasuredDimension顾名思义就是去设置view的测量的宽高,说明测试方法是在getDefaultSize这个方法中已经完成的,那么我们直接看这个方法的代码以及getSuggestedMinimumWidth方法:
/** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no constraints. Will get larger if allowed * by the MeasureSpec. * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ 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() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
我们只需要关注specMode为AT_MOST和EXACTLY这两种情况。可以看到,onMeasure返回的值其实就是measureSpec中的specSize,而这个specSize其实就是view测量之后的大小。
这里需要注意的是这里的值是view测量之后的值,实际显示之后的大小还要看layout中的布局大小,当然一般来说测量值和实际值是相等的。
这里getSuggestedMinimumWidth方法只有在specMode=UNSPECIFIED时才会有意义,而这个情况比较少见,总的来说就是如果这个view没有设置背景,就将这个view的android:minWidth属性指定的值返回,如果没有指定默认为0,;如果view设置了背景,就将背景这个Drawable的大小和minWidth对比返回大的那个值。这个值就是在specMode=UNSPECIFIED情况下测量到的宽高值。从getDefaultSize方法的实现来看,如果我们自定义直接继承View的控件,就需要重写onMeasure方法并设置wrap-content时的宽高具体大小,否则在布局中wrap-content和match-parent就会有相同的表现,如何解决这个问题呢?需要以下这段代码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec); int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec); int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec); int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec); if (widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,mHeight); }else if(widthSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(mWidth,heightSpecSize); }else if (heightSpecMode==MeasureSpec.AT_MOST){ setMeasuredDimension(widthSpecSize,mHeight); } }
其中的mWidth和mHeight就是你需要自已定义的view的宽高。
2.ViewGroup的measure过程
ViewGroup不仅仅是要测量自身,还会遍历所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,他没有重写onMeasure方法,但是它提供了一个measureChildren方法
/** * Ask all of the children of this view to measure themselves, taking into * account both the MeasureSpec requirements for this view and its padding. * We skip children that are in the GONE state The heavy lifting is done in * getChildMeasureSpec. * * @param widthMeasureSpec The width requirements for this view * @param heightMeasureSpec The height requirements for this view */ protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
这个方法很简洁的表明,measureChildren内部会遍历子元素,然后调用measureChild方法对子元素进行测量,继续看:
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param parentHeightMeasureSpec The height requirements for this view */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
很显然,measureChild方法就是将子元素的LayoutParams 取出,然后通过getChildMeasureSpec创建出子元素的MeasureSpec,然后直接将MeasureSpec传递给View的measure方法进行测量。
ViewGroup作为一个抽象类,没有直接实现onMeasure方法,这就需要各个子类去根据不同的布局特性进行不同的测量细节,而ViewGroup无法统一,具体Layout可以具体分析。3.如何在Activity启动时获取到view的宽高
获取view宽高的方法很多,这边推荐两种方式进行:
1.根据Activity的生命周期,在onWindowFocusChanged方法中进行获取。//当activity获取到焦点时调用 //如果频繁切换activity的前后台,这个方法会多次调用 @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus){ //获取view的宽高 } }
2.通过view.post方法将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view已经初始化完毕。
view.post(new Runnable() { @Override public void run() { //这里获取宽高 } });
二、layout布局过程
Layout相对于Measure来说简单的多。
Layout的作用主要是用来确定子元素的位置,当ViewGroup的位置被确定之后,它在onLayout中会遍历所有子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。Layout方法确定自身的位置,onLayout方法确定所有子元素的位置。
下面是View的layout方法代码:(源码注释尽量多看)/** * Assign a size and position to a view and all of its * descendants * * <p>This is the second phase of the layout mechanism. * (The first is measuring). In this phase, each parent calls * layout on all of its children to position them. * This is typically done using the child measurements * that were stored in the measure pass().</p> * * <p>Derived classes should not override this method. * Derived classes with children should override * onLayout. In that method, they should * call layout on each of their children.</p> * * @param l Left position, relative to parent * @param t Top position, relative to parent * @param r Right position, relative to parent * @param b Bottom position, relative to parent */ @SuppressWarnings({"unchecked"}) 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; //这个地方调用setFrame方法确定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); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } final boolean wasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if (!wasLayoutValid && isFocused()) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; if (canTakeFocus()) { // We have a robust focus, so parents should no longer be wanting focus. clearParentsWantFocus(); } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) { // This is a weird case. Most-likely the user, rather than ViewRootImpl, called // layout. In this case, there's no guarantee that parent layouts will be evaluated // and thus the safest action is to clear focus here. clearFocusInternal(null, /* propagate */ true, /* refocus */ false); clearParentsWantFocus(); } else if (!hasParentWantsFocus()) { // original requestFocus was likely on this view directly, so just clear focus clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } // otherwise, we let parents handle re-assigning focus during their layout passes. } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; View focused = findFocus(); if (focused != null) { // Try to restore focus as close as possible to our starting focus. if (!restoreDefaultFocus() && !hasParentWantsFocus()) { // Give up and clear focus once we've reached the top-most parent which wants // focus. focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } } } if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
setFrame方法确定view的四个顶点位置之后,那么view在父容器中的位置也就确定了下来,接着就会调用onLayout方法区确定子元素的位置。但是查看View和ViewGroup中的onLayout方法,这个方法也是子类去重写的。
View的onLayout方法代码 /** * Called from layout when this view should * assign a size and position to each of its children. * * Derived classes with children should override * this method and call layout on each of * their children. * @param changed This is a new size or position for this view * @param left Left position, relative to parent * @param top Top position, relative to parent * @param right Right position, relative to parent * @param bottom Bottom position, relative to parent */ protected void onLayout(boolean changed, int left, int top, int right, int bottom) { } ViewGroup的onLayout方法代码 @Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
我们接下来以LinerLayout的onLayout方法进行简单分析:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } } /** * Position the children during a layout pass if the orientation of this * LinearLayout is set to {@link #VERTICAL}. * * @see #getOrientation() * @see #setOrientation(int) * @see #onLayout(boolean, int, int, int, int) * @param left * @param top * @param right * @param bottom */ void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; // Where right end of child should go final int width = right - left; int childRight = width - mPaddingRight; // Space available for child int childSpace = width - paddingLeft - mPaddingRight; final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; switch (majorGravity) { case Gravity.BOTTOM: // mTotalLength contains the padding already childTop = mPaddingTop + bottom - top - mTotalLength; break; // mTotalLength contains the padding already case Gravity.CENTER_VERTICAL: childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; break; case Gravity.TOP: default: childTop = mPaddingTop; break; } for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); int gravity = lp.gravity; if (gravity < 0) { gravity = minorGravity; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }
以vertical状态下的LinearLayout为例,可以看到此方法是遍历所有子元素,通过childTop这个属性的不断增大来确定每一个元素顶部坐标变大,位置往下不断摆放,最后通过调用setChildFrame方法来进行子元素的摆放。
private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }
子元素调用自身的layout方法,然后再通过onLayout来确定自己的位置,层层传递完成整个view的layout过程。
View的测量宽高和实际宽高的区别
测量宽高是通过measure得到的,而实际宽高还需要经过layout阶段进行最终的确定。
final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); 、、、、、、 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
以Linearlayout的onLayout方法为例,setChildFrame来设置子元素宽高其实获取的就是子元素测量之后的宽高,两种是相同的。那么看一下View的获取宽高的方法:
public final int getHeight() { return mBottom - mTop; } public final int getWidth() { return mRight - mLeft; }
可以看到,获取view的宽高其实是通过四个顶点的计算得出的,因此我们可以得出我们的结论:
在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于measure阶段,而最终宽高形成于layout阶段,即两者的赋值时机不同。因此我们在日常开发中,可以认为View的测量宽高等同于最终宽高。
当然也存在某些特殊情况,人为的去将View的坐标点进行更改,测量的宽高必然会与最终宽高不一致。比如重写layout代码:
@Override public void layout(int l, int t, int r, int b) { super.layout(l, t, r+100, b+100); }
另一方面,如果一个View的measure方法多次调用去测量才能确定自己的测量宽高,那么在前几次的测量过程中,得出的测量宽高和最终宽高也可能不一致,但是测量结束时得到的宽高和最终宽高还是相同的。
三、draw过程
Draw过程相对比较简单,它的作用是将view绘制到屏幕上面。
View的绘制过程遵循如下几步:
1.绘制背景:background.draw(canvas);
1.绘制自身:onDraw;
1.绘制children:dispatchDraw;
1.绘制装饰:onDrawScrollBars;
截取源码如下:/** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is * called. When implementing a view, implement * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */ @CallSuper public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * 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) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; } 、、、、、、 }
View的绘制流程是通过dispatchDraw方法进行分发,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层传递下去。
-
Android view的绘制流程
2018-10-31 11:09:28Android view绘制的三个流程 1、测量(Measure) 2、布局(Layout) 3、绘制(Draw) 1、测量(Measure) 三种测量模式: 1、UNSPECIFIED:不指定测量模式,可以是想要的任何大小 2、EXACTLY:精确测量模式,对应...Android view绘制的三个流程
1、测量(Measure)
2、布局(Layout)
3、绘制(Draw)1、测量(Measure)
三种测量模式:1、UNSPECIFIED:不指定测量模式,可以是想要的任何大小
2、EXACTLY:精确测量模式,对应指定数值的尺寸或者match_parent
3、AT_MOST:最大值模式,不超过父视图允许最大尺寸的任何尺寸,对应warp_content2、布局(Layout)
确定view在父容器中的布局
3、绘制(Draw)
六个步骤:1、绘制背景
drawBackground(canvas)
2、保存图层,为fading准备,可选
saveCount=canvas.getSaveCount
3、绘制view
onDraw(canvas)
4、绘制view的子view
diapachDraw(canvas)
5、绘制view的fading边缘并恢复涂层
canvas.restoreToCount(savrCount)
6、绘制view的装饰
onDrawScrollBars(canvas) -
android view的绘制流程
2018-04-11 16:41:55当一个应用启动的时候,会启动一个主activity,android系统会根据activity的布局来对它进行绘制。每个view负责绘制自己,而viewgroup还需要负责通知自己的子view进行绘制操作。视图绘制的过程可以分为三个步骤,分别...
-
【2021】UI自动化测试框架(Selenium3)
-
Java星选一卡通
-
【Jeecg】【代码生成】【VUE】-edit-接口调用-405错误
-
shell简单命令
-
广州大学数值分析学习资料.zip
-
linux下 如何安装开源软件 手把手教你源代码安装方式.zip
-
java中对象数组排序(java比较器,Comparable 或 Comparator)
-
嵌入式系统开发笔记05:让VS Code在调试时启动特定文件
-
朱有鹏老师嵌入式linux核心课程2期介绍
-
系统盘.iso 中小学材料系统的系统盘,绿色的
-
吐槽scala
-
Excel高级图表技巧
-
"万年历" C语言 新手
-
Uncaught Error: Syntax error, unrecognized expression错误
-
RabbitMQ消息中间件实战(附讲义和源码)
-
BUU-Reverse模块练习WP
-
9000题库-下.iso
-
单元测试UnitTest+Pytest【Selenium3】
-
分享一篇详解介绍Java中定时任务Timer、Spring Task、quartz
-
【数据分析-随到随学】数据分析建模和预测