自定义View系列(三)

上一篇着重就onDraw中的canvas方法进行了一系列绘制,相信对canvas中几个常用的方法应该都有所了解了。今天,就实例做一个自定义的控件,拿最简单的TextView开始,做一个带有背景色的TextView。直接上代码:

1,自定义属性:

<declare-styleable name="MyTextView2">
    <attr name="style" format="integer|reference"/>
    <attr name="colors" format="color|reference"/>
    <attr name="textSize" format="dimension|reference"/>
    <attr name="text" format="string|reference"/>
</declare-styleable>

这一部分内容,上一篇介绍过,这里不再多说。

2,XML文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                xmlns:app="http://schemas.android.com/apk/res-auto">
    
    <com.example.administrator.myview1.MyTextView2
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ffff00"
        app:colors="#ff00"
        app:textSize="36sp"
        app:text="Hello World !"/>
</RelativeLayout>

很简单,只谢了一个自定义控件而已。

3,接下来,看一下我们在MyTextView2中都做了那些工作:

public class MyTextView2 extends View {
    private int mTextSize;
    private int mColor;
    private String mText;
    private int mType;
    private Paint mPaint;
    private Rect mBound;
    private float mTextHeight;
    private float mTextWidth;

    public MyTextView2(Context context) {
        this(context,null);
    }

    public MyTextView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyTextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.MyTextView2);
        mType = mTypedArray.getInt(R.styleable.MyTextView2_style,0);
        mColor = mTypedArray.getColor(R.styleable.MyTextView2_colors, Color.RED);
        mTextSize = mTypedArray.getDimensionPixelSize(R.styleable.MyTextView2_textSize,20);
        mText = mTypedArray.getString(R.styleable.MyTextView2_text);
        mTypedArray.recycle();
        initView();
    }
    /** 初始化 */
    private void initView(){
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);//填充类型
         mPaint.setTextSize(mTextSize);
        mPaint.setColor(mColor);
        mBound = new Rect();
        mPaint.getTextBounds(mText,0,mText.length(), mBound);
        mTextHeight = mBound.height();//测量出文字的高度
         mTextWidth = mBound.width();//测量出文字的宽度
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(mText,getWidth()/2 - mBound.width() / 2,getHeight()/2 + mBound.height()/2,mPaint);
        }
    }
}

先是构造方法中,调用了自定义属性,用于声明,TextView的大小,颜色和文本内容。接着,就是一个初始化的方法,对画笔进行了基础的设置。然后,重写了View的onMeasure方法和onDraw()方法。onMeasure()方法,美俄进行任何操作,先不管他,直接看onDraw()方法,里面就一句代码,很简单的绘制一个文本内容。其中getWidth和getHeight是控件的宽高(和getMeasureWidth和getMeasureHeight结果一样)。结合xml内容,绘制出来的应该是背景色是黄色,内容为Hello  World !这样的一个TextView,运行一下,可以看到运行结果如图一:

                         图一:                     图二:             

很明显这不是我们想要的结果。在介绍View的生命周期的时候,里面对onMeasure()方法进行的说明,主要就是对控件的尺寸进行测量。而当我们在布局文件中把宽高都设置成wrap_content时,系统帮我们测量的宽高都是match_parent。只有当我们设置了明确的宽高之后测量的才是我们设置的宽高,如果我们把布局文件中的宽高分别设置为 200dp和100dp则结果就是图二。说到这,我们有必要再说一下MeasureSpec的specMode,一共三种类型(也可以参考:自定义View系列(一)):

EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

UNSPECIFIED:表示子布局想要多大就多大,很少使用

知道了这些,我们就可以重写onMeasure()方法了,下面是我们重写的onMeasure():

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(widthMeasureSpec);

    int width;
    int height;
    if(widthMode == MeasureSpec.EXACTLY){
        width = widthSize;
    }else {
        width = (int) (getPaddingLeft() + mTextWidth + getPaddingRight());
    }
        if(heightMode == MeasureSpec.EXACTLY){
        height = heightSize;
    }else {
        height = (int) (getPaddingTop() + mTextHeight + getPaddingBottom());
    }
    setMeasuredDimension(width,height);
}

重新运行一下,得到如图三:

             图三:                         

这样子,就达到了预期的效果。但是仔细看一下,就会发现,文本内容最后的叹号,好像只显示了一半,没能完全显示。不用想,肯定是我们的宽度设置的不够啊!上文中,有一行代码测量了文本内容的的宽度为:mTextWidth = mBound.width()。没错这样也确实可以得到文本内容的宽度,只不过不精确,测量出的是粗略值。改进方法为:mTextWidth = mPaint.measureText(mText),这样就得到了文本的精确宽度,再次运行可以看到如图四:

              图四:                         

这样宽度就没问题了!那高度呢,肯定也是粗略的,这样子还不容易看,我们把文本内容加上中文,并且在onDraw()里面添加两行代码:

mPaint.setColor(Color.BLUE);
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,mPaint);

画出控件的中线,运行,看到效果图五:

                 图五:                       

   看到了吧,高度明显偏小了。高度怎么改进呢,这个有点麻烦,先看一个图:

  出自网络

看一下这个图,里面有五个参数,top,ascent,basline,descent,bottom:

Baseline是基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度);leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;top和bottom可以理解为文本绘制时,留出的内边距。之所以留出内边距,是因为因为TextView在绘制文本的时候考虑到了类似读音符号,下图中的A上面的符号就是一个拉丁文的类似读音符号的东西:


如上图,Baseline是基线,Baseline以上是负值,以下是正值,因此 ascent,top是负值, descent和bottom是正值。 

至此,我们算是了解清楚了,具体怎么修改呢?这里要用到FontMetrics 字体度量,它是Paint的一个今天内部类。这里不做介绍,只解释其用法。原来的高度为:mTextHeight = mBound.height();

修改后为:

Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
mTextHeight = Math.abs((fontMetrics.bottom - fontMetrics.top));

运行程序,可以看到:

                    

发现文本内容没有居中,我们想让文本居中,然后看效果。

我们知道,中心线到底部的距离为文本高度的一半,也就是(descent - ascent)/2,这个由上面的图可以得到,这是文本的高度;中心线与基线的间距为:(descent - ascent)/2 - descent ;这样只需要在绘制的时候,将y值向上提高中心线与基线的距离就行了,即:

canvas.drawText(mText,getWidth()/2 - mTextWidth / 2,
        getHeight()/2  - (mFontMetrics.descent - (-mFontMetrics.ascent + mFontMetrics.descent)/2),mPaint);

运行一下,可以看到:

                    

有图可以看出,文本的高度算了出来,并且居中了。由此可以得到文本垂直居中的计算方式方式

Paint.FontMetrics   mFontMetrics = mPaint.getFontMetrics();

y: getHeight/2 - mFontMetrics.descent + (mFontMetrics.descent - mFontMetrics.ascent)/ 2;

好了,最终终于达到了想要的结果!今天就说到这吧,下一篇将会说一下,自定义布局。

参考:http://blog.csdn.net/u014702653/article/details/51985821

最后,奉上最终的代码:

public class MyTextView2 extends View {
    private int mTextSize;
    private int mColor;
    private String mText;
    private int mType;
    private Paint mPaint;
    private Rect mBound;
    private float mTextHeight;
    private float mTextWidth;
    private Paint.FontMetrics mFontMetrics;

    public MyTextView2(Context context) {
        this(context,null);
    }

    public MyTextView2(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyTextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.MyTextView2);
        mType = mTypedArray.getInt(R.styleable.MyTextView2_style,0);
        mColor = mTypedArray.getColor(R.styleable.MyTextView2_colors, Color.RED);
        mTextSize = mTypedArray.getDimensionPixelSize(R.styleable.MyTextView2_textSize,20);
        mText = mTypedArray.getString(R.styleable.MyTextView2_text);
        mTypedArray.recycle();
        initView();
    }
    /** 初始化 */
    private void initView(){
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);//填充类型
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mColor);
        mBound = new Rect();
        mPaint.getTextBounds(mText,0,mText.length(), mBound);
        mFontMetrics = mPaint.getFontMetrics();
        mTextHeight = Math.abs((mFontMetrics.bottom - mFontMetrics.top));//测量出文字的高度
        mTextWidth = mPaint.measureText(mText);//测量出文字的宽度
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(widthMeasureSpec);

        int width;
        int height;
        if(widthMode == MeasureSpec.EXACTLY){
            width = widthSize;
        }else {
            width = (int) (getPaddingLeft() + mTextWidth + getPaddingRight());
        }
            if(heightMode == MeasureSpec.EXACTLY){
            height = heightSize;
        }else {
            height = (int) (getPaddingTop() + mTextHeight + getPaddingBottom());
        }
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(mText,getWidth()/2 - mTextWidth / 2,
                getHeight()/2  - (mFontMetrics.descent - (-mFontMetrics.ascent + mFontMetrics.descent)/2),mPaint);

        mPaint.setColor(Color.BLUE);
        canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,mPaint);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值