上一篇讲解了自定义属性的相关操作,本篇来讲解如何测量控件。相比于前面的步骤,测量工作的复杂了许多,在这个阶段建议准备一张草稿纸记录各种思路和计算结果,这样不容易乱。下面是我在设计WaveLoadingView时的草稿。
在正式开始写测量代码前,首先需要知道一个重要的参数,MeasureSpec。
它是一个32位的整型数据,由 模式 和 长度 组成,它的结构如下。
其中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方法是控件用来测量控件本身大小的,做好了前期的准备,现在就来重写这个方法。
@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的四倍(也就是两倍直径)。
接着有四个判断分支。分别是
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也考虑进去,那离优秀的控件就又近了一小步。
完成了测量接下来就是绘制控件了。
在此之前,简单提一下。其实控件从初始化到真正显示出来是要先后调用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设备里,从左到右是x轴正方向,从上到下是y轴正方向。
左上角是原点(0,0)
先来做个小练习吧。控件宽度为100,,高度为50,求控件正中央的坐标。红线长度为30,求黑点坐标。
题解:控件左上角是原点坐标(蓝点),根据刚刚说的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 根据项目起的名字不同,这里会有一点小区别注意一下。
下一篇将讲解如何给自定义控件添加状态监听器。