自定义view和View、Window、Activity、Display之间的关系

           好就没有看csdn了,发现好多东西可以学了,唉,最近股市的震荡头有点痛,哈哈,搞个学习的心情都没有。然后一直拖到现在简历也没有改好,就还是要看一些东西,怕哪天心血来潮去面试随便一个东西都答不上来那就是有点蛋疼了。当问到自定义控件的时候,或许你知道怎么用,可能说起来也就不是那么回事了啊,所以还是有必要去把一些原理去搞清楚。

         一、View

       一般自定义控件的话呢,可实现的空间还是很大的,如果你只是想改变原控件的属性那就继承相对的控件就好了,但是我们更多的还是继承Layout、view/viewGroup等,这样的自定义空间会比较大点,所以我们还是讲讲view和viewGroup吧。

   View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。这就证明了一点,View代表了用户界面组件的一块可绘制的空间块。每一个View在屏幕上占据一个长方形区域。在这个区域内,这个VIEW对象负责图形绘制和事件处理。View是小控件widgets和ViewGroup的父类。ViewGroup又是Layout的基类。

   最简单的自定义就是加载布局,所以不得不说下setContentView(View view, ViewGroup.LayoutParams params)和LayoutInflater.inflate(layoutResID, mContentParent)的区别,

哈哈哈,先来看下我们每加载创建一个activity的onCreate(Bundle savedInstanceState)方法中都会有一个初始布局的加载,然后不难发现我们的setContentView()有3个构造方法:

 public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

因为这是api22的初始化,所以是actionBar的init,然后不得不知道phoneWindow这个类,PhoneWindow类的setContentView(int layoutResID)方法源码

 public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

@Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

可以看出,首先判断mContentParent是否为null,也就是第一次调运);如果是第一次调用,则调用installDecor()方法,否则判断是否设置FEATURE_CONTENT_TRANSITIONS Window属性(默认false),如果没有就移除该mContentParent内所有的所有子View;接着mLayoutInflater.inflate(layoutResID, mContentParent);将我们的资源文件通过LayoutInflater对象转换为View树,并且添加至mContentParent视图中(其中mLayoutInflater是在PhoneWindow的构造函数中得到实例对象的LayoutInflater.from(context);)。我们其实只用分析setContentView(View view, ViewGroup.LayoutParams params)方法即可,如果你在Activity中调运setContentView(View view)方法,实质也是调运setContentView(View view, ViewGroup.LayoutParams params),只是LayoutParams设置为了MATCH_PARENT而已。所以直接分析setContentView(View view, ViewGroup.LayoutParams params)方法就行,可以看见该方法与setContentView(int layoutResID)类似,只是少了LayoutInflater将xml文件解析装换为View而已,这里直接使用View的addView方法追加道了当前mContentParent而已。所以说在我们的应用程序里可以多次调用setContentView()来显示界面,因为会removeAllViews。

然后我们来看LayoutInflater.inflate()

LayoutInflater lif = LayoutInflater.from(Context context);

LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

这两方法很熟悉吧,接下来看源码

public View inflate(int resource, ViewGroup root) {  

return inflate(resource, root, root != null);  

}

 public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");


            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            //定义返回值,初始化为传入的形参root
            View result = root;
            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                //如果一开始就是END_DOCUMENT,那说明xml文件有问题
                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }
                //有了上面判断说明这里type一定是START_TAG,也就是xml文件里的root node
                final String name = parser.getName();
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }
                if (TAG_MERGE.equals(name)) {
                //处理merge tag的情况(merge,你懂的,APP的xml性能优化)
                    //root必须非空且attachToRoot为true,否则抛异常结束(APP使用merge时要注意的地方,
                    //因为merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容加到某个上层
                    //ViewGroup中。)
                    if (root == null || !attachToRoot) {
                        throw new InflateException(" can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //递归inflate方法调运
                    rInflate(parser, root, attrs, false, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //xml文件中的root view,根据tag节点创建view对象
                    final View temp = createViewFromTag(root, name, attrs, false);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        //根据root生成合适的LayoutParams实例
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            //如果attachToRoot=false就调用view的setLayoutParams方法
                            temp.setLayoutParams(params);
                        }
                    }
                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    // Inflate all children under temp
                    //递归inflate剩下的children
                    rInflate(parser, temp, attrs, true, true);
                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        //root非空且attachToRoot=true则将xml文件的root view加到形参提供的root里
                        root.addView(temp, params);
                    }
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        //返回xml里解析的root view
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            //返回参数root或xml文件里的root view
            return result;
        }
    }

从上面的源码分析我们可以看出inflate方法的参数含义:

  • inflate(xmlId, null); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。

  • inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。

到此其实已经可以说明我们上面示例部分执行效果差异的原因了(在此先强调一个Android的概念,下一篇文章我们会对这段话作一解释:我们经常使用View的layout_width和layout_height来设置View的大小,而且一般都可以正常工作,所以有人时常认为这两个属性就是设置View的真实大小一样;然而实际上这些属性是用于设置View在ViewGroup布局中的大小的;这就是为什么Google的工程师在变量命名上将这种属性叫作layout_width和layout_height,而不是width和height的原因了。),如下:

  • mInflater.inflate(R.layout.textview_layout, null)不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
  • mInflater.inflate(R.layout.textview_layout, parent)能正确显示我们设置的宽高是因为我们的View在设置setLayoutParams时params = root.generateLayoutParams(attrs)不为空。 Inflate(resId , parent,false ) 可以正确处理,因为temp.setLayoutParams(params);这个params正是root.generateLayoutParams(attrs);得到的。
  • mInflater.inflate(R.layout.textview_layout, null, true)与mInflater.inflate(R.layout.textview_layout, null, false)不能正确处理我们设置的宽和高是因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。
  • textview_layout_parent.xml作为item可以正确显示的原因是因为TextView具备上级ViewGroup,上级ViewGroup的layout_width,layout_height会失效,当前的TextView会有效而已。
自定义View
一般都会调用onMeasure(),onLayout(),onDraw()方法,那么我们分别来看源码:

先看下View的measure方法源码,如下:
   //final方法,子类不可重写
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        //回调onMeasure()方法
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
所以我们自定义控件不得不知道这个方法,知道这个方法才能知道view的控件的大小。
  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //通过MeasureSpec解析获取mode与size
        int specMode = MeasureSpec.getMode(measureSpec);
        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;
    }
看见没有,如果specMode等于AT_MOST或EXACTLY就返回specSize,也就3种状态,UNSPECIFIED我基本上觉得是不需要管的。通过取得的大小我们就会setMeasuredDimension()然后控件的大小就这样的确定了。
接着是OnLayout():
先看下ViewGroup的layout方法,如下:
 @Override
    public final void layout(int l, int t, int r, int b) {
        ......
        super.layout(l, t, r, b);
        ......
    }
ViewGroup的layout方法实质还是调运了View父类的layout方法,所以我们看下View的layout源码,如下:
 public void layout(int l, int t, int r, int b) {
        ......
        //实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
        //判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //需要重新layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //回调onLayout
            onLayout(changed, l, t, r, b);
            ......
        }
        ......
    }

对比上面View的layout和ViewGroup的layout方法可以发现,View的layout方法是可以在子类重写的,而 ViewGroup的layout是不能在子类重写的,言外之意就是说ViewGroup中只能通过重写onLayout方法。那我们接下来看下 ViewGroup的onLayout方法,如下:
 @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
ViewGroup的onLayout()方法竟然是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自 定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用 onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout 的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。
ViewGroup的onLayout重写一般步骤就拿这里的VERTICAL模式来解释吧,如下是layoutVertical方 法源码:
void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
 
        int childTop;
        int childLeft;
 
        // Where right end of child should go
        //计算父窗口推荐的子View宽度
        final int width = right - left;
        //计算父窗口推荐的子View右侧位置
        int childRight = width - mPaddingRight;
 
        // Space available for child
        //child可使用空间大小
        int childSpace = width - paddingLeft - mPaddingRight;
        //通过ViewGroup的getChildCount方法获取ViewGroup的子View个数
        final int count = getVirtualChildCount();
        //获取Gravity属性设置
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
        //依据majorGravity计算childTop的位置值
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;
 
               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;
 
           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
        //重点!!!开始遍历
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            else if (child.getVisibility() != GONE) {
                //LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                //获取子View的LayoutParams
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
 
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                //依据不同的absoluteGravity计算childLeft位置
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;
 
                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;
 
                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }
 
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
 
                childTop += lp.topMargin;
                //通过垂直排列计算调运child的layout设置child的位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
 
                i += getChildrenSkipCount(child, i);
            }
        }
    }
getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight() 这两对方法之间的区别(上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在 onMeasure之后使用才有效)。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效.
接下来就OnDraw(),里面带了canvas画布所以我们就最好不需要去new一个对象了,因为这方法会反复的调用,如果多次的去实例化就会造成极大的内存浪费,影响性能:
当ViewRootImpl的performTraversals中measure和layout执行完成以后会接着执行mView.layout,具体如下:
private void performTraversals() {
    ......
    final Rect dirty = mDirty;
    ......
    canvas = mSurface.lockCanvas(dirty);
    ......
    mView.draw(canvas);
    ......
}
draw过程也是在ViewRootImpl的performTraversals()内部调运的,其调用顺序在measure()和 layout()之后,这里的mView对于Actiity来说就是PhoneWindow.DecorView,ViewRootImpl中的代码会创 建一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工。所以又回归到了ViewGroup与View的树状递归draw过程。
ViewGroup没有重写View的draw方法,所以如下直接从View的draw方法开始分析:
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
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
 
        // skip step 2 & 5 if possible (common case)
        ......
 
        // Step 2, save the canvas' layers
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......
 
        // 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
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......
 
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        ......
    }

然后绘制的话先会绘制背景再绘制内容接着绘制子view,

绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

  • View的绘制是借助onDraw方法传入的Canvas类来进行的。

  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对 ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对 LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

最后调用View的invalidate和postInvalidate来进行界面的刷新就好。

View、Window、Activity、Dispay之间的关系

我们首先来了解Dispay。Dispay代表了硬件显示屏幕信息。

 

 

通过这些函数可以了解一个屏幕的宽、高及分辨率还有是横屏还坚屏等一些基本情况,透过这些函数,我们开发应用时可以方便的得到当前安装我这个应用的屏幕的大小,以便调整应用使用户得到更好的用户体验。接下来我们看其它三者之间的关系,我想大家虽然看了前面的View的介绍和SDK中关系UI的基本介绍之后还是对Android图形窗口十分困惑,看API也是,有WindowMangaer接口和Window类,但是在说明文档中,并未提到如何用这些。但实际上这里面要去看到Android核心,Android核心底层GDI是SKIA,同chrome是一样的GDI,但是GUI是不一样的。这里面Android实现的是C/S模式。如下图所示

 

 

从上图,我们可以理出大致的显示过程如下:
1、ActivityManagerService创建Activity线程,激活一个activity
2、系统调用Instrumentation.newActivity创建一个activity
3、Activity创建后,attach到一个新创建的phonewindow中。这样Activity获取一个唯一的WindowManager服务的实例
4、Activity创建过程中使用setcontentView设置用用户UI,这些VIEW被加入到PhoneWindow的ContentParent中。
5、Activity线程继续执行,当执行到Activity.makeVisible是将根view DecoView加入到WindowManger中,WindowManger实全会为每个DecoView创建对应的ViewRoot
6、每个ViewRoot拥有一个Surface,每个Surface将会调用底层库创建图形绘制的内存空间。这个底层库就是SurfaceFlinger。SurfaceFlinger同时也负责将个View绘制的图形合到一块(按照Z轴)显示到用户屏幕。
7、如果用户直接在Canvas上绘制,实际上它直接操作Surface。但对每个View的变更,它是要通知到ViewRoot,然后ViewRoot获取Canvas。如果绘制完成,surfaceFlinger得到通知,合并Surface成一个Surface到设备屏幕。
从上面的图形输出过程分析,我们可以知道真正显示图形的实际上跟Activity没有关系,完全由WindowManager来决定。WindowManager是一个系统服务,因此可以直接调用这个服务来创建界面,并且更绝的是Dialog、Menu也是有WindowManager来管理的。另外一个我们也可以看到,最底层都是Surface来,因此,常见开发游戏的人都推荐你使用SurfaceView来创建界面。

你可能感兴趣的:(android)