Android View 小结

2015/6/9 posted in  Android

本文介绍了 Android 自定义 View 的基本步骤

LayoutInflater分析

功能:加载布局的

Activity中调用setContentView()方法也是调用LayoutInflater()完成加载布局的

使用步骤

-获取LayoutInflater的实例

法1:

LayoutInflater layoutInflater = LayoutInflater.from(context);  

法2:

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

-使用inflate()方法加载布局

layoutInflater.inflate(resourceId, root);  

参数1:需要加载的布局Id

参数2:是指给该布局的外部再嵌套一层父布局,如果不需要就直接传null。这样就成功成功创建了一个布局的实例,之后再将它添加到指定的位置就可以显示出来了。

举例

定义一个button_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="button">

</Button>

MainActivity中


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        mainLayout = (LinearLayout) findViewById(R.id.activity_main);
        View buttonLayout = layoutInflater.inflate(R.layout.bytton_layout, null);
        mainLayout.addView(buttonLayout);
    }

此时即可将button_layout.xml加载进来

测试若想调整button_layout.xml的大小,修改android:layout_width是没有效果的,必须将button_layout.xml 放入一个layout中,跳转layout的大小来。

而setContentView()方法中的子控件可以调整大小是因为,Android会自动在布局文件的最外层再嵌套一个FrameLayout,所以layout_width和layout_height属性才会有效果。

分析源码

View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot):pull解析xml

->View temp = createViewFromTag(name, attrs); 根据节点名来创建View对象的

    ->createView() 创建view实例并且返回

->rInflate()循环遍历这个根布局下的子元素

    -> rInflate()递归调用,查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中

View的绘制过程

onMeasure()

功能:测量视图的大小的

View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。

MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:

-EXACTLY

表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

-AT_MOST

表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

-UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

widthMeasureSpec和heightMeasureSpec两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小

measure()这个方法是final的,因此我们无法在子类中去重写这个方法,说明Android是不允许我们改变View的measure框架的。然后在第9行调用了onMeasure()方法,这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小,

onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:

public class MyView extends View {  
  
    ......  
      
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        setMeasuredDimension(200, 200);  
    }  
  
} 

这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

onLayout()

ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程

View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。

onDraw()

ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:

public void draw(Canvas canvas) {  
    if (ViewDebug.TRACE_HIERARCHY) {  
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
    }  
    final int privateFlags = mPrivateFlags;  
    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  
    // Step 1, draw the background, if needed  
    int saveCount;  
    if (!dirtyOpaque) {  
        final Drawable background = mBGDrawable;  
        if (background != null) {  
            final int scrollX = mScrollX;  
            final int scrollY = mScrollY;  
            if (mBackgroundSizeChanged) {  
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                mBackgroundSizeChanged = false;  
            }  
            if ((scrollX | scrollY) == 0) {  
                background.draw(canvas);  
            } else {  
                canvas.translate(scrollX, scrollY);  
                background.draw(canvas);  
                canvas.translate(-scrollX, -scrollY);  
            }  
        }  
    }  
    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);  
        // Step 6, draw decorations (scrollbars)  
        onDrawScrollBars(canvas);  
        // we're done...  
        return;  
    }  
}

视图重绘

调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现

-> invalidate()

->skipInvalidate()判断是否许重绘

-> invalidateChild()

    -> invalidateChildInParent()

        -> invalidateChild()

            -> scheduleTraversals()

                -> sendEmptyMessage(DO_TRAVERSAL); 

                    handler 接受

                    - > performTraversals() [onmeasure的入口]

    不断地获取当前布局的父布局,并调用它的invalidateChildInParent()方法,当循环到最外层的根布局后,就会调用ViewRoot的invalidateChildInParent()方法了,仅仅是去调用了invalidateChild()方法而