Android 面试题总结之Android 进阶

Posted fuchenxuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 面试题总结之Android 进阶相关的知识,希望对你有一定的参考价值。

android 之美 从0到1 之Android 进阶(二)

在上一章节中《Android 之美 从0到1 之Android 进阶(一)》中我们已经理解了一些View的基本知识并且知道如何自定义View。那么本章节将继续深入理解View,关于View的绘制流程,View的事件分发。刷新机制等等。

在阅读过程中有任何问题,请及时联系。如需转载请注明 fuchenxuan blog
本章系《Android 之美 从0到1 – 高手之路》Android 深入理解View的绘制流程。

掌握

  1. Window是什么?
  2. View的绘制流程
  3. View的事件分发机制
  4. View 与SurfaceView,GLSurfaceView

View的绘制流程

上一文章中我们已经自定义View以及View的三大过程,基本操作由三个函数完成:measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法

在理解View的绘制流程之前我们应该知道这几个类:

  • View:最基本的UI组件,表示屏幕上的一个矩形区域。
  • Window: 是一个抽象基类,作用于外观用户界面和行为策略表示一个窗口,它包含一个View tree和窗口的layout 参数。View tree的root View可以通过getDecorView得到。还可以设置Window的Content View。其实现类是PhoneWindow。Activity,Dialog,Toast,都包含一个Window,该Window在Activity的attach()函数中mWindow = new PhoneWindow(this);创建。
  • DecorView:该类是PhoneWindow类的内部类,继承自FrameLayout,它是所有应用窗口的根View,PhoneWindow设置DecorView为应用窗口的根视图。
  • PhoneWindow:PhoneWindow对象帮我们创建了一个PhoneWindow内部类DecorView(父类为FrameLayout)窗口顶层视图
  • ViewRootImpl:ViewRootImpl是连接WindowManager与DecorView的纽带,View的整个绘制流程的三大步(measure、layout、draw)以及我们一些addView()的操作,都是通过ViewRootImpl完成的。
  • WindowManager:应用程序界面和窗口管理器

    在Activity onCreate使用的setContentView()就是设置的ContentView,通过LayoutInflater将xml内容布局解析成View树形结构添加到DecorView顶层视图中id为content的FrameLayout父容器上面。

    那么DecorView是如何绘制的呢?我们分两个步骤来理解:

    • DecorView添加到Window的过程
    • DecorView的绘制过程

DecorView添加到Window的过程

我们根据下图步骤来解析DecorView添加到Window的过程,以便让我们更容易的理解。

  1. **Activity初始化:**Activity 启动,关于Activity的创建过程啊或者其他细节,因为不是本篇幅重点故不做详细讨论。我们尽量简化理解View的绘制流程。
  2. 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);
                ..........
      
    
  3. 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 还没有开始绘制。

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

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;

View 的measure过程

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 的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

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过程

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件事:

  1. 绘制该View的背景
    • 如果要视图显示渐变框,这里会做一些准备工作
    • 调用onDraw()方法绘制视图本身 ,每个View都需要override该方法,ViewGroup不需要实现该方法,因为ViewGroup没有内容,但是ViewGroup需要通知View 调用onDraw函数,也就是下面的dispatchDraw();
    • 绘制子视图的内容,dispatchDraw()函数。在View中这是个空函数,具体的视图不需要实现该方法,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,该方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的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;
        /*
         * 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的事件机制有助于解决开发过程中经常会遇到滑动、点击事件冲突问题、View事件机制已经是android开发者必不可少的知识。那么
之前《Android 面试题总结之Android 基础 (六)》一文中我们已经熟悉了View 和ViewGroup之间的关系,为我们理解View的事件分发机制奠定了基础。

View 事件构成

在Android中,事件主要包括onClick、onLongClick、onScroll、onFling等,onClick又包括单击和双击,另外还包括单指操作和多指操作。
用户在手指与屏幕接触过程中通过MotionEvent对象产生一系列事件,它有四种状态:

  • 按下(ACTION_DOWN)
  • 移动(ACTION_MOVE)
  • 抬起(ACTION_UP)
  • 退出 (ACTION_CANCEL) 一般由程序产生,不会由用户产生

所有这些都构成了Android中的事件响应,Touch事件由 Action_Down、Action_Move、Aciton_UP 组成,其中一次完整的Touch事件中,Down 和 Up 都只 有一个,Move 有若干个,可以为 0 个。

View的事件分发

我们先来了解三个常见的函数的作用。

View事件机制三个过程

事件分发的过程

在这个事件分发过程,我们分为ViewGroup的事件分发过程和View的事件分发过程这两个方面。

  • public boolean dispatchTouchEvent(MotionEvent event)
    Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理进行事件分发,dispatchTouchEvent 的事件分发逻辑如下:

    • 如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;
    • 如果 return false,事件分发给父View的onTouchEvent 进行消费(如果在最外层是Activity 则是返回给Activity的onTouchEvent):
    • 如果返回系统默认的 super.dispatchTouchEvent(ev),事件会自动的分发给当前 ViewGroup 的 onInterceptTouchEvent方法 ,如果是View 就继续往下分发。

ViewGroup分发过程

当事件分发到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 事件,标志着本次事件取消。

View的事件分发

下面通过点击Activity上的一个Button小例子来分析View的事件流程:

先来一张简化的流程图

当点击这个Button,首先执行到的是MainActivity的dispatchTouchEvent方法,这将是事件分发的开始。

  • 如果MainActivity 不进行拦截,那么继续分发给Button,询问Button是否分发,如果不进行分发,回调onTouchEvent是否进行事件消费。
  • 如果MainActivity 进行拦截,子View就分发不到。

总结一张图理解事件分发流程(红色箭头流向):

事件响应的过程

响应的过程是子View 回传递到父View的过程
还是用这张图来理解事件响应流程(绿色箭头):

  • 如果子View(Button)消费了事件,所以开始回传,一层一层往上告诉父View他已经消费了事件。
  • 如果子View(Button)没有消费事件,也开始回传,一层一层往上告诉父View他没有消费事件,问ViewGroup2是否消费事件,如果ViewGroup1也不消费事件,继续回传到ViewGroup1,问ViewGroup1是否消费事件,如果也不消费事件,最终回传到Activity,让Activity去消费。

事件处理的过程

onTouchEvent方法用于事件的处理,

  • 如果事件传递到当前 View(Btuuon) 的 onTouchEvent 方法,如果返回false,那么这个事件会从当前 View(Bttuon) 向上ViewGroup1传递。
  • 如果返回了 true 接收并消费该事件。
  • 如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。
    最终当一个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();
                       
            
    

View 与SurfaceView,GLSurfaceView

SurfaceView是从View基类中派生出来的显示类。android游戏开发中常用的三种视图是:view、SurfaceView和GLSurfaceView

  • View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。
  • SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快,缺点 非常消耗cpu和内存的开销
  • GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用,。

问题总结

  1. View的绘制流程分几步,从哪开始?哪个过程结束以后能看到view?
    ViewRootImplperformTraversals开始,经过measure,layout,draw 三个流程。draw流程结束以后就可以在屏幕上看到view了。

  2. view的测量宽高和实际宽高有区别吗?
    基本上百分之99的情况下都是可以认为没有区别的。有两种情况,有区别。第一种 就是有的时候会因为某些原因 view会多次测量,那第一次测量的宽高 肯定和最后实际的宽高 是不一定相等的,但是在这种情况下

    最后一次测量的宽高和实际宽高是一致的。此外,实际宽高是在layout流程里确定的,我们可以在layout流程里 将实际宽高写死 写成硬编码,这样测量的宽高和实际宽高就肯定不一样了,虽然这么做没有意义 而且也不好。

  3. view的measureSpec 由谁决定?顶级view呢?
    View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同确定,而对于DecorView是由它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同确定。
    在ViewRootImpl的performTraversals方法中,完成了创建DecorView的MeasureSpec的过程,一旦确定了spec,onMeasure中就可以确定view的宽高了。

  4. 对于普通view来说,他的measure过程中,与父view有关吗?如果有关,这个父view也就是viewgroup扮演了什么角色?

    对于普通view的measure来说 是由这个view的 父view ,也就是viewgroup来触发的。
    通过前面归纳总结一张图大致理解View 的 measure过程

  5. view的meaure和onMeasure有什么关系?
    view的measure是final 方法 我们子类无法修改的,是在measure方法里调用了onMeasure方法。

  6. 自定义view中 如果onMeasure方法 没有对wrap_content 做处理 会发生什么?为什么?怎么解决?

    如果没有对wrap_content做处理 ,那即使你在xml里设置为wrap_content.其效果也和match_parent相同。
    解决方式就是在onMeasure里 针对wrap 来做特殊处理 比如指定一个默认的宽高,当发现是wrap_content 就设置这个默认宽高即可。

  7. ViewGroup有onMeasure方法吗?为什么?

    没有,这个方法是交给子类自己实现的。不同的viewgroup子类 肯定布局都不一样,那onMeasure索性就全部交给他们自己实现好了。

  8. 为什么在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);
                    
                );
            
    
  9. draw方法 大概有几个步骤?
    主要分为6个步骤:

    1. 绘制该View的背景
      • 如果要视图显示渐变框,这里会做一些准备工作
      • 调用onDraw()方法绘制视图本身 ,每个View都需要override该方法,ViewGroup不需要实现该方法,因为ViewGroup没有内容,但是ViewGroup需要通知View 调用onDraw函数,也就是下面的dispatchDraw();
      • 绘制子视图的内容,dispatchDraw()函数。在View中这是个空函数,具体的视图不需要实现该方法,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,该方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法。
      • 如果需要, 绘制当前视图在滑动时的边框渐变效果
      • 绘制滚动条
  10. View的刷新机制?
    当子View需要刷新时会调用子View的invalidate()来重新绘制。View的刷新机制,是通过父View负责刷新、布局显示子View;而当子View需要刷新时,则是通知父View来完成,我们可通过下图更容易理解之间的关系。

  11. 事件分发中的 onTouch 和 onTouchEvent 有什么区别,又该如何使用?
    这两个方法都是在 View 的 Android 面试题总结之Android 进阶

    Android 面试题总结之Android 进阶

    Android 面试题总结之Android 进阶

    Android 面试题总结之Android 进阶

    Android 面试题总结之Android 进阶

    Android高级架构进阶之数据传输与序列化,Android多态实现原理