美文网首页
全面总结Android面试知识要点:高级UI面试题

全面总结Android面试知识要点:高级UI面试题

作者: 代码我写的怎么 | 来源:发表于2023-05-30 16:31 被阅读0次

    请点赞,你的点赞对我意义重大,满足下我的虚荣心。
    🔥常在河边走,哪有不湿鞋。或许面试过程中你遇到的问题就在这呢?
    🔥关注我个人简介,面试不迷路~

    一、View的绘制原理

    背景

    对于Android开发,在面试的时候,经常会被问到,说一说View的绘制流程?我也经常问面试者,View的绘制流程.

    对于3年以上的开发人员来说,就知道onMeasure/onLayout/onDraw基本,知道他们是干些什么的,这样就够了吗?

    如果你来我们公司,我是你的面试官,可能我会考察你这三年都干了什么,对于View你都知道些什么,会问一些更细节的问题,比如LinearLayout的onMeasure,onLayout过程?他们都是什么时候被发起的,执行顺序是什么?

    如果以上问题你都知道,可能你进来我们公司就差不多了,可能我会考察你draw的 canvas是哪里来的,他是怎么被创建显示到屏幕上呢?看看你的深度有多少?

    对于现在的移动开发市场逐渐趋向成熟,趋向饱和,很多不缺人的公司,都需要高级程序员.在说大家也都知道,面试要造飞机大炮,进去后拧螺丝,对于一个3年或者5年以上Android开发不稍微了解一些Android深一点的东西,不是很好混.扯了这么多没用的东西,还是回到今天正题,Android的绘图原理浅析.

    这道题想考察什么?

    1. 是否了解View绘制原理的知识?

    考察的知识点

    1. View的Framework相关知识
    2. View的measure、layout、draw

    考生应该如何回答

    注意: 本文中涉及ActivityThread、WindowManagerImpl、WindowManagerGlobal、ViewRootImpl知识,如果对上述概念不熟悉的同学,先学习对应享学课堂相关的知识章节。

    1.View的Framework相关知识

    先简单说下View的起源,有助于我们后续的分析理解。

    1.1 从ActivityThread.java开始

    下面只贴出源码中的关键代码,重点是vm.addView(decor, l) 这句,那么有三个对象需要理解:

    vm:a.getWindowManager()即通过Activity.java的getWindowManager方法得到的对象;继续跟踪源码,发现此对象由Window.java的getWindowManager方法获得,即是((WindowManagerImpl)wm).createLocalWindowManager(this)。

    decor:通过r.activity.getWindow()可知r.window是PhoneWindow对象,在PhoneWindow中找到getDecorView方法,得知decor即DecorView对象。

    l:通过下面源码得知,l通过r.window.getAttributes获取到,由于PhoneWindow中没有getAttributes方法,故从他的父类Window中获取,得知宽高均为LayoutParams.MATCH_PARENT

    //ActivityThread.java
    
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason)  {   
           ...
           r.window = r.activity.getWindow();
           View decor = r.window.getDecorView();
           decor.setVisibility(View.INVISIBLE);
           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) {
               if (!a.mWindowAdded) {
                  a.mWindowAdded = true;
                  wm.addView(decor, l); // 核心代码
               } else {
                ...
               }
          }
    }
    

    1.2 接上面wm.addView(decor, l),通过上面分析wm是WindowManagerImpl,则走进此类的addView中

      @Override
        public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
            applyDefaultToken(params);
            mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
        }
    

    addView 会将函数转交给WindowManagerGlobal 类中的addView,我们继续看下面的代码:

    1.3 WindowManagerGlobal

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            ...
            ViewRootImpl root;
            ...
            root = new ViewRootImpl(view.getContext(), display);
            ...
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                ...
            }
        }
    }
    

    在WindowManagerGlobal中的addView里面会先创建一个 ViewRootImpl对象,ViewRootImpl大家可以理解为管理viewTree的根布局的一个对象,甚至可以狭义的理解为viewTree根布局的管理者,具体的解析大家可以参考7.8章节关于ViewRootImpl的理解。从上面的代码我们看到WindowManagerGlobal中addView会调用viewRootImpl的setView函数。

    1.4 由1.3步可知,最终走到了ViewRootImpl.java,曙光在前方

     public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
          ...
          requestLayout();
          ...
    }
    

    通过上面的代码我们发现,setView中调用了一个非常重要的代码,那就是requestLayout()函数,这个函数是view系统体系中非常重要的一个函数,可以说是viewTree 绘制管理的真正启动,具体的代码,我们看下面的调用流程:

     public void requestLayout() {
            if (!mHandlingLayoutInLayoutRequest) {
                ...
                scheduleTraversals();
            }
     }
    

    上面的代码会调用 scheduleTraversals(),那么这个函数是干什么的呢?继续往下看:

      void scheduleTraversals() {
            if (!mTraversalScheduled) {
                ...
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                ...
            }
        }
    

    通过scheduleTraversals(),我们发现它设置了一个回调函数mTraversalRunnable,回调函数里面又有什么呢?

     final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
    

    mTraversalRunnable其实就是一个 runnable,所以,她的关键是看run函数中所调用的doTraversal()。

        void doTraversal() {
                ...
                performTraversals();
                ...
        }
    

    在doTraversal()里面出现了一个非常重要的函数performTraversals,为什么说它非常重要呢?

    重点来了看下面的代码调用

     private void performTraversals() {
                ...
                WindowManager.LayoutParams lp = mWindowAttributes;
                ...
                int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                ...
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                ...
                performLayout(lp, mWidth, mHeight);
                ...
                performDraw();
                ...
        }
    

    在上面代码中,有三个非常重要的函数performMeasure、performLayout、performDraw,相信大家通过名字不难发现这几个函数就是执行onMeasure、onLayout,onDraw的关键入口。那么他们是怎么触发到具体view 的onMeasure,onLyout,onDraw上面的呢?我们下面以performMeasure 为例进行讲解。

      private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
            ...
            try {
                mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            } finally {
                ...
            }
        }
    

    上面的mView就是DecorView,也就是根view,当调用measure的时候,会调用view 的 measure函数,measure函数

    2.绘制流程

    2.1 measure过程

    接上面说到了performMeasure,即走到了DecorView的measure,而DecorView实际是FrameLayout,FrameLayout的父类是ViewGroup,而ViewGroup的父类是View,所以直接走 到了View的measure里面。

    主角登场,接下来分析View的measure,measure是final的,也就是不能不能重写此方法,但是里面有一个onMeasure方法,到这里应该很熟悉了,我们自定义控件都会重写这个方法;

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }
    

    由于DecorView的父类的FrameLayout,那么我们来看FrameLayout的onMeasure方法;可以看到会测量所有的子View,最后测量自己。所以有两点:测量子View宽高,确定自己宽高。

     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            ...
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);//1
                ...
            }
         }
         ...省略代码段
         setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
               resolveSizeAndState(maxHeight, heightMeasureSpec,
                            childState << MEASURED_HEIGHT_STATE_SHIFT));//2
            ...
         }
    }
    

    上面的代码有两处非常重要的代码,其中代码1处是度量孩子的宽高, 具体的代码我们可以看接下来的解释,但是在这个函数里面还有一处非常重要的代码,那就是代码2,代码2 就是确定当前onMeasure的view的宽高确定的,那么当前的view是怎么进行确定的呢?它就是各种layout 自身的布局算法了,比如 FrameLayout,LinearLayout,RelativeLayout,在它们上面摆放的子view以一个什么方式排列,排列完成后,需要多高多宽,这些就是文中标注了省略代码段的代码计算的过程(这个过程就是各个layout的核心),最终计算的结果就是得到当前View的宽高,然后调用setMeasuredDimension将得到的宽高保存起来。

    上面代码1处所调用的measureChildWithMargins请看下面的解析。

    我们先来关注测量子View即measureChildWithMargins,大概的意思是:先获到宽高的measureSpec,然后再基于这个measureSpec对view进行measure,从而可以将度量动作分发给当前这个child的孩子。

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

    在上面代码中先获取到LayoutParams,然后通过getChildMeasureSpec计算出自定义view的宽高,里面涉及父view的padding与子view的margin,接着调用子View的measure方法传入计算出的宽高MeasureSpec,层层递归直到无子View为止。

    注意:MeasureSpec 是什么?怎么得到?这个也是一个面试常问的点,大家感兴趣可以去查找书籍找到答案,或者可以去享学课堂找到对应的课程进行学习。

    分析完测量子View,接下来看测量自己,即上面源码中提到的setMeasuredDimension

     protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
            ...
            setMeasuredDimensionRaw(measuredWidth, measuredHeight);
        }
    
      private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
            ...
        }
    

    最后为mMeasureWidth、mMeasureSpec赋值,测量完毕。

    我们来看下View类中的onMeasure(即若不覆写onMeasure的默认逻辑),同上调用了setMeasureSpec为测量结果赋值;这里有需要注意的地方,当我们自定义View覆写onMeasure时,最后一定要为测量结果赋值(setMeasuredDimension),否则会报错。

    小结

    image.png

    图7-1

    view的度量过程就是按照上面的图7-1 viewTree的层次结构进行分发,先从viewRootImpl中执行performMeasure函数,然后再调用view的measure函数,此时的view是rootView属于一个ViewGroup,此时的ViewGroup会执行自己的onMeasure函数:度量孩子并确定自己的宽高;同时在度量孩子的时候,孩子view(viewGroup)就会执行进行同样的分发流程,从而遍历整棵树完成所有view的度量。

    2.2 layout过程

    layout的过程是基于度量的值,对viewTree上面的节点进行布局的过程,整体流程也是按照图7-1的结构,对7-1所指的树中的节点进行深度遍历,直到所有的树节点完成遍历为止,由于过程基本一致本文就不再赘述,感兴趣的朋友可以自行阅读源码或者通过享学课堂的课程进行学习。

    2.3 draw 绘制流程

    Draw 的分发过程也是和Measure的过程一样,入口是ViewRootImpl中的performTraversals(),然后再经过 performDraw()将draw的事件逐步通过函数调用分发到 View.java 中的draw(Canvas canvas)函数,所以我们接下来的分析就重点探讨View.java 中的draw函数。

    代码中的draw方法注释和解析请大家认真理解

    View.java
        public void draw(Canvas canvas) {
            ...
    
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      1. Draw the background
             *      2. If necessary, save the canvas' layers to prepare for fading
             *      3. Draw view's content
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */
    
            // Step 1, draw the background, if needed
            int saveCount;
    
            if (!dirtyOpaque) {
                ...
                drawBackground(canvas);
            }
    
            // skip step 2 & 5 if possible (common case)
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, draw the content
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, draw the children
                dispatchDraw(canvas);
    
                drawAutofilledHighlight(canvas);
    
                // Overlay is part of the content and draws beneath Foreground
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().dispatchDraw(canvas);
                }
    
                // Step 6, draw decorations (foreground, scrollbars)
                onDrawForeground(canvas);
    
                // Step 7, draw the default focus highlight
                drawDefaultFocusHighlight(canvas);
    
                if (debugDraw()) {
                    debugDrawFocus(canvas);
                }
    
                // we're done...
                return;
            }
        }
    

    step1:绘制背景时,首先根据滚动值对canvas的坐标进行调整,然后再恢复坐标, 图中外面是一个任意ViewGroup的实例,内部包含一个TextView对象,粗实线区域代表该TextView 在 ViewGroup中的位置, TextView中的文字由于滚动,一部分已经超出了粗实线区域,从而不可见。此时,如果调用canvas.getClipBoundsO返回的矩形区域是指粗实线所示的区域,该矩形的坐标是相对其 父视图ViewGroup的左上角,并且如果调用canvas的 getHeight()和 getWidth()方法将返回父视图的高度 和宽度,此处分别为200dip和 320dip。如 果ViewGroup中包含多个子视图,那么每个子视图内部的onDraw()函数中参数canvas的大小都是相同的,为父视图的大小。唯一不同的是“剪切区”,这个剪切区正是父视图分配给子视图的显示区 域 。 canvas之所以被设计成这样正是为了 View树的绘制,对于任何一个View而言,绘制时都可以认为原点坐标就是该View本身的原点坐标,从而 对 于View而言,当用户滚动屏幕时,应用程序只需要 调 用View类 的 scrollBy()函数即可,而不需要在onDraw()函数中做任何额外的处理,View的 onDraw() 函数内部可以完全忽略滚动值。 由于背景本身针对的是可视区域的背景,而 不 是 整 个 V iew 内部的背景,因此,本步中先调用translateO将原点移动到粗实线的左上角,从而使得背景Drawable对象内部绘制的是粗实线的区域。当绘制完背景后,还需要重新调用transalte()将原点坐标再移回到TextView本 身 的 (0 ,0 )坐标。

    step2:如果该程序员要求显示视图的渐变框,则需要先为该操作做一点准备,但是大多数情况下都不需要显示渐变框,因此,源码中针对这种情况进行快速处理,即略过该准备。

    step3:绘制视图本身,实 际 上 回 调onDraw()函数即可, View 的设计者可以在onDraw()函数中调用canvas的各种绘制函数进行绘制。

    step4:调 用 dispatchDraw()绘制子视图。如果该视图内部不包含子视图,则不需要重载该函数,而对所有 的ViewGroup实例而言,都必须重载该函数,否则它也就不是ViewGroup 了。

    其他的步骤我们就不再介绍了,相信大家都能搞明白。本问题的系统回答大家可以移步到高级UI课程&WMS课程中学习,有详细的视频讲解

    总结

    View的绘制流程是面试最容易被问到的问题,而且这个问题面试者非常容易满足于一知半解。这道问题的正确的回答方式是从viewRootImpl开始,解析整个viewTree的构建分发流程。

    二、View绘制流程与自定义View注意点

    这道题想考察什么?

    是否了解View绘制流程与自定义View注意点与实际场景使用,是否熟悉View绘制流程与自定义View注意事项

    考察的知识点

    View绘制流程与自定义View注意事项的概念在实际项目中使用

    考生应该如何回答

    这个问题先需要回答View的绘制流程,然后再回到自定义View的注意点。

    9.2.1. View的绘制流程

    这个问题的回答请看7-1题,View的绘制原理。

    9.2.2. 自定义View注意点?

    1) View需要实现四个构造函数

    自定义View中构造函数有四种

        //  主要是在java代码中new一个View时所调用,没有任何参数,一个空的View对象
        public ChildrenView(Context context) {
            super(context);
        }
        
        // 在布局文件中使用该自定义view的时候会调用到,一般会调用到该方法
        public ChildrenView(Context context, AttributeSet attrs) {
            this(context, attrs,0);
        }
        
        //如果你不需要View随着主题变化而变化,则上面两个构造函数就可以了
        //下面两个是与主题相关的构造函数
       public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, defStyleAttr, 0);
        }
    
        public ChildrenView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    

    四个参数解释:

    context:上下文

    AttributeSet attrs:在xml中定义的参数内容

    int defStyleAttr:主题中优先级最高的属性

    int defStyleRes: 优先级次之的内置于View的style(这里就是自定义View设置样式的地方),只有当defStyleAttr为0或者当前Theme中没有给defStyleAttr属性赋值时才起作用.

    在android中的属性可以在多个地方进行赋值,涉及到的优先级排序为:在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义View所在的Activity的Theme中指定style引用 > 构造函数中defStyleRes指定的默认值

    2)自定义View的种类各不相同,须要根据实际须要选择一种简单低成本的方式来实现,尽可能的减少UI的层级,view的种类如下,开发中需要尽可能的选择适合自己的。

    • 继承View重写onDraw方法

    主要用于实现不规则的效果,也就是说这种效果不适宜采用布局的组合方式来实现。也就是需要使用canvas,Paint,运用算法去“绘制”了。采用这种方式须要本身支持wrap_content,padding也须要本身处理canvas。

    • 继承ViewGroup派生特殊的Layout

    主要用于实现自定义的布局,看起来很像几种View组合在一块儿的时候,可使用这种方式。这种方式须要合适地处理ViewGroup的测量和布局,尤其在测量的时候需要注意padding 和margin,比如:自定义一个自动换行的LinerLayout等。

    • 继承特定的View,好比TextView

    这种方法主要是用于扩展某种已有的View,增长一些特定的功能。这种方法比较简单,也不须要本身支持wrap_content和padding。这种效果就相当于我们使用imageView来实现一个圆形的imageView。

    • 继承特定的ViewGroup,好比LinearLayout

    这种方式也比较常见,也就是基于原来已经存在的layout,在其上去添加新的功能。和上面的第2种方法比较相似,第2种方法更佳接近View的底层。

    3)View需要支持padding
    

    直接继承View的控件需要在onDraw方法中处理padding,否则用户设置padding属性就不会起作用。直接继承ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

    4)尽量不要在view中使用handler

    如果在view中需要使用handler来处理异步信息,这个时候尽量使用view中的post方法,因为view中给用户提供了post方法,这个方法的处理逻辑是:将Runable 的action 保存到一个队列,在viewRootImpl里面执行度量后再添加进Handler的MessageQueue中,这样就保障了action里面执行的内存是在完成度量 后的结果,避免了使用延迟消息带来的困扰具体的代码如下:

    public class View implements Drawable.Callback, KeyEvent.Callback,
            AccessibilityEventSource {
            
        ...
            
        public boolean post(Runnable action) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                return attachInfo.mHandler.post(action);
            }
        
            // Postpone the runnable until we know on which thread it needs to run.
            // Assume that the runnable will be successfully placed after attach.
            getRunQueue().post(action);
            return true;
        }
    
        public boolean postDelayed(Runnable action, long delayMillis) {
            final AttachInfo attachInfo = mAttachInfo;
            if (attachInfo != null) {
                return attachInfo.mHandler.postDelayed(action, delayMillis);
            }
        
            // Postpone the runnable until we know on which thread it needs to run.
            // Assume that the runnable will be successfully placed after attach.
            getRunQueue().postDelayed(action, delayMillis);
            return true;
        } 
        
        ...
    }
    

    5)在自定义view的onMeasure,onDraw,onLayout方法中尽量少使用局部变量

    由于onMeaaure &onDraw&onLayout这3个函数都可能存在频繁调用的可能,尤其在动画里面,onDraw是非常频繁的调度的,在这些频繁被调用的函数里面,开发过程中一定要尽量避免创建局部对象,尤其是比较占用内存的像bitmap等的局部对象,这很容易产生内存抖动,而内存抖动容易带来手机app的卡顿,具体的细节大家可以去享学课堂查找对应的课程进行学习。


    三、自定义view与viewgroup的区别

    这道题想考察什么?

    是否了解自定义view与viewgroup的区别与真实场景使用,是否熟悉自定义view与viewgroup的区别在工作中的表现是什么?

    考生应该如何回答

    说说自定义view与viewgroup的区别?

    Android的UI界面都是由View和ViewGroup及其派生类组合而成的。其中,View是所有UI组件的基类,而ViewGroup是容纳View及其派生类的容器,ViewGroup也是从View派生出来的。一般来说,开发UI界面都不会直接使用View和ViewGroup(一般在写自定义控件的时候使用),而是使用其派生类。

    image.png

    ViewGroup的职责是什么?

    ViewGroup相当于一个放置View的容器,在写布局xml的时候,会告诉容器:容器宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity),还有margin等。因此ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ,并决定childView的位置,将childView布局到这个Layout上面。 当然我们要知道为什么只是建议的宽和高,而不是直接确定呢?原因很简单,子View的宽和高可以设置为wrap_content,这样只有子View自己才能计算出自己的宽和高。 View的职责是什么?

    View的职责,根据测量模式和ViewGroup给出的建议宽和高,计算出自己的宽和高;另外还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形状,所以,view的主要职责会集中在实现onDraw方法。
    

    View和ViewGroup的区别: 可以从两方面来说:

        一.事件分发方面的区别;
        二.UI绘制方面的区别;
    

    1)事件分发方面的区别:

    事件分发机制主要有三个方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

        1.ViewGroup包含这三个方法,而View则只包含dispatchTouchEvent()、onTouchEvent()两个方法,不包含onInterceptTouchEvent()。
    
        2.触摸事件由Action_Down、Action_Move、Action_Up组成,一次完整的触摸事件,包含一个Down和Up,以及若干个Move(可以为0);
    
        3.在Action_Down的情况下,事件会先传递到最顶层的ViewGroup,调用ViewGroup的dispatchTouchEvent():a)如果ViewGroup的onInterceptTouchEvent()返回false不拦截该事件,则会分发给子View,调用子View的dispatchTouchEvent(),如果子View的dispatchTouchEvent()返回true,则调用View的onTouchEvent()消费事件;b)如果ViewGroup的onInterceptTouchEvent()返回true拦截该事件,则调用ViewGroup的onTouchEvent()消费事件,接下来的Move和Up事件将由该ViewGroup直接进行处理。
    
        4.当某个子View的dispatchTouchEvent()返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下来的Move和Up事件将由该子View直接进行处理。
    
        5.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch();触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。
    
        6..由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上层ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象。如ViewGroup0——ViewGroup1——TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,将被保存在ViewGroup0中;当Move和Up事件来时,会先从ViewGroup0传递到ViewGroup1,再由ViewGroup1传递到TextView,最后事件由TextView消费掉。
    
        7.子View可以调getParent().requestDisallowInterceptTouchEvent(),请求父ViewGroup不拦截事件。
    

    总之, viewGroup处理和分发事件要相对于View而言复杂很多,更多详细的关于事件分发的问题,大家可以参考后面的章节,我们会有详细的说明。

    2)UI绘制方面的区别:

    UI绘制主要有五个方法:onDraw(),onLayout(),onMeasure(),dispatchDraw(),drawChild(),

        1.ViewGroup包含这五个方法,而View只包含onDraw(),onLayout(),onMeasure()三个方法,不包含dispatchDraw(),drawChild(),当我们自定义View的时候,最主要的需要实现的方法是onDraw(),其他的方法可以使用View.java中的,不一定必须重写;如果需要改变view的大小,那么才需要重写onMeasure()方法;如果需要改变View的(在父控件的)位置,那么才需要重写onLayout()方法。同时,当我们在自定义ViewGroup的时候,最主要的需要实现的是onLayout()和onMeasure()方法,其他放到大多可以基础使用父ViewGroup的。
    
        2.绘制流程基本一直:onMeasure(测量)——》onLayout(布局)——》onDraw(绘制)。
    
        3.绘制按照ViewTree的顺序执行,视图绘制时会先绘制子控件。如果视图的背景可见,视图会在调用onDraw()之前调用drawBackGround()绘制背景。强制重绘,可以使用invalidate();
    
        4.如果发生视图的尺寸变化,则该视图会调用requestLayou(),向父控件请求再次布局。如果发生视图的外观变化,则该视图会调用invalidate(),强制重绘。如果requestLayout()或invalidate()有一个被调用,框架会对视图树进行相关的测量、布局和绘制。
        注意:视图树是单线程操作,直接调用其它视图的方法必须要在UI线程里。跨线程的操作必须使用Handler。
    
        5.onMeasure():用于计算自己及所有子对象的大小。这个方法是所有View、ViewGroup及其派生类都具有的方法。自定义控件时,可以重载该方法,重新计算所有对象的大小。 MeasureSpec包含了测量的模式和测量的大小,通过MeasureSpec.getMode()获取测量模式,通过MeasureSpec.getSize()获取测量大小。mode共有三种情况: 分别为MeasureSpec.UNSPECIFIED( View想多大就多大), MeasureSpec.EXACTLY(默认模式,精确值模式:将layout_width或layout_height属性指定为具体数值或者match_parent),MeasureSpec.AT_MOST( 最大值模式:将layout_width或layout_height指定为wrap_content)。
    
        6.onLayout():对于View来说,onLayout()只是一个空实现,一般情况下不需要重写;而对于ViewGroup来说,onLayout()使用了关键字abstract的修饰,要求其子类必须重载该方法,目的就是安排其children在父视图的具体位置。
    
        7.draw过程:drawBackground()绘制背景——》onDraw()对View的内容进行绘制——》dispatchDraw()对当前View的所有子View进行绘制——》onDrawScrollBars()对View的滚动条进行绘制。
        
        8.dispathDraw():ViewGroup及其派生类具有的方法,主要用于控制子View的绘制分发。自定义ViewGroup控件时,重载该方法可以改变子View的绘制,进而实现一些复杂的视效,在自定义View中不存在此方法。
        
        9.drawChild(Canvas canvas, View child, long drawingTime):ViewGroup及其派生类具有的方法,用于直接绘制具体的子View。自定义控件时,重载该方法可以直接绘制具体的子View。
    

    总结

    我们从事件分发和绘制两个角度全面剖析了自定义View和ViewGroup的区别,请大家在平时开发中,一定要多实践,只有在实践中才能真正的明白自定义View背后的逻辑。

    四、View的绘制流程是从Activity的哪个生命周期方法开始执行的

    这道题想考察什么?

    考察同学对Activity的生命周期和View的绘制流程是否熟悉

    考生应该如何回答

    View的绘制流程是从Activity的 onResume 方法开始执行的。 首先我们找到 onResume 在哪儿执行的,代码如下:

    // ActivityThread.java
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
        // 1 执行 onResume 流程
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    
        // 2 执行 View 的流程
        wm.addView(decor, l);
    }
    

    由上面1代码进入,我们继续跟进:

    public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
                String reason) {
        r.activity.performResume(r.startsNotResumed, reason);
    }
    
    // Activity.java
    final void performResume(boolean followedByPause, String reason) {
        mInstrumentation.callActivityOnResume(this);
    }
    
    public void callActivityOnResume(Activity activity) {
        activity.onResume();
    }    
    

    到这儿我们就找到了onResume方法的执行位置。而View的绘制就是由2代码进入:wm.addView 中的wm 就是 WindowManager,但是WindowManger是一个接口,实际调用的是 WindowManagerImpl 的 addView 方法

    // WindowManagerImpl.java
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                    mContext.getUserId());
    }
    

    mGlobal 是 WindowManagerGlobal 对象

    // WindowManagerGlobal.java
    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow, int userId) {
        root = new ViewRootImpl(view.getContext(), display);
        root.setView(view, wparams, panelParentView, userId);
    }
    
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
                int userId) {
        requestLayout();
    }
    

    到这儿我们可以看到,通过 requestLayout 开始绘制 View。 所以通过以上分析可以知道,在调用了 onResume 生命周期方法后,开始执行 View 的绘制。

    今天的面试分享到此结束拉~下期在见

    相关文章

      网友评论

          本文标题:全面总结Android面试知识要点:高级UI面试题

          本文链接:https://www.haomeiwen.com/subject/qmheedtx.html