API DEMO LabelView分析

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的路径如图所示:

API DEMO LabelView分析_第1张图片

注意到标签中的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对象


你可能感兴趣的:(API DEMO LabelView分析)