天天看点

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

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

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

认识MeasureSpec

在正式开始写测量代码前,首先需要知道一个重要的参数,MeasureSpec。

它是一个32位的整型数据,由 模式 和 长度 组成,它的结构如下。

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

其中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);

举两个例子。

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:text="hello world"/>

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 自定义控件 获取宽高,手把手写Android自定义控件(三):测量宽高与绘制...

接着有四个判断分支。分别是

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 自定义控件 获取宽高,手把手写Android自定义控件(三):测量宽高与绘制...

在Android设备里,从左到右是x轴正方向,从上到下是y轴正方向。

左上角是原点(0,0)

先来做个小练习吧。控件宽度为100,,高度为50,求控件正中央的坐标。红线长度为30,求黑点坐标。

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

题解:控件左上角是原点坐标(蓝点),根据刚刚说的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控件,就按照下面这样设置。

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 自定义控件 获取宽高,手把手写Android自定义控件(三):测量宽高与绘制...

最后

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