Android--自定义控件(二)

View的绘制流程

Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View的

每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()

一. onMeasure()

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

MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。

1. EXACTLY

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

2. AT_MOST

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

3. UNSPECIFIED(不常用unspecified)

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

通常情况下,这两个值是父视图通过计算传给子视图的,说明父视图会在一定程度上决定子视图的大小。但是最外层的根视图,它的widthMeasureSpec和heightMeasureSpec又是从哪里得到的呢?这就需要去分析ViewRoot中的源码了,观察performTraversals()方法可以发现如下代码:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  

可以看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。

private int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}  
private int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}  

可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。

并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。

介绍了这么多MeasureSpec相关的内容,接下来我们看下View的measure()方法里面的代码吧,如下所示:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
        }
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }
        mPrivateFlags |= LAYOUT_REQUIRED;
    }
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}  

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

public static int getDefaultSize(int size, int measureSpec) {
    int result = 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;
}  

这里传入的measureSpec是一直从measure()方法中传递过来的。

然后调用MeasureSpec.getMode()方法可以解析出specMode,调用MeasureSpec.getSize()方法可以解析出specSize。

接下来进行判断,如果specMode等于AT_MOST或EXACTLY就返回specSize,这也是系统默认的行为。之后会在onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

当然,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。

由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,

  1. 父视图会提供给子视图参考的大小,
  2. 而开发人员可以在XML文件中指定视图的大小,
  3. 然后视图本身会对最终的大小进行拍板。

二. onLayout()

View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。既然如此,我们来看下ViewGroup中的onLayout()方法是怎么写的吧

protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

可以看到,ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。

public class SimpleLayout extends ViewGroup {  

    public SimpleLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }  

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {  //判断SimpleLayout 是否包含一个子布局
            View childView = getChildAt(0);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);  //测量子视图的大小
        }
    }  

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());  //确定它在SimpleLayout布局中的位置
        }  这里传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分别代表着子视图在SimpleLayout中左上右下四个点的坐标。其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高。
    }
} 
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >  

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher"
        />  

</com.example.viewtest.SimpleLayout>  

我们能够像使用普通的布局文件一样使用SimpleLayout,只是注意它只能包含一个子视图,多余的子视图会被舍弃掉。这里SimpleLayout中包含了一个ImageView,并且ImageView的宽高都是wrap_content。现在运行一下程序,结果如下图所示:

  在onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高了

  getWidth()方法和getMeasureWidth()方法到底有什么区别呢?它们的值好像永远都是相同的。其实它们的值之所以会相同基本都是因为布局设计者的编码习惯非常好,实际上它们之间的差别还是挺大的。

   首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

三. onDraw()绘制

measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。

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

第三步完成之后紧接着会执行第四步,这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

以上都执行完后就会进入到第六步,也是最后一步,这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。

通过以上流程分析,相信大家已经知道,View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西

public class MyView extends View {
    private Paint mPaint;  

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }  

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.YELLOW);
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
        mPaint.setColor(Color.BLUE);
        mPaint.setTextSize(20);
        String text = "Hello View";
        canvas.drawText(text, 0, getHeight() / 2, mPaint);
    }
}  
时间: 2024-08-25 01:01:42

Android--自定义控件(二)的相关文章

android 自定义控件二之仿QQ长按删除

自定义Dialog 1.先上个效果图: 虽然效果丑了点,但主要学习修改已有的控件,例如修改Dialog控件 2.一些基本的只是进行了解 Dialog: theme是Dialog的样式,常用样式为: <style name="MyDialogStyle" parent="@android:Theme.Dialog"> <item name="android:windowFrame">@null</item> &l

android自定义控件(二) 入门,继承View

转载请注明地址:http://blog.csdn.net/ethan_xue/article/details/7313788 ps: 可根据apidemo里LableView,list4,list6学习 文档在dev guide/Framework Topics/User Interface/Building Custom Components 自定义控件的步骤: 1 View的工作原理  2 编写View类  3 为View类增加属性  4 绘制屏幕  5 响应用户消息  6 自定义回调函数

玩转android自定义控件二——自定义索引栏listview

带索引栏的listview,在android开发非常普遍,方便用户进行字母索引,就像微信通讯录这样: 今天,我们就从零到一实现这个具有索引栏的listview. 怎么实现这个控件了,我们应当梳理出一个思路. ①首先应当将字母的索引栏继承与一个控件,通过ondraw方法将字母画出来. ②然后我们应该监听这个字母控件的ontouch事件,来判断用户到底是按了那个字母. ③就是实现这个索引栏与listview的联动,就是将listview滑动到按下字母的位置. 大体流程图如下: 有了前面铺垫,我们引出

老猪带你玩转android自定义控件二——自定义索引栏listview

带索引栏的listview,在android开发非常普遍,方便用户进行字母索引,就像微信通讯录这样: 今天,我们就从零到一实现这个具有索引栏的listview. 怎么实现这个控件了,我们应当梳理出一个思路. ①首先应当将字母的索引栏继承与一个控件,通过ondraw方法将字母画出来. ②然后我们应该监听这个字母控件的ontouch事件,来判断用户到底是按了那个字母. ③就是实现这个索引栏与listview的联动,就是将listview滑动到按下字母的位置. 大体流程图如下: 有了前面铺垫,我们引出

android 自定义控件 (二) 初步认识

最近一直在忙项目,也在不断的面试,每次问道这个自定义控件,好多人云里雾绕的,今天就这个机会,简单做个完全自定义控件的入门吧.上一篇讲了继承已有控件的过程,发现我们只是简答的在封装的布局里操作,并没有重写onDraw,onMeasure,onLayout这些方法.其实继承控件这种形式基本能满足我们大部分的功能,但对于现有控件无法满足的怎么办,那就让我们重写上述三个方法,自己写特定需求的控件.完全自定义控件一般有两种,一种继承View,一种继承Viewgroup.根据view树结构,我们知道View

Android 自定义控件开发入门(二)

上一次我们讲了一堆实现自定义控件的理论基础,列举了View类一些可以重写的方法,我们对这些方法的重写是我们继承View类来派生自定义控件的关键 我通过一个最简单的例子给大家展示了这一个过程,无论是多么复杂的自定义控件,思路总是这样子的,但是因为我们仅仅重写了onDraw方法使得大家觉得怪怪的,作为一个控件,我们居然还要为了他的实现为其增加麻烦的监听,这就不能叫做控件了. 下面再给大家介绍一个经常重写的方法法:publicboolean onTouchEvent (MotionEvent even

android自定义控件(二)Canvas

一.重要方法 1.translate2.scale3.rotate 二.注意 1.明确顺序 canvas.rotate(45); canvas.drawRect(new Rect(50, 50, 100, 100), paint); 如果顺序调换,则没有旋转的效果 2.转换的时候,需要把转换的中心点移到shape自身的中心 int left=50,top=50, right = 100,bottom = 100; canvas.translate(right/2, bottom/2); canv

Android自定义控件的实现步骤(二)

前天已经写了一个关于自定义控件的实现步骤的博客,这个是附上代码的详细版本 首先,我们得新建一个attrs.xml的资源文件,在上面添加我们将要自定义控件的额外属性,就是自定义控件的自定义属性,具体代码如下: <resources> <declare-styleable name="TestView"> <attr name="textColor" format="color"></attr> <

Android自定义控件View(二)

在前一篇博客中学习了Android自定义控件View的流程步骤和注意点,不了解的童鞋可以参考Android自定义控件View(一).这一节开始学习自定义控件View(二)之继承系统已有的控件.我们来自定义一个圆形ImageView. RoundImageView 随着Android UI效果越来越炫,很多系统自带的控件已经无法满足日常开发需求,比如很多应用的头像是圆形的,QQ头像就是圆形的图片.但是Android系统提供的控件当中没有一个是圆形的.那么怎么才能实现圆形头像效果呢?两种方法: 图片

Android自定义控件系列八:详解onMeasure()(二)--利用onMeasure测量来实现图片拉伸永不变形,解决屏幕适配问题

上一篇文章详细讲解了一下onMeasure/measure方法在Android自定义控件时的原理和作用,参看博文:Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一),今天就来真正实践一下,让这两个方法大显神威来帮我们搞定图片的屏幕适配问题. 请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45038329,非允许请勿用于商业或盈利用途,违者必究. 使用ImageView会遇到