LabelView是Android API DEMO中的一个例子,演示了如何写一个简单的自定义View。
先看一下效果
布局文件是这样的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/com.example.labelview" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.example.labelview.LabelView android:background="@android:color/holo_blue_light" android:layout_width="match_parent" android:layout_height="match_parent" app:text="Red"/> <com.example.labelview.LabelView android:background="@android:color/holo_red_light" android:layout_width="match_parent" android:layout_height="wrap_content" app:text="Blue" app:textSize="20dp"/> <com.example.labelview.LabelView android:background="@android:color/holo_purple" android:layout_width="match_parent" android:layout_height="wrap_content" app:text="Green" app:textColor="#ffffffff" /> </LinearLayout>xml元素名com.example.labelview.LabelView,对于自定义的控件,要写上它完整的包路径
LabelVIew的路径如图所示:
注意到标签中的app:text,app:textColor,app:textSize属性都是控件的自定义属性,app:前缀是labelview专属的,定义它的步骤如下:
1.在布局文件中声明命名空间
LinearLayout元素下的xmlns:app="http://schemas.android.com/apk/res/com.example.labelview" 属性,声明了app:这个自定义View属性,格式为:
xmlns:命名空间 /res/应用程序包名。
2,在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>
3.在布局文件xml中使用这个属性
app:text="Red"
app:textSize="20dp"
app:textColor="#ffffffff"
4.在代码中实现初始化。
LabelView的代码:
public class LabelView extends View { private Paint mTextPaint; private String mText; private int mAscent; /** * Constructor. This version is only needed if you will be instantiating * the object manually (not from a layout XML file). * @param context */ public LabelView(Context context) { super(context); initLabelView(); } /** * Construct object, initializing with any attributes we understand from a * layout file. These attributes are defined in * SDK/assets/res/any/classes.xml. * * @see android.view.View#View(android.content.Context, android.util.AttributeSet) */ public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if (s != null) { setText(s.toString()); } // Retrieve the color(s) to be used for this view and apply them. // Note, if you only care about supporting a single color, that you // can instead call a.getColor() and pass that to setTextColor(). setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); } private final void initLabelView() { mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); // Must manually scale the desired text size to match screen density mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density); mTextPaint.setColor(0xFF000000); setPadding(20, 3, 3, 3); } /** * Sets the text to display in this label * @param text The text to display. This will be drawn as one line. */ public void setText(String text) { mText = text; requestLayout(); invalidate(); } /** * Sets the text size for this label * @param size Font size */ public void setTextSize(int size) { // This text size has been pre-scaled by the getDimensionPixelOffset method mTextPaint.setTextSize(size); requestLayout(); invalidate(); } /** * Sets the text color for this label. * @param color ARGB value for the text */ public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); } /** * @see android.view.View#measure(int, int) */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.v("test", "onMeasure"); setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); } /** * Determines the width of this view * @param measureSpec A measureSpec packed into an int * @return The width of the view, honoring constraints from measureSpec */ private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Determines the height of this view * @param measureSpec A measureSpec packed into an int * @return The height of the view, honoring constraints from measureSpec */ private int measureHeight(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); mAscent = (int) mTextPaint.ascent(); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text (beware: ascent is a negative number) //-asent+desent = 字符的高度,再加上山下的padding等于这个view的总高度 result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; } /** * Render the text * * @see android.view.View#onDraw(android.graphics.Canvas) */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Log.v("test", "onDraw"); //y是指定这个字符baseline在屏幕上的位置 canvas.drawText(mText, getPaddingLeft(), ((int) (-mAscent + getPaddingTop())), mTextPaint); } }
先看构造方法
public LabelView(Context context, AttributeSet attrs) { super(context, attrs); initLabelView(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView); CharSequence s = a.getString(R.styleable.LabelView_text); if (s != null) { setText(s.toString()); } // Retrieve the color(s) to be used for this view and apply them. // Note, if you only care about supporting a single color, that you // can instead call a.getColor() and pass that to setTextColor(). setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000)); int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0); if (textSize > 0) { setTextSize(textSize); } a.recycle(); }intiLavbelVIew方法设置了Paint对象绘制时的一些初始值。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LabelView);
public void setTextSize(int size) { // This text size has been pre-scaled by the getDimensionPixelOffset method mTextPaint.setTextSize(size); requestLayout(); invalidate(); }
public void setText(String text) { mText = text; requestLayout(); invalidate(); }
public void setTextColor(int color) { mTextPaint.setColor(color); invalidate(); }可以看到setTextColor并没有调用requestLayout方法,因为设置字体的颜色并不需要变化字体的大小形状,所以不用测量,就不需要走onMeasure的流程,自然不需调用requestLayout方法。
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));用来设置测量完毕的尺寸,宽度和高的测量是通过measureWidth和measureHeight进行的。
private int measureWidth(int measureSpec) { int result = 0; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + getPaddingRight(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec 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) mTextPaint.ascent(); if (specMode == MeasureSpec.EXACTLY) { // We were told how big to be result = specSize; } else { // Measure the text (beware: ascent is a negative number) //-asent+desent = 字符的高度,再加上山下的padding等于这个view的总高度 result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom(); if (specMode == MeasureSpec.AT_MOST) { // Respect AT_MOST value if that was what is called for by measureSpec result = Math.min(result, specSize); } } return result; }大体上和measureWidth差不多。计算字体高度的方式很有意思,涉及到baseLine的问题,
result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop() + getPaddingBottom();
protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText, getPaddingLeft(), ((int) (-mAscent + getPaddingTop())), mTextPaint); }使用画布对象canvas绘制字体,参数是:显示的字体内容,x坐标,y坐标,paint对象