从今天开始就要学习自定义控件的使用了,以后看到什么酷炫的效果都会试着写一写实现的,不然还是要先学会基础的使用才行,作为一个新手第一步肯定是照着别人的敲了,所以先找了一篇相对简单的自定义控件的实现的文章,http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2013/0308/994.html
首先在res/values下面建立一个attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name = "LabelView">
<attr name ="text" format="string"/>
<attr name ="textColor" format = "color"/>
<attr name ="textSize" format = "dimension"/>
</declare-styleable>
</resources>
蛋疼的eclipse对于declare-styleable,attr没有代码提示,开始还以为敲错了,这是自定义textview的一篇文章,我们先设置三个变量text,textColor,textSize,后面的format是属性,总共有10种之多,用的比较多的 reference(引用系统资源),color(颜色),boolean(true或false),dimension(sp,dp之类的),integer(整数),string(字符串),float(浮点数),fraction(百分比),enum(枚举),flag(位或运算)
接下来就建立一个类继承view,并实现构造方法,分为1、2、3, 3种构造方法,这里用到了attribute属性,我们实现其中两种,当然3个都写也没问题。在构造函数中我们获取属性,并设置给画笔,调用onMeasure和onDraw方法。
public class LabelView extends View {
private Paint mPaint;
private String mText;
private int mAscent;
public LabelView(Context context) {
super(context, null);
}
public LabelView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
setPadding(10, 10, 10, 10);
TypedArray array = context.obtainStyledAttributes(attrs,
R.styleable.LabelView);
CharSequence s = array.getString(R.styleable.LabelView_text);
if (s != null) {
mText = s.toString();
}
int color = array.getColor(R.styleable.LabelView_textColor, 0xFF000000);
mPaint.setColor(color);
int textSize = array.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
if (textSize > 0) {
mPaint.setTextSize(textSize);
}
requestLayout();
invalidate();
array.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = (int) (mPaint.measureText(mText) + getPaddingLeft() + getPaddingRight());
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
mAscent = (int) mPaint.ascent();
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = (int) (-mAscent + mPaint.descent()) + getPaddingTop()
+ getPaddingBottom();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
Log.i("---onmeasure",
"ascent:" + mAscent + " descent:" + mPaint.descent());
return result;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent,
mPaint);
Log.i("---ondraw", "left:" + getPaddingLeft() + " top:"
+ getPaddingTop());
}
}
在构造函数中初始化画笔,并将设置好的属性赋给画笔,其中的R.styleable.LabelView_text是系统自己将declare-styleable的name加上下面text的属性name拼接起来,为了区分各个attr属性。
画笔对象mPaint的setTextSize中的参数是px,而不是sp,但是我们有方法getDimensionPixelOffset,其实还有另外两个方法getDimension,getDimensionPixelSize,区别就是getDimensionPixelOffset和getDimension一个返回int,一个返回float,并且在dp,sp时会乘以density,在px时不会乘,而getDimensionPixelSize无论什么情况都会乘以density,然后返回一个int值。
继续往下走有两个方法requestLayout和invalidate,前者会导致调用onMeasure和onLayout方法,但不会onDraw,后者用来刷新View,更新UI,显示新的绘制的界面。
然后我们看到一行代码array.recycle(); 那这行代码是干什么的呢?我也没有弄懂,在stackoverflow上有一个回答
http://stackoverflow.com/questions/13805502/why-should-a-typedarray-be-recycled
上面说recycle的源码是
/**
* Give back a previously retrieved StyledAttributes, for later re-use.
*/
public void recycle() {
synchronized (mResources.mTmpValue) {
TypedArray cached = mResources.mCachedStyledAttributes;
if (cached == null || cached.mData.length < mData.length) {
mXml = null;
mResources.mCachedStyledAttributes = this;
}
}
}
</pre><span style="font-size:14px;">看代码是有cache缓存的,但是我自己打开源码</span><p><pre name="code" class="java">public void recycle() {
if (mRecycled) {
throw new RuntimeException(toString() + " recycled twice!");
}
mRecycled = true;
// These may have been set by the client.
mXml = null;
mTheme = null;
mResources.mTypedArrayPool.release(this);
}
这个先放着吧,以后知道了再回来补充。
onMeasure中使用了一个方法setMeasuredDimension,这个方法至关重要,看super中的方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
也是默认的使用了这个方法,设置宽和高,如果没有这个方法就会报错。
再看一下下面的两个方法,一个是ascent,一个是descent,在网上找了一张下面的图
首先有一个基线baseLine,而ascent是红线到绿线的距离,是一个负数,descent是红线到下面的黄线的距离,是正数。
measureHeight中的result,就是ascent的绝对值加上descent加上 上和下的padding就是高度,宽度就是measureText()加上左右的padding,measureText()返回的是text的宽度,下面是源码
/**
* Return the width of the text.
*
* @param text The text to measure. Cannot be null.
* @return The width of the text
*/
public float measureText(String text) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
}
if (text.length() == 0) {
return 0f;
}
if (!mHasCompatScaling) {
return (float) Math.ceil(native_measureText(text, mBidiFlags));
}
final float oldSize = getTextSize();
setTextSize(oldSize*mCompatScaling);
float w = native_measureText(text, mBidiFlags);
setTextSize(oldSize);
return (float) Math.ceil(w*mInvCompatScaling);
}
设置了高度和宽度后,在onDraw中进行绘制,canvas.drawText()是从text的左上角开始绘制的,第二个参数是x,第三个参数是左上角的y
然后在布局中就可以调用了
首先加入命名空间
xmlns:app="http://schemas.android.com/apk/res/com.example.customview"
app是名字,com.examlpe.customview是包名
<com.example.customview.LabelView
android:id="@+id/label1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000000"
android:layout_centerInParent="true"
app:text="helloworld"
app:textColor="#FF0000"
app:textSize="26sp" />
运行效果如下