Android自定义View 基础篇

Posted mChenys

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义View 基础篇相关的知识,希望对你有一定的参考价值。

目录

一、基本结构

组件主要由两部分构成:组件类和属性定义。我们从第一种定义方式说起创建自定义组件类最基本的做法就是继承自类 View,其中,有三个构造方法和两个重写的方法又是重中之重。下面是自定义组件类的基本结构:

public class MyView extends View 
    public MyView(Context context) 
        super(context);
    

    public MyView(Context context, AttributeSet attrs) 
        super(context, attrs);
    

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) 
        super(context, attrs, defStyleAttr);
    

    @Override
    protected void onDraw(Canvas canvas) 
        super.onDraw(canvas);
    

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    


上述代码中,我们定义了一个名为 MyView 的类,该类继承自 View,同时,为该类定义了三个构造方法并重写了另外两个方法:

构造方法

public MyView(Context context) 
public MyView(Context context, AttributeSet attrs) 
public MyView(Context context, AttributeSet attrs, int defStyleAttr)

这三个构造方法的调用场景其实并不一样,第一个只有一个参数,在代码中创建组件时会调用该构造方法,比如创建一个按钮:

Button btnOK = new	Button(this); 

第二构造个方法在 layout 布局文件中使用时调用,参数 attrs 表示当前配置中的属性集合,例如在要 layout.xml 中定义一个按钮:

<Button	
    android:layout_width="match_parent"	
    android:layout_height="wrap_co-ntent"	 	
    android:text="OK"
/>

Android 会调用第二个构造方法 Inflate出Button对象。而第三个构造方法是不会自动调用的,当我们在 Theme 中定义了 Style 属性时通常在第二个构造方法中手动调用。

绘图

protected void onDraw(Canvas canvas)

该方法我们再熟悉不过了,前面Android绘图篇一直重写了该方法,用于显示组件的外观。最终的显示结果需要通过 canvas 绘制出来。在 View 类中,该方法并没有任何的默认实现。

测量尺寸

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec)

这是一个 protected 方法,意味着该方法主要用于子类的重写和扩展,如果不重写该方法,父类 View 有自己的默认实现。在 Android 中,自定义组件的大小都由自身通过onMeasure()进行测量,不管界面布局有多么复杂,每个组件都负责计算自己的大小。

1.1 重写onMeasure 方法

View 类对于 onMeasure()方法有自己的默认实现

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    // 真正指定宽高的方法是setMeasuredDimension
    setMeasuredDimension(getDefaultSize(  
            getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));


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:
    // 可以看到无论子View的宽高布局属性是wrap_content、match_parent、具体的dp,
    // 得到的大小都是MeasureSpec.getSize(measureSpec)返回的,而measureSpec是由父容器计算而来的
        result = specSize; 
        break;
    
    return result;

大部分情况下onMeasure方法都要重写,用于计算组件的宽度值和高度值。定义组件时,必须指定 android:layout_width和android:layout_height 属性,属性值有三种情况:match_parent、wrap_content 和具体值。

  1. match_parent: 表示组件的大小跟随父容器,所在的容器有多大,组件就有多大;
  2. wrap_content: 表示组件的大小由内容决定,比如 TextView 组件的大小由文字的多少决定,ImageView 组件的大小由图片的大小决定;
  3. 具体值: 这个相对就简单了,直接指定即可,单位为 dp。

总结来说,不管是宽度还是高度,都包含了两个信息:模式和大小
模式可能是 match_parent、wrap_content 和具体值的任意一种;大小则要根据不同的模式进行计算。
其实,match_parent 也是一个确定了的具体值,为什么这样说呢?因为 match_parent 的大小跟随父容器,而容器本身也是一个组件,他会算出自己的大小,所以我们根本不需要去重复计算了,父容器多大,组件就有多大,View 的绘制流程会自动将父容器计算好的大小通过参数传过来。

1.2 三种测量模式

模式使用三个不同的常量来区别:
MeasureSpec.EXACTLY

当组件的尺寸指定为 match_parent 或具体值时用该常量代表这种尺寸模式,很显然,处于该模式的组件尺寸已经是测量过的值,不需要进行计算。

MeasureSpec.AT_MOST

当组件的尺寸指定为 wrap_content 时用该常量表示,因为尺寸大小和内容有关,所以,我们要根据组件内的内容来测量组件的宽度和高度。比如 TextView 中的 text 属性字符串越长,宽度和高度就可能越大。

MeasureSpec.UNSPECIFIED

未指定尺寸,这种情况不多,一般情况下,父控件为 AdapterView 时,通过 measure 方法传入。

通过如下方法可以获取当前View的测量模式和尺寸大小,它是由父容器计算后返回的:

int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);

而widthMeasureSpec和heightMeasureSpec刚好就是来自重写的onMeasure方法的参数,它是由父容器测量后传入的,他代表当前子View的宽度测量模式和高度测量模式。

1.3 子View的onMeasure方法参数测量模式的由来

为什么说View的onMeasure方法的参数是由父容器计算返回的呢?我们可以查看ViewGroup的源码,ViewGroup没有重写View的onMeasure方法,但是提供了measureChildren和measureChildWithMargins两个有用的方法,这两个方法最终都会调用子View的measure方法

/**
 * 测量子View
 * @param child 要测量的子View
 * @param parentWidthMeasureSpec 父容器的宽度模式
 * @param parentHeightMeasureSpec 父容器的高度模式
 */
protected void measureChild(View child, int parentWidthMeasureSpec,
                            int parentHeightMeasureSpec) 
    final LayoutParams lp = child.getLayoutParams();
    // 计算子View的宽度测量模式
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    // 计算子View的高度测量模式
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    // 最终会触发View的onMeasure方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);


/**
 * 测量子View
 * @param child 要测量的子View
 * @param parentWidthMeasureSpec 父容器的宽度测量模式
 * @param widthUsed 父容器已使用的宽度
 * @param parentHeightMeasureSpec 父容器高度测量模式
 * @param heightUsed 父容器已使用的高度
 */
protected void measureChildWithMargins(View child,
                                       int parentWidthMeasureSpec, int widthUsed,
                                       int parentHeightMeasureSpec, int heightUsed) 
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    // 计算子View的宽度测量模式
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    // 计算子View的高度测量模式
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
                    
    // 最终会触发View的onMeasure方法
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

在调用 child.measure方法传入的2个测量模式其实是经过getChildMeasureSpec方法计算而来的,我们来看看ViewGroup的getChildMeasureSpec源码如下所示:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) 
    // 获取父容器的模式和大小
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    // 计算最大的可用大小
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) 
        // 父容器模式是精确模式
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) 
                // 如果子view布局的宽高是固定值,那么直接使用子View的固定值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) 
                // 如果子View布局宽高是匹配父容器,那么使用父容器的size
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.WRAP_CONTENT) 
                // 如果子view布局宽高是包裹类型,那么最多的大小不能超过父容器的size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            
            break;
    
        // 父容器是包裹类型
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) 
                // 如果子View的布局宽高是固定值,那么直接使用子View的固定值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) 
                // 如果子View的布局宽高是匹配父容器,那么子View的大小最大不能超过父容器的size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
             else if (childDimension == LayoutParams.WRAP_CONTENT) 
                // 如果子view的布局宽高是包裹内容,那么子View的大小最大不能超过父容器的size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            
            break;
    
        // 父容器未指定模式
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) 
                // 如果子View的布局宽高是固定值,那么直接使用子View的固定值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
             else if (childDimension == LayoutParams.MATCH_PARENT) 
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
             else if (childDimension == LayoutParams.WRAP_CONTENT) 
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            
            break;
        
    // 计算子View的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

然后View的measure方法最终会触发View的onMeasure方法,由此可见我们重写View的onMeasure方法得到的2个参数widthMeasureSpec和widthMeasureSpec其实是经过父容器计算后返回的,不信可以查看DecorView的onMeasure方法:

上图可知DecorView调用的是父类的onMeasure方法,而DecorView的父类是FrameLayout,我们来瞧瞧

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    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) 
    	 // 关键代码:这里调用ViewGroup的方法测量子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) 
                    // 这里是记录使用了匹配父窗口的子view 
                    mMatchParentChildren.add(child);
                
            
        
    

    // 计算最大宽度和高度
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) 
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    
    // 这里设置FrameLayout的宽度和高度,如果是根FrameLayout,那就是屏幕的宽高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // 下面的逻辑是计算使用了匹配父窗口的子view的特殊处理,可以不关注
    count = mMatchParentChildren.size();
    if (count > 1) 
        for (int i = 0; i < count; i++) 
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) 
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
             else 
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            

            final int childHeightMeasureSpec;
            if (lp.height == LayoutParams.MATCH_PARENT) 
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
             else 
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        
    

从上面的关键代码也可以知道,FrameLayout调用measureChildWithMargins方法后子View最终会回调onMeasure方法,并且入参是经过父View计算后的宽度和高度测量模式。如果子View不重写onMeasure方法,那么就采用View的默认处理,基本上就是随父容器的大小了,除非父容器设置了padding。

1.4 子View的onMeasure重写的基本写法

对于继承View的自定义View,我们可以重写onMeasure方法调用setMeasuredDimension方法传入具体的宽高,这样我们的自定义View就可以通过getMeasureWidth和getMeasureHeight得到宽高了.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
    // 测量子View的宽高,传入子View的宽度和高度测量模式,此测量模式是父容器计算后传入的
    int width = measureWidth(widthMeasureSpec);
    int height = measureHeight(heightMeasureSpec);
    // 设置子View的宽高
    setMeasuredDimension(width, height);


private int measureWidth(int widthMeasureSpec) 
    // 获取子View的宽度测量模式
    int mode = MeasureSpec.getMode(widthMeasureSpec);
    // 获取子View的宽度size
    int size = MeasureSpec.getSize(widthMeasureSpec);
    int width = 0;
    if (mode == MeasureSpec.EXACTLY) 
        //子View宽度为 match_parent或者具体值时,直接将 size 作为组件的宽度
        width = size;
     else if (mode == MeasureSpec.AT_MOST) 
        //宽度为 wrap_content,宽度需要计算
    
    return width;


private int measureHeight(int heightMeasureSpec) 
    // 获取子View的高度测量模式
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    // 获取子View的高度size
    int size = MeasureSpec

以上是关于Android自定义View 基础篇的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View基础篇

Android Studio开发基础之自定义View组件

Android自定义 View 系列实战篇-ViewGroup

Android自定义 View 系列实战篇-ViewGroup

Android进阶技术分享之——自定义 View 系列实战篇-ViewGroup(内含自定义View 宝藏图)

Android 自定义View控件