标签:
在上一章节中《Android 之美 从0到1 之Android 进阶(一)》中我们已经理解了一些View的基本知识并且知道如何自定义View。那么本章节将继续深入理解View,关于View的绘制流程,View的事件分发。刷新机制等等。
在阅读过程中有任何问题,请及时联系。如需转载请注明 fuchenxuan blog
本章系《Android 之美 从0到1 – 高手之路》Android 深入理解View的绘制流程。
上一文章中我们已经自定义View以及View的三大过程,基本操作由三个函数完成:measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法
在理解View的绘制流程之前我们应该知道这几个类:
mWindow = new PhoneWindow(this);
创建。WindowManager:应用程序界面和窗口管理器
在Activity onCreate
使用的setContentView()就是设置的ContentView,通过LayoutInflater将xml内容布局解析成View树形结构添加到DecorView顶层视图中id为content的FrameLayout父容器上面。
那么DecorView是如何绘制的呢?我们分两个步骤来理解:
我们根据下图步骤来解析DecorView添加到Window的过程,以便让我们更容易的理解。
PhoneWindow的创建:
Activity对象创建完成后,初始化了PhoneWindow对象,该Window在Activity的attach()函数中mWindow = new PhoneWindow(this);
创建,相关代码块如下:
final void attach(Context context, ActivityThread aThread..){
..........
mFragments.attachHost(null /*parent*/);
//创建PhoneWindow对象
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
..........
}
DecorView添加Window:
ActivityThread.java
类会调用handleResumeActivity方法将顶层视图DecorView添加到PhoneWindow窗口,因此通过PhoneWindow的setContentView将Activity与Window进行关联了。
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//获得当前Activity的PhoneWindow对象
r.window = r.activity.getWindow();
//获得当前PhoneWindow内部类DecorView对象
View decor = r.window.getDecorView();
//设置DecorView为可见
decor.setVisibility(View.INVISIBLE);
//获取Activity的WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
//标记已添加至Window
a.mWindowAdded = true;
//添加DecorView到Window
wm.addView(decor, l);
}
}
接着DecorView通过WindowManager设置到ViewRootImpl中,然后就是下面DecorView的绘制流程了。
因此我们知道在Activity的onCreate和onResume方法中调用View.getWidth()和View.getMeasuredHeight()返回值是0,因为View 还没有开始绘制。
ViewRootImpl是连接WindowManager与DecorView的纽带,View的整个绘制流程的三大步(measure、layout、draw)都是通过ViewRootImpl完成的,
绘制是从根节点开始,对布局树进行 measure 和 draw 。整个 View 树的绘图流程在 ViewRootImpl.java 类的 performTraversals() 函数展开,该函数所做 的工作可简单概括为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),结合DecorView添加至Window过程,整体大概的流程图如下:
那么我们围绕图上过程来分析View的绘制流程,首先我们进入ViewRootImpl.java
中,查看performTraversals
函数,这个函数非常长,View的绘制三大流程将在此展开。
“` private void performTraversals() {
// 缓存DecorView ,因为在下面用的比较多
final View host = mView;
…..
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(TAG,
“And hey let’s measure once more: width=” + width
+ ” height=” + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
…..
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
.....
//执行布局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
//执行绘制操作
performDraw();
}
“`
主要分下面三大步骤。
measure操作主要用于计算视图的大小
在前面文章 Android 之美 从0到1 Android 进阶(一)中我们知道View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同确定,而对于DecorView是由它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同确定。
在ViewRootImpl的performTraversals方法中,完成了创建DecorView的MeasureSpec的过程,相应的代码片段如下:
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
我们知道Activity的根视图总是全屏的,因为ViewRootImpl 在创建DecorView的MeasureSpec的过程 测量模式是EXACTLY,而Size是windowSize,相应的代码片段如下:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//匹配父容器时,测量模式为MeasureSpec.EXACTLY,测量大小直接为屏幕的大小,也就是充满真个屏幕
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can‘t resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
......
}
return measureSpec;
}
measure在performMeasure开始的,该函数在view中定义为final类型,要求子类不能修改。measure()函数中又会调用onMeasure()函数,相应的代码片段如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...........
//如果上一次的测量规格和这次不一样,重新测量视图View的大小
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -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;
} ...........
}
实际为整个View tree计算大小是onMeasure()函数,里面直接调用setMeasuredDimension()提供一个默认模式View计算大小,相应的代码片段如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其中默认使用getDefaultSize() 获取默认尺寸大小,如果自定义View不重写onMesure(),在布局中使用wrap_content就相当于使用match_parent的效果相应的代码片段如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//获得测量模式
int specMode = MeasureSpec.getMode(measureSpec);
//获得父亲容器留给子视图View的大小
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;
}
普通View的measure()函数是由ViewGroup在measureChild方法中调用的,ViewGroup调用其子View的measure时即传入了该子View的widthMeasureSpec和heightMeasureSpec,共同决定了View的大小。而DecorView是继承自FrameLayout的,所以我们看下面ViewGroup的measure过程。
ViewGroup需要先完成子View的measure过程,才能完成自身的measure过程,在ViewGroup的onMeasure()函数中,不同的布局(LinearLayout、RelativeLayout、FrameLayout等等)有不同的实现。FrameLayout的onMeasure()方法代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取子View的个数
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//测量FrameLayout下每个子视图View的宽和高
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
.............
}
归纳总结一张图大致理解View 的 measure过程
至此View的measure 过程大致清楚了,下面是View的layout过程。
layout在view中定义为final类型,要求子类不能修改,用于设置子View的位置,因而是由父容器获取子View的位置参数后,调用child.layout方法并传入已获取的位置参数,从而完成对子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的方法,该方法由ViewGroup实现
onLayout(changed, l, t, r, b);
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
通过上面代码我们知道layout主要完成两个操作:setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,该函数用于将这些参数保存起来,onLayout() 是空方法由ViewGroup实现,在ViewGroup中,onLayout是一个抽象方法,因为对于不同的布局管理器类,对子元素的布局方式是不同的。而DecorView是继承自FrameLayout的,所以我们看下面DecorView的onLayout代码片段:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
//遍历每一个子View 进行布局
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//当子视图View可见度设置为GONE时,不进行当前子视图View的布局
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//获取子View的位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//子View布局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
归纳总结一张图大致理解View 的 layout过程
通过上面代码我们知道主要是遍历子View 获取位置,进行布局,至此View的layout 过程大致清楚了,下面是View的draw过程
View视图绘制流程中的最后一步绘制draw是由ViewRootImpl中的performDraw成员方法开始的,用于绘制View内容到画布上,每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。(View不需要绘制任何内容,可通过这个方法将相应标记设为true,系统会进行相应优化。ViewGroup默认开启这个标记,View默认不开启)相应代码片段如下:
//设置是否需要重绘
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
绘制开始
private void performDraw() {
....
if (!mAttachInfo.mScreenOn && !mReportNextDraw) {
return;
}
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
//调用下面draw方法
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
.....
}
接着会在ViewRootImpl类中的drawSoftware方法绘制View,然后调用View的成员方法draw开始绘制,相应代码块如下:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
boolean scalingRequired, Rect dirty) {
............
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
mView.draw(canvas);
............
return true;
}
接着我们看mView.draw() 开始绘制,主要做了以下6件事:
理解相应代码块如下:
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);
// 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);
// we‘re done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas‘ layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
.........
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
.........
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
........
canvas.restoreToCount(saveCount);
// 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);
}
归纳总结一张图大致理解View 的 draw过程
至此View的绘制流程就大致清楚了,通过ViewRootImpl完成的,
绘制是从根节点开始,对布局树进行 measure 和 layout、draw,接下来在最后面我们小结一下一些View比较重要的问题。
理解View的事件机制有助于解决开发过程中经常会遇到滑动、点击事件冲突问题、View事件机制已经是android开发者必不可少的知识。那么
之前《Android 面试题总结之Android 基础 (六)》一文中我们已经熟悉了View 和ViewGroup之间的关系,为我们理解View的事件分发机制奠定了基础。
在Android中,事件主要包括onClick、onLongClick、onScroll、onFling等,onClick又包括单击和双击,另外还包括单指操作和多指操作。
用户在手指与屏幕接触过程中通过MotionEvent对象产生一系列事件,它有四种状态:
所有这些都构成了Android中的事件响应,Touch事件由 Action_Down、Action_Move、Aciton_UP 组成,其中一次完整的Touch事件中,Down 和 Up 都只 有一个,Move 有若干个,可以为 0 个。
我们先来了解三个常见的函数的作用。
在这个事件分发过程,我们分为ViewGroup的事件分发过程和View的事件分发过程这两个方面。
public boolean dispatchTouchEvent(MotionEvent event)
Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理进行事件分发,dispatchTouchEvent 的事件分发逻辑如下:
当事件分发到ViewGroup的dispatchTouchEvent方法,如果返回系统默认的 super.dispatchTouchEvent(ev),事件会自动的分发给当前 ViewGroup 的 onInterceptTouchEvent方法,如果onInterceptTouchEvent 返回true,则调用onTouchEvent方法进行事件处理,否则继续向child View.dispatchTouchEvent分发。
Android中事件传递按照从上到下进行层级传递,事件处理从Activity开始到ViewGroup再到View,举个下面的例子:
当一个 Touch 事件(触摸事件为例)到达根节点,即 Acitivty 的 DecorView 时,它会依次下发,下发的过程是调用子 View(ViewGroup)的 dispatchTouchEvent 方法实现的。简单来说,就是 ViewGroup 遍历它包含着的子 View,再进行判断当前的x,y坐标是否落在子View身上,如果在,那么调用每个 View 的 dispatchTouchEvent 方法,而当子 View 为 ViewGroup 时,又会通过调用 ViwGroup 的dispatchTouchEvent 方法继续调用其内部的 View的 dispatchTouchEvent 方法。上述例子中的消息下发顺序是这样的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent 方法只负责事件的分发,它拥有 boolean 类型的返回值,当返回为 true 时,顺序下发会中断。在上述例子中如果⑤的 dispatchTouchEvent 返回结果为 true,那么⑥-⑦-③-④将都接收不到本次 Touch 事件,标志着本次事件取消。
下面通过点击Activity上的一个Button小例子来分析View的事件流程:
先来一张简化的流程图
当点击这个Button,首先执行到的是MainActivity的dispatchTouchEvent方法,这将是事件分发的开始。
总结一张图理解事件分发流程(红色箭头流向):
响应的过程是子View 回传递到父View的过程
还是用这张图来理解事件响应流程(绿色箭头):
onTouchEvent方法用于事件的处理,
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {}
如果此view被禁用了 返回的是false;相关代码块如下:
// 如果View被禁用的话,则返回它是否可以点击。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
如果此View有触碰事件处理代理,那么将此事件交给mTouchDelegate,相关代码块如下:
// 如果该View的mTouchDelegate不为null的话,将触摸消息分发给mTouchDelegate。
// mTouchDelegate的默认值是null。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
如果View不可点击则直接返回false,如果可以点击进入处理点击,更新View状态等。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
if (!post(mPerformClick)) {
performClick();
}
}
SurfaceView是从View基类中派生出来的显示类。android游戏开发中常用的三种视图是:view、SurfaceView和GLSurfaceView
View的绘制流程分几步,从哪开始?哪个过程结束以后能看到view?
从ViewRootImpl
的performTraversals
开始,经过measure,layout,draw 三个流程。draw流程结束以后就可以在屏幕上看到view了。
view的测量宽高和实际宽高有区别吗?
基本上百分之99的情况下都是可以认为没有区别的。有两种情况,有区别。第一种 就是有的时候会因为某些原因 view会多次测量,那第一次测量的宽高 肯定和最后实际的宽高 是不一定相等的,但是在这种情况下
最后一次测量的宽高和实际宽高是一致的。此外,实际宽高是在layout流程里确定的,我们可以在layout流程里 将实际宽高写死 写成硬编码,这样测量的宽高和实际宽高就肯定不一样了,虽然这么做没有意义 而且也不好。
view的measureSpec 由谁决定?顶级view呢?
View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同确定,而对于DecorView是由它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同确定。
在ViewRootImpl的performTraversals方法中,完成了创建DecorView的MeasureSpec的过程,一旦确定了spec,onMeasure中就可以确定view的宽高了。
对于普通view来说,他的measure过程中,与父view有关吗?如果有关,这个父view也就是viewgroup扮演了什么角色?
对于普通view的measure来说 是由这个view的 父view ,也就是viewgroup来触发的。
通过前面归纳总结一张图大致理解View 的 measure过程
view的meaure和onMeasure有什么关系?
view的measure是final 方法 我们子类无法修改的,是在measure方法里调用了onMeasure方法。
自定义view中 如果onMeasure方法 没有对wrap_content 做处理 会发生什么?为什么?怎么解决?
如果没有对wrap_content做处理 ,那即使你在xml里设置为wrap_content.其效果也和match_parent相同。
解决方式就是在onMeasure里 针对wrap 来做特殊处理 比如指定一个默认的宽高,当发现是wrap_content 就设置这个默认宽高即可。
ViewGroup有onMeasure方法吗?为什么?
没有,这个方法是交给子类自己实现的。不同的viewgroup子类 肯定布局都不一样,那onMeasure索性就全部交给他们自己实现好了。
为什么在activity的生命周期里无法获得测量宽高?有什么方法可以解决这个问题吗?
因为measure的过程和activity的生命周期 没有任何关系。你无法确定在哪个生命周期执行完毕以后 view的measure过程一定走完。可以尝试如下几种方法 获取view的测量宽高。
//重写activity的这个方法
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = tv.getMeasuredWidth();
int height = tv.getMeasuredHeight();
Log.v("burning", "width==" + width);
Log.v("burning", "height==" + height);
}
}
延时一段时间,等待控件测量、布局完成后再获取
//延时一段时间,等待控件测量、布局完成后再获取
@Override
protected void onStart() {
super.onStart();
tv.post(new Runnable() {
@Override
public void run() {
int width = tv.getMeasuredWidth();
int height = tv.getMeasuredHeight();
}
});
}
监听onlayout方法执行完成之后,就可以获取控件大小了
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = tv.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int width = tv.getMeasuredWidth();
int height = tv.getMeasuredHeight();
tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
draw方法 大概有几个步骤?
主要分为6个步骤:
View的刷新机制?
当子View需要刷新时会调用子View的invalidate()来重新绘制。View的刷新机制,是通过父View负责刷新、布局显示子View;而当子View需要刷新时,则是通知父View来完成,我们可通过下图更容易理解之间的关系。
事件分发中的 onTouch 和 onTouchEvent 有什么区别,又该如何使用?
这两个方法都是在 View 的 dispatchTouchEvent 中调用的,onTouch 优先于 onTouchEvent 执行。如果在 onTouch 方法中通过返回 true 将事件消费掉,onTouchEvent 将不会再执行。
onTouch 执行需要满足两个条件:
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
onTouch 和onClick有什么区别,?
onTouch事件要先于onClick事件执行,onTouch在事件分发方法dispatchTouchEvent中调用,而onClick在事件处理方法onTouchEvent中被调用,onTouchEvent要后于dispatchTouchEvent方法的调用。
部分问题总结参考自:http://www.cnblogs.com/punkisnotdead/p/5181821.html
更多Android 之美,请阅读《Android 之美 从0到1 – 高手之路》系列文章
水平有限,若有错漏,欢迎指正,批评,如需转载,请注明出处–http://blog.csdn.net/vfush,谢谢!
标签:
原文地址:http://blog.csdn.net/vfush/article/details/51790079