API DEMO LabelView分析

来源:互联网 发布:轻淘客和淘宝联盟 编辑:程序博客网 时间:2024/05/04 12:51

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

获得了attrs.xml中定义的属性类型,然后用TypedArray对象中的方法获得具体的值,因为attr文件中,text是string类型的,所以用getString,textSize和textColor分别对应getColor和getDimensionPixelOffset。最后要调用a.recycle();回收TypedArray对象。
注意到 setTextSize方法:
  public void setTextSize(int size) {        // This text size has been pre-scaled by the getDimensionPixelOffset method        mTextPaint.setTextSize(size);        requestLayout();        invalidate();    }
requestLayout的调用会触发View的onMeasure过程,invalidate的调用会触发View的onDraw过程。
View的绘制流程中,onMeasure用来测量控件的尺寸,onDraw用来绘制图形
其他2个设置text的方法,
    public void setText(String text) {        mText = text;        requestLayout();        invalidate();    }
    public void setTextColor(int color) {        mTextPaint.setColor(color);        invalidate();    }
可以看到setTextColor并没有调用requestLayout方法,因为设置字体的颜色并不需要变化字体的大小形状,所以不用测量,就不需要走onMeasure的流程,自然不需调用requestLayout方法。

LabelView重写了onMeasure方法
        setMeasuredDimension(measureWidth(widthMeasureSpec),                measureHeight(heightMeasureSpec));
用来设置测量完毕的尺寸,宽度和高的测量是通过measureWidth和measureHeight进行的。
measureWidth:
    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;    }
onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值,需要用静态方法getMode和getSize分别取出
MeasureSpec.getMode获取MeasureSpec的Mode,MeasureSpec的Mode有三种类型:
MeasureSpec.AT_MOST
MeasureSpec.EXACTLY
MeasureSpec.UNSPECIFIED
调试代码时,发现MeasureSpec.getSize会返回屏幕的真实宽度,mode和布局xml中宽度属性值的对应关系为:
xx dp:specMode == MeasureSpec.EXACTLY
wrap_content:specMode == MeasureSpec.AT_MOST
match_parent:specMode ==MeasureSpec.EXACTLY
所以,如果布局中是具体的dp的话,result返回dp换算后的实际px。而如果是match_parent的话,会返回MeasureSpec.getSize的值,即屏幕的实际宽度。如果是wrap_content的话,会先调用Paint的measureText方法算出text的具体宽度然后返回。
再来看measureHeight方法:
    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的问题,
以下图片来自网络,说明baseLine和各个属性的关系

ascent:是baseline之上至字符最高处的距离
descent:是baseline之下至字符最低处的距离
所以用如下代码计算字体高度
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()                    + getPaddingBottom();

最后来看onDraw方法,它负责具体的绘制
    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);                canvas.drawText(mText, getPaddingLeft(),  ((int) (-mAscent  + getPaddingTop())), mTextPaint);    }
使用画布对象canvas绘制字体,参数是:显示的字体内容,x坐标,y坐标,paint对象