Android学习之View绘制
安卓APP中最先让用户注意到的无疑是APP的界面,一个界面好与不好,很大程度上影响到用户的体验。因此,作为一个安卓开发人员,学习与了解View界面的绘制就十分重要了。在我们平常的开发过程中,经常都使用到View相关知识,任何布局和控件,比如TextView、Button、ImageView等等,都是直接或者间接的继承View,这些都是系统提供的,可能我们使用过程中对View绘制的流程也就没什么感觉。在Android中,View绘制过程最主要的三个过程,分别是onMeasure()、onLayout()和onDraw()。下面就这三点详细学习一下:
一. onMeasure
onMeasure,顾名思义,测量的意思,方法的主要作用是测量视图的大小,以下是谷歌官方API文档下对这个方法介绍的截图
可以看到,上面截图里面提到,onMeasure这个方法是在measure这个方法里面有被调用,因此我们打开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);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
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) {
// measure ourselves, this should set the measured dimension flag back
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;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
这段代码有点长,不过关注一下重点部分代码就好了,首先,measure这个方法是final修饰的,说明不允许去重写这个方法。然后在这个方法里面,我们看到了onMeasure这个方法,并且上面的注释说measure ourselves, this should set the measured dimension flag back,大致意思是测量自身,设置测量尺寸的标志。接下来再看一下onMeasure的具体实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到onMeasure方法的代码比较少,通过getDefaultSize来获取大小之后,然后通setMeasureDimension来设置大小,视图的大小最终是通过这个方法来设置的。这样一次measure过程就结束了。按照着这个流程,首先onMeasure方法是可以进行重写的,假设你想要重写了onMeasure方法,并在里面设置setMeasuredDimension(200, 200);,这样子无论你在布局中定义视图的大小是多少,最终显示在界面上的视图大小都是200*200。
以上是比较单一视图情况下的测量过程,经常看到一个视图下往往会有多个子视图,这时候就需要进行多一步的操作了,因此涉及到多次的measure过程。
/**
* 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);
}
}
}
然后for循环里面对每一个子视图进行measureChild操作,再来看看measureChild的实现,可以看到里面同样是调用了measure方法,接下来的流程就和上面的一样了。
/**
* 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);
}
二. onLayout
同样,onLayout,顾名思义,就是给一个视图设置布局的意思。当你视图大小测量完之后,接下来就是给视图安排布局,同样看看谷歌官方对于这个方法的介绍。
首先看一下View里面的onLayout方法,可以看到,方法里面并没有任何的实现方式,再来看看ViewGroup里面的onLayout方法,ViewGroup是继承View的,而LinearLayout、RelativeLayout等布局是继承ViewGroup的
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
ViewGroup里重写了这个方法,变成了一个抽象的方法,那么继承ViewGroup的子类都必须去实现这个方法,比如打开Linearlayout的源码,就可以看到具体的实现方式了
@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);
}
}
至于直接继承自View的基本控件,比如Button,ImageView等,打开源码可以看到重写实现了onLayout方法。由于源码过长,就不一一贴出源码了。
最后需要注意一个问题,那就是getMeasureWidth、getMeasureHeight和getWidth、getHeight之间的区别,首先第一点是,getMeasureWidth、getMeasureHeight是在onMeasure方法结束后才能调用的方法,而getWidth、getHeight是在onLayout方法完成后才能调用的方法,这是第一点区别;第二点区别是,getMeasureWidth、getMeasureHeight方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth、getHeight方法中的值则是通过视图右边的坐标减去左边的坐标、下边的坐标减去上边的坐标计算出来的。因此,如果自定义布局的开发者不规范或者有特殊需求,有可能会导致两者返回的结果不一致。这也是会经常困惑到我们初学者的一个问题。
三. onDraw
在测量onMeasure和布局onLayout之后,接下来就到了比较核心的一步,也就是绘制onDraw。首先,了解官方文档是如何介绍这个方法的。相对于其他两个方法,官方文档这边对这个方法的介绍很简洁。
接下来View里面的draw这个方法,里面有很大一段注释说明了绘制的流程
从步骤流程上面来看,目前需要了解的最重要的一步就是第三步——绘制视图的内容,第三步里面实现了什么。
只有简单地判断之后,调用onDraw(Canvas canvas),但是onDraw方法里面是空的,这也就说明了对视图内容的绘制需要交给具体的子类去实现。通过上面的流程,梳理一下思路,大概就是一般来说View不会去帮我们实现视图内容的绘制,所有的想要实现的内容就需要到相对的具体子类中去实现,同时绘制是借助Canvas这个类去实现的,因此作为参数传入onDraw方法中。
流程图来表示这整个View绘制的大致流程:
推荐阅读
-
Android自定义View绘制的方法及过程(二)
-
android自定义view之模拟qq消息拖拽删除效果
-
Android开发使用自定义View将圆角矩形绘制在Canvas上的方法
-
Android学习笔记之Shared Preference
-
Android学习教程之2D绘图基础及绘制太极图
-
Android自定义View实现绘制虚线的方法详解
-
浅谈Android View绘制三大流程探索及常见问题
-
Android自定义view Path 的高级用法之搜索按钮动画
-
Android学习之Intent中显示意图和隐式意图的用法实例分析
-
Android学习笔记之AndroidManifest.xml文件解析(详解)