Android进阶知识——View的工作原理

5种隐藏代码到图片的实用技巧:从Notepad++到Linux命令全解析 本文详细解析了5种将代码或文本隐藏到图片中的实用数据隐写技巧。从Notepad++尾部追加、Windows命令行合并,到编辑图像元数据、使用十六进制编辑器,再到Linux的cat/dd命令,全面覆盖了从图形界面到命令行的工程化实践方法,帮助开发者和安全研究人员掌握隐蔽信息传输与存储的核心技术。 阅读详情


本章我们主要介绍两个方面的内容,首先介绍View的工作原理,接着介绍自定义View的实现方式。

有的时候我们可能想要在界面上实现一些比较华丽的效果,而往往系统提供的现有控件并不能满足我们的需求,这个时候我们就需要使用自定义View了。为了更好地自定义View,还需要掌握View的底层工作原理,比如View的测量流程、布局流程以及绘制流程。除了View的三大流程以外,View常见的回调方法也是需要熟练掌握的,比如构造方法、onAttach、onVisibilityChanged、onDetach等。另外对于一些具有滑动效果的自定义View,我们还需要处理View的滑动,如果遇到滑动冲突就还需要解决相应的滑动冲突。

1.初识ViewRoot和DecorView

在正式介绍View的三大流程之前,我们必须先介绍一些基本概念,这样才能更好地理解View的measure、layout和draw过程,本节主要介绍ViewRoot和DecorView的概念。

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,该过程可以参考如下源码:

root=new ViewRootImpl(view.getContext(),display);//创建ViewRootImpl对象
root.setView(view,wparams,panelParentView);//将ViewRootImpl对象和DecorView建立关联

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的位置,而draw则负责将View绘制在屏幕上。performTraversals的大致流程如下图所示:
在这里插入图片描述
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复就完成了整个View树的遍历。同理,performLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有本质区别。

measure过程决定了View的宽/高,Measure完成以后,可以通过getMeasureWidth和getMeasureHeight方法来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽/高,但依然有特殊情况,这点在本章后面会进行说明。Layout过程决定了View的四个顶点的坐标和实际的View的宽/高,完成以后,可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法来拿到View的最终宽/高。Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两部分,上面是标题栏,下面是内容栏,如下图所示。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,因此可以理解为Activity指定布局的方法不叫setview而叫setContentView,因为我们的布局加到了id为content的FrameLayout中。那么如何得到content呢?我们可以这样:ViewGroup content=(ViewGroup) findViewById(android.R.id.content)。那么如何得到我们设置的View呢?我们可以这样:content.getChildAt(0)。(DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后才传递给我们的View)在这里插入图片描述

2.理解MeasureSpec

为了更好地理解View的测量过程,我们还需要理解MeasureSpec。MeasureSpec是干什么的呢?确切来说,MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个measureSpec来测量出View的宽/高。(上面提到过,这里的宽/高是测量宽/高,不一定等于View的最终宽/高)

2.1MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。接下来,我们通过MeasureSpec中的部分源码,来理解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;

public static int ma keMeasureSpec(int size, int mode) (
	if (sUseBrokenMakeMeasureSpec) {
		return size + mode;
	} else {
		return (size & ~MODE_MASK) | (mode & MODE_MASK);
	}
}

public static int getMode (int measureSpec) {
	return (measureSpec & MODE_MASK);
}

public static int getSize (int measureSpec) (
	return (measureSpec & ~MODE_MASK);
}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包为一个MeasureSpec(一个MeasureSpec中共有宽/高两个测量规则),而一个MeasureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。

SpecMode有三类,每一类都表示特殊的含义,如下所示。

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大,该情况一般用于系统内部,表示一种测量的状态。

  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。

  • AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

2.2MeasureSpec和LayoutParams的对应关系

一般情况下我们使用View指定MeasureSpec,尽管如此,我们也可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。另外,对于顶级View(DecorView)和普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。(DecorView的MeasureSpec由窗口大小和自身的LayoutParams共同决定;普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定)

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码,它展示了DecorView的MeasureSpec的创建过程:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth/*窗口的尺寸*/, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight/*窗口的尺寸*/, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着再看一下getRootMeasureSpec方法的实现:

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;
}

通过上述代码,DecorView的MeasureSpec的产生过程就很明确了,具体来说其遵守如下规则,根据它的LayoutParams中的宽/高的参数来划分。

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。

  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。

  • 固定模式(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通View来说(这里是指我们布局中的View),View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildWidthMargins方法:

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.1eftMargin + 1p.rightMargin + widthUsed, lp.width);
	final int childHeightMeasureSpec = getChildMeasureSpec (parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, 1p.height);
	
	child.measure (childWidthMeasureSpec, childHeightMeasureSpec);
}

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码中我们可以看出,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin及padding有关。具体我们再来看ViewGroup的getChildMeasureSpec方法,如下所示:

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) {
		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 = 0;
				resultMode = MeasureSpec.UNSPECIFIED:
			} else if (childDimension == LayoutParams.WRAP_CONTENT) {
				resultSize = 0;
				resultMode = MeasureSpec.UNSPECIFIED;
			}
			break;
	}
	return MeasureSpec.makeMeasureSpec(resultsize, resultMode); 
}

上述方法的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,具体代码如下所示:

int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding) ;

getChildMeasureSpec清楚地展示了普通View的MeasureSpec的创建规则,为了更清晰地理解getChildMeasureSpec的逻辑,这里提供一个表,表中对getChildMeasureSpec的工作原理进行了梳理,图表如下所示:(注意:parentSize是指父容器中目前可使用的大小)
在这里插入图片描述
针对上表,这里再做一下说明。当View采用固定宽/高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循LayoutParams中的大小。当View的宽/高是match_parent时,如果父容器是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不能超过父容器的剩余空间。(注:在我们的分析中漏掉了UNSPECIFIED模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式)(总结:父容器的MeasureSpec只会影响子元素的LayoutParams为wrap_content的情形)

从上图中我们可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定出子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。

4.View的工作流程

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。

3.1measure过程

measure过程要分情况来看,如果是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程,接下来我们针对这两个情况分别讨论。

1.View的measure过程

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可,View的onMeasure方法如下所示:

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

上述代码中,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://默认情况下AT_MOST和EXACTLY的结果一样
		case MeasureSpec.EXACTLY:
			result = specSize;
			break;
	}
	return result:
}

在上述代码中我们只需要关心AT_MOST和EXACTLY这两种情况。其实,getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分。(但几乎是所有的情况下View的测量大小和最终大小是相等的)

UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为getSuggestMinimumWidth和getSuggestMinimumHeight这两个方法的返回值,看一下它们的源码:

protected int getSuggestedMinimumWidth() {
	return (mBackground == nul1) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
	return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

这里只分析getSuggestedMinimumWidth方法的实现,getSuggestedMinimumHeight和它的实现原理是一样的。从getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对应于android:minWidth这个属性所指定的值,因此View的宽度即为android:minWidth属性所指定的值。这个属性如果不指定,那么mMinWidth则默认为0;如果View指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth())。mMinWidth的含义我们已经知道了,那么mBackground.getMinimumWidth()是什么呢?我们看一下Drawable的getMinimumWidth方法,如下所示。

public int getMinimumWidth() {
	final int intrinsicWidth = getIntrinsicWidth();
	return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就会返回0。那么Drawable在什么情况下有原始宽度呢?这里举个例子说明一下,ShapeDrawable无原始宽/高,而BitmapDrawable有原始宽/高。(图片的尺寸)

这里再总结一下getSuggestedMinimumWidth的逻辑:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽/高。 (getSuggestMinimumWidth的返回值对应View在UNSPECIFIED下的测量宽/高,当无背景时就返回宽度所需的最小值(minWidth),当有背景时就返回背景和最小宽度中的最大值,原则就是要让View刚好放下)

从getDefaultSize方法的实现来看,View的宽/高由SpecSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。为什么呢?从上述代码中我们知道,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize;该情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致。那么如何解决这个问题呢?代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
	int widthSpecsize = MeasureSpec.getSize(widthMeasureSpec);
	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
	int heightSpecsize = MeasureSpec.getSize(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);
	}
}

上述代码中,我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置次宽/高即可。对于非wrap_content情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。

2.ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此他没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法,如下所示:

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);
		}
	}
}

通过上述代码我们可以看出,ViewGroup在measure时,会对每一个子元素进行measure,measureChild这个方法的实现也很好理解,如下所示:

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 chi1dHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
	
	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

显然,measureChild的思想就是取出元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。

我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法做统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因此ViewGroup无法做统一实现。下面我们就来通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。

首先我们来看LinearLayout的onMeasure方法,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) (
	if (mOrientation == VERTICAL) {
		measureVertical(widthMeasureSpec, heightMeasureSpec);
	} else {
		measureHorizontal(widthMeasureSpec, heightMeasureSpec);
	}
}

上述代码即判断LinearLayout的布局方式(水平或竖直),我们选择一个来看一下,比如选择查看竖直布局的LinearLayout的测量过程,即measureVertical方法,首先看一段代码:

for (int i = 0; i < count; ++i) {
	final View child = getVirtualChildAt(i);
	...
	measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0) ;
	
	if (oldHeight != Integer.MIN_VALUE) {
		lp.height = oldHeight;
	}
	
	final int childHeight = child.getMeasuredHeight();
	final int totalLength = mTotalLength;
	mTotalLength = Math.max(totalLength, totalLength+childHeight+lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}

从上述代码我们可以看出,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕之后,LinearLayout会测量自己的大小,源码如下所示:

mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength; 

heightSize = Math.max (heightSize, getSuggestedMinimumHeight());

int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndstate(maxWidth, widthMeasureSpec, childState) , heightSizeAndState) ;

当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程则和View有所不同。具体来说是指,如果它的布局中高度采用的是match_parent或者具体的数值,那么它的测量过程和View一致,即高度为specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余总和,当然它的最终高度还需要考虑其在竖直方向上的padding,这个过程可以进一步参考如下源码:

public static int resolveSizeAndstate(int size, int measureSpec, int childMeasuredstate) {
	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://只有这块较为特殊
			if (specSize < size) {
				result = specSize | MEASURED_STATE_TOO_SMALL;
			} else {
				result = size;
				break;
			}
		case MeasureSpec.EXACTLY:
			result = specSize;
			break;
	}
	return result | (childMeasuredState & MEASURED_STATE_MASK);
}

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Height方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。

上面我们已经对View的measure过程进行了详细的分析,现在考虑一种情况,比如我们想在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽/高。面对这样一个问题可能大多数人的想法都是,我在onCreate或者onResume里面去获取这个View的宽/高不就行了。可实际上在onCreate、onStart、onResume中均无法正确得到某个View的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽/高就是0。那么该怎样解决这样一个问题呢?这里给出四种方法来解决这个问题:

(1) Activity/View#onWindowFocusChanged。(当View已经初始化完毕后再去获取宽/高)

onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没有问题的。需要注意的是,onWindowFocusChanged会被调用好多次,当Activity的窗口得到焦点和失去焦点时均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁调用。代码如下:

public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

(2) view.post(runnable)。(利用post将测量代码投递到消息队列的尾部)

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已近初始化好了。代码如下:

protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

(3) ViewTreeObserver。(利用ViewTreeObserver中的回调完成)

使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutLinstener这个接口,当View树的状态发生改变或者View树内部的View的可见性发现改变时,onGlobalLayout方法将被回调,因此这是获取View的宽/高一个很好的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。代码如下:

protected void onStart() {
    super.onStart();
    ViewTreeObserver observer=view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width=view.getMeasuredWidth();
            int height=view.getMeasuredHeight();
        }
    });
}

(4) view.measure(int widthMeasureSpec,int heightMeasureSpec)。(手动测量View的宽高)

通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:

  • match_parent

直接放弃,无法measure出具体的宽/高。因为,根据View的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。

  • 具体的数值(dp/px)

比如说宽/高都是100px,如下measure:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
  • wrop_content

如下measure:

int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30)-1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);

注意到(1 << 30)-1,通过分析MeasureSpec的实现可以知道,View的尺寸使用30位二进制表示,也就是说最大是30个1(即2^30-1),也就是(1 << 30)-1,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。

3.2layout过程

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用器其layout方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,我们先来看看View的layout方法,如下所示:

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;
	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);//确定子元素位置
		mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
	
		ListenerInfo li = mListenerInfo;
		if (1i != 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); 
		}
	}
	
	mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
	mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

layout方法的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mButton这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和ViewGroup的onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。接下来,我们还是以LinearLayout为例来分析它的onLayout方法,如下所示:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
	if i (mOrientation == VERTICAL) {
		layoutVertical(l, t, r, b) ;
	} else {
		layoutHorizontal(l, t, r, b);
	}
}

上述代码的逻辑是,判断LinearLayout的布局方式(水平或竖直),这里我们选择竖直的布局方式进行分析。

void layoutVertical(int left, int top, int right, int bottom) {
	...
	final int count = getVirtualChildCount();
	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();
			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) ;
		}
	}
}

该方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout的特性。而setChildFrame则仅仅是调用了子元素的layout方法,这样父元素在layout中完成自己的定位后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层地传递下去就完成了整个View树的layout过程。setChildFrame方法的实现如下所示:

private void setChildFrame(View child, int left, int top, int width, int height) {
	child.layout(left, top, left + width, top + height);
}

setChildFrame中的width和height实际上就是子元素的测量宽/高,从下面的代码中我们可以看出这一点:

final int childWidth = child.getMeasuredwidth(); 
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight) ;

而在子元素的layout方法中会通过setFrame去设置子元素的四个顶点的位置,在setFrame中有如下几句赋值语句,这样一来子元素的位置就确定了:

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

接下来我们来回答一个问题:View的测量宽/高和最终宽/高有什么区别?这个问题可以具体为:View的getMeasureWidth和getWidth这两个方法有什么区别,至于获取height的两个方法和前者完全一样。首先我们来看一下getWidth和getHeight这两个方法的具体实现:

public final int getWidth() {
	return mRight - mLeft;
}

public final int getHeight() {
	return mBottom - mTop;
}

从上述代码中我们就可以看出,getWidth方法的返回值刚好就是View的测量宽度,而getHeight方法的返回值也刚好就是View的测量高度。现在我们可以回答这个问题了:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值机制不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为View的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,下面举例说明。

如果重写View的layout方法,代码如下:

public void layout(int l,int t,int r,int b){
	super.layout(l,t,r+100,b+100);
}

上述代码会导致在任何情况下View的最终宽/高总是比测量宽/高大100px,虽然这样做会导致View显示不正常并且也没有实际意义,但是这证明了测量宽/高的确可以不等于最终宽/高。另外一种情况是在某些情况下,View需要多次measure才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同。

3.3draw过程

Draw过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

  • (1) 绘制背景background.draw(canvas)。

  • (2) 绘制自己(onDraw)。

  • (3) 绘制children(dispatchDraw)。

  • (4) 绘制装饰(onDrawScrollBars)。

接下来我们来分析一下draw方法的源码:

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;
	
	int saveCount;
	if (!dirtyOpaque) {
		drawBackground (canvas);//绘制背景
	}
	
	final int viewFlags = mViewFlags;
	boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
	boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
	if (!verticalEdges && !horizontalEdges) {
		if (!dirtyOpaque) onDraw(canvas);//绘制自己
		dispatchDraw(canvas);//绘制children
		onDrawScrollBars(canvas);//绘制装饰
		if (mOverlay != null && !mOverlay.isEmpty()) {
			mOverlay.getoverlayView() .dispatchDraw (canvas) ;
		}
		return;
	}
	...
}

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。View还有一个特殊的方法setWillNotDraw,先来看一下它的源码,如下所示:

public void setWillNotDraw(boolean willNotDraw) {
	setFlags(WillNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

接下来我们来分析setWillNotDraw这个方法的作用,如果一个View不需要绘制任何内容,那么设置这个标志位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们自定义控件继承与ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw(用于绘制自己)来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。

4.自定义View

本节我们将详细介绍自定义View相关的知识。

4.1自定义View的分类

自定义View的分类标准不唯一,而笔者则把自定义View分为4类。

  • 1.继承View重写onDraw方法:这种方法主要实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

  • 2.继承ViewGroup派生特殊的Layout:这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

  • 3.继承特定的View(比如 TextView):这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding等。

  • 4.继承特定的ViewGroup(比如 LinearLayout):这种方法也比较常见,当某种效果看起来很像几种View组合在一起时,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。

4.2自定义View须知

本节将介绍自定义View过程中的一些注意事项,这些问题如果处理不好,有些会影响View的正常使用,而有些则会导致内存泄漏等,具体的注意事项如下所示:

  • 1.让View支持wrap_content:这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用warp_content时就无法达到预期的效果。

  • 2.如果有必要,让你的View支持padding:这时因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin无效。

  • 3.尽量不要在View中使用Handler,没必要:这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确地要使用Handler来发送消息。

  • 4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow:如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。

  • 5.View带有滑动嵌套情形时,需要处理好滑动冲突:如果有滑动冲突的话,那么要合适的处理滑动冲突,否则将会严重影响View的效果。

4.3自定义View示例

本节将通过几个实际的例子来演示如何自定义一个规范的View,通过本节的例子再结合上面两节的内容,可以让读者更好地掌握自定义View。下面我们仍然按照自定义View的分类来介绍具体的实现细节。

1.继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

这里我们选择实现一个很简单的自定义控件,简单到只是绘制一个圆,尽管如此,这里面需要注意的细节还是很多的。为了实现一个规范的控件,在实现过程中必须考虑到wrap_content模式以及padding,同时为了提高便捷性,还需要对外提供自定义的属性。我们先来看一下最简单的实现,代码如下:

public class CircleView extends View {
    
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    
    private void init(){
        mPaint.setColor(mColor);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width,height)/2;//半径
        canvas.drawCircle(width/2,height/2,radius,mPaint);
    }
}

上面的代码实现了一个具有圆形效果的自定义View,它会在自己的中心点以宽/高的最小值为直径绘制一个红色的实心圆。不过上面的代码只是一种初级的实现,并不是一个规范的自定义View,为什么这么说呢?有兴趣的同学可以去试一下,分别调整
布局属性中的margin,padding及wrap_content属性并观察其运行结果。

这里直接给出结果,margin属性是生效的,这是因为margin属性是由父容器控制的,因此不需要在CircleView中做特殊处理;padding属性是不生效的,这就是我们在前面提到的直接继承自View和ViewGroup的控件,padding是默认无法生效的,需要自己处理;wrap_content属性也是不生效的,这一点也在前面已经提到过,对于直接继承自View的控件,如果不对wrap_content做特殊处理,那么使用wrap_content就相当于使用match_parent。

为了解决上面提到的问题,我们需要做如下处理:

首先,针对wrap_content的问题,这里只需要在onMeasure方法中为wrap_content指定一个模式的默认宽/高即可,比如选择200px作为默认的宽/高。代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    int mWidth=200;
    int mHeight=200;
    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);
    }
}

其次,针对padding的问题,也很简单,只要在绘制的时候考虑一下padding即可,因此我们需要对onDraw稍微做一下修改,修改后的代码如下:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    final int paddingLeft = getPaddingLeft();
    final int paddingRight = getPaddingRight();
    final int paddingTop = getPaddingTop();
    final int paddingBotton = getPaddingBottom();
    int width = getWidth()-paddingLeft-paddingRight;
    int height = getHeight()-paddingTop-paddingBotton;
    int radius = Math.min(width,height)/2;
    canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
}

上面的代码很简单,中心思想就是在绘制的时候考虑到View四周的空白即可,其中圆心和半径都会考虑到View四周的padding,从而做相应的调整。

最后,为了让我们的View更加容易使用,很多情况下我们还需要为其提供自定义属性,像android:layout_width和android:padding这种以android开头的属性是系统自带的属性,那么该如何添加自定义属性呢?我们要遵循如下几步:

第一步,在values目录下面创建自定义属性的XML,比如attrs.xml,也可以选择类似于attrs_circle_view等这种以attrs_开头的文件名,当然这个文件名没有什么限制,可以随便取名字。针对本例来说,选择创建attrs.xml文件,文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>
</resources>

在上面的XML中声明了一个自定义属性集合“CircleView”,在这个集合里面可以有很多自定义属性,这里只定义了一个格式为“color”的属性“circle_color”,这里的格式color指的是颜色。除了颜色格式,自定义属性好友其他格式,比如reference是指资源id,dimension是指尺寸,而像string、integer和boolean这种是指基本数据类型。除了这些以外还有其他类型,感兴趣的同学可以去查看一下文档。

第二步,在View的构造方法中解析自定义属性的值并做相应处理。对于本例来说,我们需要解析circle_color这个属性的值,代码如下:

public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.CircleView);
    mColor=a.getColor(R.styleable.CircleView_circle_color,Color.RED);
    a.recycle();
    init();
}

首先加载自定义属性集合CircleView,接着解析CircleView属性集合中的circle_color属性,它的id为R.styleable.CircleView_circle_color。在这一步中,如果在使用时没有指定circle_color这个属性,那么就会选择红色为默认的颜色值,解析完自定义属性后,通过recycle方法来释放资源,这样CircleView中所做的工作就完成了。

第三步,在布局文件中使用自定义属性,如下所示。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:background="#ffffff"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.androidpuls_4.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="20dp"
        android:background="#000000"
        app:circle_color="@color/purple_200"/>

</LinearLayout>

2.继承ViewGroup派生特殊的Layout

这种方法主要用于实现自定义的布局,采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

这里我们来实现一个HorizontalScrollViewEx,它的功能主要类似于ViewPager,也可以说是一个类似于水平方向的LinearLayout的控件,它内部的子元素可以进行水平滑动。这里有一个假设,那就是所有子元素的宽/高都是一样的。下面主要看一下它的onMeasure和onLayout方法的实现,先来看看onMeasure,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = 0;
    int measureHeight = 0;
    final int childCount = getChildCount();
    measureChildren(widthMeasureSpec,heightMeasureSpec);
    int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
    int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
    int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
    if(childCount==0){
        setMeasuredDimension(0,0);
    }else if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
        final View childView = getChildAt(0);
        measureWidth=childView.getMeasuredWidth()*childCount;
        measureHeight=childView.getMeasuredHeight();
        setMeasuredDimension(measureWidth,measureHeight);
    }else if(heightSpecMode==MeasureSpec.AT_MOST){
        final View childView = getChildAt(0);
        measureHeight = childView.getMeasuredHeight();
        setMeasuredDimension(widthSpecSize,measureHeight);
    }else if(widthSpecMode==MeasureSpec.AT_MOST){
        final View childView=getChildAt(0);
        measureWidth=childView.getMeasuredWidth()*childCount;
        setMeasuredDimension(measureWidth,heightSpecSize);
    }
}

这里说明一下上述代码的逻辑,首先会判断是否有子元素,如果没有子元素就直接把自己的宽/高设为0;然后就是判断宽和高是不是采用了wrap_content,如果采用了wrap_content,那么HorizontalScrollViewEx的宽度就是所有子元素的宽度之和;如果高度采用了wrap_content,那么HorizontalScrollViewEx的高度就是第一个子元素的高度。

上面的代码中有两点不太规范的地方:第一点是没有子元素的时候不应该直接把宽/高设为0,而应该根据LayoutParams中的宽/高来做相应处理;第二点是在测量HorizontalScrollViewEx的宽/高时没有考虑到它的padding以及子元素的margin,因为它的padding以及子元素的margin会影响到HorizontalScrollViewEx的宽/高。(padding占用的是自己的控件,margin占用的是父元素的空间)

接着再来看一下HorizontalScrollViewEx的onLayout方法,如下所示:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;
    
    for(int i = 0;i<childCount;i++){
        final View childView = getChildAt(i);
        if(childView.getVisibility()!=View.GONE){
            final int childWidth = childView.getMeasuredWidth();
            mChildWidth=childWidth;
            childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight());
            childLeft+=childWidth;
        }
    }
}

上述代码的逻辑并不复杂,其作用是完成子元素的定位。首先会遍历所有的子元素,如果这个子元素不是出于GONE状态,那么就通过layout方法将其放置在合适的位置上。从代码来看,这个放置过程是由左向右的,这和水平方向的LinearLayout比较类似。上述代码的不完美之处在于放置子元素的过程没有考虑到自身的padding以及子元素的margin,而从一个规范的控件的角度来看,这些都是应该考虑的。

继承特定的View(比如TextView)和继承特定的ViewGroup(比如LinearLayout)这两种方式比较简单,这里就不再举例说明了。

OS实验xv6 6.S081 开坑 OS实验xv6 6.S081 开坑 终于把xv6 2019 6.S081做完了,网络部分没有写,因为有计网实验了。怎么说呢,xv6的坑有点多,能做完多亏了:和我同学ljj交流,参考RedemptionC老哥的博客(他写的非常详细,遗憾的是,他好像没有写mmap),再者就是参考这位老哥的做法(没有源码,写得不是特别详细,不过有借鉴作用,mmap就是借鉴了他的思想)……废话不多说了,直接开始。 实验指导 首推RedemptionC的博客 这位老哥的个人博客 xv6 6.S081官网(后面的实验都要参考官网 阅读详情

相关推荐

“迎圣诞,拿大奖” 活动赛题 Web-SQLi

1. php中sprintf()函数漏洞原理及其利用; 2. mysql盲注语法及python脚本; 3. sqlmap的需要进一步学习;

qq_34106499的博客 674

从头到尾给你讲明白Android View实现原理

Activity是Android开发者写第一行代码起就开始接触到的。而在onCreate方法中调用setContentView(R.layout.main_activity),恐怕也是绝大多数开发者的头等任务。然后我们可以调用findViewById(R.id.xxx)来获取布局中的某一个View。通过给View设置点击事件的监听来响应用户的操作。就这么简单我们和Android的View过了一段幸...

御风而行 4275

滤波器初步入门(非常全面)

主要关注的是-3dB这个点,-3dB就是截止频率所对应的点,我们把功率降至50%以下的信号定义为被衰减掉了(但要跟实际情况相结合,有些信号衰减-3db不够,需要衰减更多)-3dB这个点对应我们的电压来说是0.707。设计电路可以留一定的余量,如果你把4k设计为截止频率,那么4k时候衰减倍数为-3dB通常是不够的,你可以将截止频率设为2k,那么4k的衰减就可以达到15dB(假设),根据自己的需要去设计对应的截止频率。理想的滤波器是当输入的信号频率大于截止频率时,大于部分的频率对应的电压信号直接归0。

yybyybds的博客 5720

Android View原理浅析——View的事件体系

Android View原理浅析——View的事件体系 View是什么 首先我们需要理解什么是View。View是安卓中所有控件的基类,无论是简单的TextView、Button,还是复杂的LinearLayout、ListView,它们的共同基类都是View,ViewGroup其实也是继承了View。Button是一个View,而LinearLayout是View的同时也是ViewGroup,V...

N0tExpecterr0r的博客 413

智能排班考勤系统原型

可实现从业务预测,到工时预估,再到人员分配全流程自动智能排班。可应用于客服行业或工厂的智能排班系统。

gywm15111336483的专栏 995

AURIX TC397开发板到手第一步:用iLLD库驱动LED流水灯(附完整代码)

本文详细介绍了如何使用英飞凌AURIX TC397开发板和iLLD库实现LED流水灯效果。从开发环境配置到硬件连接,再到完整的代码实现,逐步引导开发者完成项目。文章还提供了优化建议和常见问题解决方案,帮助嵌入式开发新手快速上手。

weixin_30911451的博客 390

深入解析Android中View的工作原理

当子元素测量完毕后,LinearLayout会测量自身的大小,对于竖直的LinearLayout,它在水平方向上的测量过程,遵循View的测量过程,在竖直方向上,如果采用的是match_parent或者具体的数值,那么它的测量过程和View的一致,即高度为specSize;1.MeasureSpec很大程度上决定一个View的尺寸规格,测量过程中,系统会将View的layoutParams根据父容器所施加的规则转换成对应的MeasureSpec,再根据这个measureSpec来测量出View的宽/高。

Android_XG的博客 2191

【Qt】QML-01:使用QtCreator10创建QML工程,并讲解第一个程序:Hello World

QML第一讲:创建QML工程、详解QML第一个程序Hello World

郭老二 7795

Android View 原理

Android View原理 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。 新的改变 我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客: 全新的界...

qq_41878375的博客 436

Python实战:逐笔高频因子构建与回测全流程解析

本文详细解析了使用Python构建与回测逐笔高频因子的全流程。从逐笔数据的价值与清洗入手,通过实战代码演示了‘大买成交金额占比’因子的计算逻辑,并重点阐述了因子正交化处理的重要性以剔除风格影响。最后,提供了一个经典的分位数回测框架来验证因子有效性,并分享了数据质量、计算效率与过拟合等核心避坑指南。

grass的博客 391

PrimeTime low power-SMVA分析(4)

这些功能只有在将timing_enable_cross_voltage_domain_analysis变量设置为true时才能使用。DVFS场景与在分布式多场景分析(DMSA)中分析的运行条件场景无关。- check_timing 和 report_exceptions 命令通过将检查或报告应用于所有可能的场景和电压组合来支持SMVA。- 相位锁定环生成的时钟(create_generated_clock -pll_feedback 和 -pll_output)在多个场景的情况下,返回最差情况的值。

QRBQ 649

Android进阶知识(十二):View的工作原理之基本概念

Android进阶知识(十二):View的工作原理之基本概念   从这一篇章开始,笔者将介绍关于View的工作原理与自定义View的相关内容。在介绍View的工作原理(三大流程)之前,读者有必要了解一些基本概念。 一、Activity的视图层结构   在ActivityThread中,当Activity对象被创建完毕之后,会将DecorView添加到Window中。Window是一个抽象类,其唯一...

weixin_38196407的博客 435

Kaggle赛题总结:Enefit - Predict Energy Behavior of Prosumers

竞赛旨在解决能源不平衡的问题,即预计使用的能源与实际使用或产生的能源不符。自发自用者是既消耗又产生能源的个体,他们在造成能源不平衡方面起到了重要作用。尽管他们只占所有能源消费者的一小部分,但他们不可预测的能源使用行为给能源公司带来了物流和财务问题。如果不加以解决,可能会导致运营成本增加、电网不稳定以及能源资源的低效利用。如果这个问题得到有效解决,将显著降低不平衡成本,提高电网的可靠性,并使自发自用者更高效和可持续地融入能源系统中。

猫敷雪 741

自定义View(一) View的基础知识

自定义View系列 自定义View(一) View的基础知识 自定义View(二) View的工作原理 1.View是什么 View是Android所有控件的父类,其子类包括ViewGroup和View 2.View的基础知识 2.1 View的位置参数 Button button = new Button(this); float x = button.getX(...

yuemingxingxing的博客 286

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

2024年安卓最全Android面试题(数据存储、view篇)(3),40岁程序员面试

【Android 详细知识点思维脑图(技能树)】其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

2401_84538278的博客 514

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

自定义view总结

1.源码分析自定义view 源码分析自定义view 2.view工作原理 ViewRoot和DecorView、MeasureSpec、view测量布局绘制过程、自定义view、getMeasuredWidth()与getWidth()函数区别 3.基础自定义view 绘图基础,自定义属性 4.Paint基本使用 硬件加速、文字、paint常用函数 5.动画深入分析 view动画、自定义view动画、属性动画(时间插值器和类型估值器)、ValueAnimator与ObjectAnimato

龚礼鹏的博客 422

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

android手机串口开发,Android面试知识点梳理

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。最后针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》

2301_82243139的博客 934

android 头条适配 的使用_这套1307页的字节跳动Android面试真题解析火了!囊括所有Android知识点和面试题...

/ 前言 /金九银十到了,很多读者都反映有面试的需求,所以我特地给大家准备了一点资料!下面的题目是我花了很长时间整理的群友在面试字节跳动时被问到的面试真题和答案解析,如果大家还有其他好的题目或者好的见解欢迎分享。内容概要:包括 Handler、Activity相关、Fragment、service、布局优化、AsyncTask相关、Android 事件分发机制、 Binder、A...

weixin_31173113的博客 115

年终总结:疫情被裁—进外包后裸辞—大厂高级Android工程师,我这一年的学习之路

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

2401_84009626的博客 422

组件是如何通信的?2021年最新Android面试点梳理,系列篇

热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能。本篇重点讲解热修复热修复的原理,各大热修复框架的比较,以及自身产品中热修复功能的实践。 一、什么是热修复? 正常开发流程 热修复开发流程 热修复优势 让我们来一起看看腾讯微视第三面,面试官都问了什么呢?你能答上来多少? ArrayList的使用,然后问他ArrayList使用过程中有没有遇到过坑; 还问了HashMap,问了差不多20分钟。还有aba问题,GC算法,泛型的边际这些东西; 接着问了h

CHAMPION8888的博客 150

博客的全站式导航

Android开发必备 Android四大组件 Activity Service BoardCastReciever ContentProvider Android基础控件 Android自定义View Android开发进阶

不忘初心 942

【Android】View工作原理

View是Android在视觉上的呈现在界面上Android提供了一套GUI库,里面有很多控件,但是很多时候我们并不满足于系统提供的控件,因为这样就意味这应用界面的同类化比较严重。那么怎么才能做出与众不同的效果呢?答案是自定义View,也可以叫自定义控件,通过自定义View我们可以实现各种五花八门的效果。除了View的三大流程以外,View常见的回调方法也是需要熟练掌握的,比如构造方法、onAttach、onViVisibilityChanged、onDetach等。

2301_79977698的博客 1431

Android View原理浅析——View的工作原理

Android View原理浅析——View的工作原理 下图是Android的UI管理系统的层级关系。 PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口 DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质...

N0tExpecterr0r的博客 1万+

Android高级UI进阶之路(一) —— View的基础知识

View 可以说是在日常开发中,天天使用的角色,虽然说 View 不属于四大组件,但是它的作用跟重要程度你真不可小视。该类型的文章打算写一个系列,对于自己复习或新手学习提供一个方式。更多Android系列教程上传在GitHub:在介绍 View 的基础知识之前,我们需要知道它到底是什么?

金戈鐡馬 1162

揭开Android View的神秘面纱:深入探索工作原理

在 Android 开发中,自定义 View 是实现个性化界面和独特交互效果的重要手段。根据不同的需求和场景,自定义 View 主要分为以下几种常见类型,每种类型都有其独特的实现要点和挑战。继承 View 类是最基础的自定义 View 方式,适用于需要实现完全自定义的绘制效果,无法通过现有控件组合来实现的场景。在这种方式下,开发者需要重写 onDraw 方法,使用 Canvas 和 Paint 来绘制各种图形、文本和图像,以实现所需的视觉效果。

顾林海 3126

Android进阶学习(四)View的工作原理

Android进阶学习(四)View的工作原理View的工作原理1 初识ViewRoot和DecorView2 理解MeasureSpec2.1 MeasureSpec2.2 MeasureSpec和LayoutParams对应关系3. View的工作流程3.1 measure过程3.1.1 View的measure3.1.2 ViewGroup的measure3.2 layout过程3.3 dr...

qq_37149418的博客 265
上一篇: Android进阶知识——View的事件体系
下一篇: Android进阶知识——Android动画深入分析
ABded
博客等级 码龄7年 179粉丝 36原创
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值