手把手写Android自定义控件(三):测量宽高与绘制

上一篇讲解了自定义属性的相关操作,本篇来讲解如何测量控件。相比于前面的步骤,测量工作的复杂了许多,在这个阶段建议准备一张草稿纸记录各种思路和计算结果,这样不容易乱。下面是我在设计WaveLoadingView时的草稿。
手把手写Android自定义控件(三):测量宽高与绘制_第1张图片

认识MeasureSpec

在正式开始写测量代码前,首先需要知道一个重要的参数,MeasureSpec。
它是一个32位的整型数据,由 模式长度 组成,它的结构如下。
手把手写Android自定义控件(三):测量宽高与绘制_第2张图片
其中0 ~ 29位封装了具体的尺寸值(像素个数),30 ~ 31位封装了模式。

模式有三种:EXACTLY,AT_MOST 和 UNSPECIFIED

EXACTLY:使用具体的尺寸值(如10dp)或match_parent
AT_MOST:使用wrap_content
UNSPECIFIED:父布局不对本控件尺寸作要求

UNSPECIFIED 是用在布局控件上的,这里不做过多说明。如此一来我们需要关心的就只有EXACTLY 和 AT_MOST 了。

获取尺寸值和模式用到如下两个方法。

int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);

举两个例子。

<TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="hello world"/>

<Button
        android:layout_width="100px"
        android:layout_height="50px"/>

TextView的宽度为0,宽度模式为EXACTLY,高度为某个固定值,高度模式为AT_MOST。

而Button的宽度为100,宽度模式为EXACTLY,高度为50,高度模式也为EXACTLY。

重写onMeasure方法

onMeasure方法是控件用来测量控件本身大小的,做好了前期的准备,现在就来重写这个方法。

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

        //获取xml上的宽高尺寸及模式
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        
        //wrap_content
        int wrapWidth = mRadius * 2 * 2;
        int wrapHeight = mRadius * 2;

		//1
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            width = wrapWidth;
            height = wrapHeight;
        //2
        }else if(widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.AT_MOST){
            height = wrapHeight;
        //3
        }else if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.EXACTLY){
            width = wrapWidth;
        //4
        }else{
        }

        setMeasuredDimension(MeasureSpec.makeMeasureSpec(width,widthMode),MeasureSpec.makeMeasureSpec(height,heightMode));

		getPaddingAttr();
    }

首先获取宽高的尺寸值和模式。

接着计算一下wrap_content所需的尺寸。规定最小高度为mRadius的两倍(也就是直径),最小宽度为mRadius的四倍(也就是两倍直径)。
手把手写Android自定义控件(三):测量宽高与绘制_第3张图片
接着有四个判断分支。分别是
1.宽高都取最小
2.宽取具体值,高取最小值
3.宽取最小值,高取具体值
4.宽高都去具体值

计算好测量结果后调用setMeasuredDimension方法,把宽高尺寸放入。

这里回顾一下,当宽度使用match_parent时,获取的尺寸值为0,模式为EXACTLY。setMeasuredDimension方法传入的宽度虽然为0,但父布局是会把这个控件的宽度设置为和父布局一样宽的。

onMeasure的难度跨度有点大,还没弄明白的朋友可以先在草稿纸上演算一下。

最后是获取Padding(内间距),getPaddingAttr方法如下。

private void getPaddingAttr(){
        //获取控件上下左右的内间距
        mPaddingRight = getPaddingRight();
        mPaddingLeft = getPaddingLeft();
        mPaddingTop = getPaddingTop();
        mPaddingBottom = getPaddingBottom();
}

如果能把padding也考虑进去,那离优秀的控件就又近了一小步。

完成了测量接下来就是绘制控件了。

重写onDraw方法

在此之前,简单提一下。其实控件从初始化到真正显示出来是要先后调用onMeasure,onLayout 和 onDraw 三个方法的。

onMeasure所测量出来的尺寸只是一个初步的结果。如果现在设计的是布局控件,那么还得考虑子控件之间的位置关系。LinearLayout 和 RelativeLayout 对控件摆放的策略就不一样。摆放的结果就是在onLayout中计算的,这个时候得到的就是布局控件真正的尺寸了。

不过由于我们目前写的Switch控件并不是布局控件,所以可以不用考虑重写onLayout方法。

先写一个init方法,在 所有的 构造函数里调用它。Paint 是画笔类,控件的图像就是通过它画出来的。再定义一个布尔量记录当前的开关状态。

private Paint mPaint;
private boolean switchOn;

private void init(){
        mPaint = new Paint();
        mPaint.setAlpha(255);
        mPaint.setAntiAlias(true);
        switchOn = false;
}

setAntiAlias开启反锯齿,这样绘制出来的图像质量会好很多。当然,会对性能造成细微的损耗。

接下来讲Android坐标系,这个非常重要
手把手写Android自定义控件(三):测量宽高与绘制_第4张图片
在Android设备里,从左到右是x轴正方向从上到下是y轴正方向
左上角是原点(0,0)

先来做个小练习吧。控件宽度为100,,高度为50,求控件正中央的坐标。红线长度为30,求黑点坐标。
手把手写Android自定义控件(三):测量宽高与绘制_第5张图片
题解:控件左上角是原点坐标(蓝点),根据刚刚说的Android坐标系,红点坐标为(50,25)。

黑点在原点(0,0)的左边,红线长度为30,所以黑点坐标为(-30,0)。

如果一个点在原点上方,则 y 坐标为负。

做好了必要的准备,接下来重写onDraw方法。

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

        drawInside(canvas);

        if(switchOn){
            drawOn(canvas);
        }else{
            drawOff(canvas);
        }
}

默认情况下,后面绘制的东西会叠在最上面。所以这里绘制的顺序是 底部 -> 凹槽 -> 开关。

先来写drawBack方法,这个方法绘制控件底部,其实就是两个圆中间放一个矩形。

protected void drawBack(Canvas canvas){
        int width = getWidth();

        mPaint.setColor(mBackColor);
        canvas.drawCircle(mPaddingLeft + mRadius,
                mPaddingTop + mRadius,
                mRadius, mPaint);

        canvas.drawCircle(width - mPaddingRight - mRadius,
                mPaddingTop + mRadius,
                mRadius, mPaint);

        canvas.drawRect(mPaddingLeft + mRadius,
                mPaddingTop,
                width - mPaddingRight - mRadius,
                mPaddingTop + mRadius * 2,
                mPaint);
}

接下来绘制凹槽。

protected void drawInside(Canvas canvas){
        int width = getWidth();

        mPaint.setColor(mInsideColor);
        canvas.drawCircle(mPaddingLeft + mRadius,
                mPaddingTop + mRadius,
                mRadius - mInsidePadding, mPaint);

        canvas.drawCircle(width - mPaddingRight - mRadius,
                mPaddingTop + mRadius,
                mRadius - mInsidePadding, mPaint);

        canvas.drawRect(mPaddingLeft + mRadius,
                mPaddingTop + mInsidePadding,
                width - mPaddingRight - mRadius,
                mPaddingTop + mRadius * 2 - mInsidePadding,
                mPaint);
}

最后绘制开关,开关有两种状态,开 和 关,这里分别绘制。

protected void drawOn(Canvas canvas){
        mPaint.setColor(mOnColor);
        canvas.drawCircle(mPaddingLeft + mRadius,
                mPaddingTop + mRadius,
                mRadius - mSwitchPadding, mPaint);
}

    protected void drawOff(Canvas canvas){
        int width = getWidth();

        mPaint.setColor(mOffColor);
        canvas.drawCircle(width - mPaddingRight - mRadius,
                mPaddingTop + mRadius,
                mRadius - mSwitchPadding, mPaint);
}

简单测试

控件代码写到这里基本上可以显示出来了。下面我们做个小测试。
现在布局文件里放一个Switch控件,就按照下面这样设置。

<com.pyjtlk.widgetlib.Switch
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:BackColor="#AA0000"
        app:InsideColor="#FF00AA00"
        app:OffColor="#FFAAAAAA"
        app:OnColor="#0000AA"
        app:InsidePadding="10dp"
        app:SwitchPadding="5dp"
        app:radius="20dp"/>

com.pyjtlk.widgetlib.Switch 根据项目起的名字不同,这里会有一点小区别注意一下。

如果显示如下,则说明目前的工作算是成功的。
手把手写Android自定义控件(三):测量宽高与绘制_第6张图片

最后

下一篇将讲解如何给自定义控件添加状态监听器。

你可能感兴趣的:(自定义控件)