×

Android 自定义View学习(三)——Paint 绘制文字属性

96
英勇青铜5
2016.09.02 11:45* 字数 1979

自定义View学的是啥?无非就两种:绘制文字和绘制图像

通过上篇的学习,了解到Paint类中有很多方法关于属性设置的方法。本篇就记录我学习绘制文字的过程。

baseline

学习资料:


1.简单效果

简单绘制文字

代码:

public class DrawTextView extends View {
    private Paint mPaint ;
    private String text = "英勇青铜5+abcdefg";
    public DrawTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    /**
     * 初始化画笔设置
     */
    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#FF4081"));
        mPaint.setTextSize(90f);
    }

    /**
     * 绘制
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawText(text, 0 ,getHeight()/2 ,mPaint);
    }

    /**
     * 测量
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(300,300);
        }else  if (wSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(300,hSpecSize);
        }else if (hSpecMode ==  MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize,300);
        }
    }
}

代码比较简单。
再看效果图,此时文字的y坐标虽然设置了getHeight()/2,但很明显,文字所处的y轴的位置不是控件的高的一半。很简单,文字本身也有高度,在绘制的时候,计算坐标并没有考虑文字本身的宽高。

现在首先解决的需求,就是让文字在这个自定义的DrawTextView控件中居中


2.在X轴居中

x轴居中
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //拿到字符串的宽度
    float stringWidth = mPaint.measureText(text);
    float x =(getWidth()-stringWidth)/2;
    canvas.drawText(text, x ,getHeight()/2 ,mPaint);
}

利用measureText(String text)这个方法,很容易拿到要绘制文字的宽度,再根据(getWidth()-stringWidth)/2简单计算,就可以得到在X轴起始绘制坐标


源码中,measureText(String text)调用了measureText(text, 0, text.length())

  • measureText(String text, int start, int end)

text The text to measure. Cannot be null.
start The index of the first character to start measuring
end 1 beyond the index of the last character to measure

方法中传入字符串,并指定开始测量角标和结束角标,返回结果为float型的值


2.在Y轴居中

想要在Y轴居中,就要确定出绘制文字baseline时的所在Y轴的坐标。

Android中,和文字高度相关的信息都存在FontMetrics对象中。。


2.1 FontMetrics 字体度量

FontMetricsPaint的一个静态内部类

    /**
     * Class that describes the various metrics for a font at a given text size.
     * Remember, Y values increase going down, so those values will be positive,
     * and values that measure distances going up will be negative. This class
     * is returned by getFontMetrics().
     */
    public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }
FontMetrics

FontMetrics有五个float类型值:

  • leading 留给文字音标符号的距离

  • ascentbaseline线到最高的字母顶点到距离,负值

  • topbaseline线到字母最高点的距离加上ascent

  • descentbaseline线到字母最低点到距离

  • bottomtop类似,系统为一些极少数符号留下的空间。topbottom总会比ascentdescent大一点的就是这些少到忽略的特殊符号


baseline上为负,下为正。可以理解为文字坐标系中的x轴,实际的Log打印的值

FontMetrics的值

文字的绘制是从baseline开始的


2.2确定文字Y轴的坐标

y轴位置确定图

由于文字绘制是从baseline开始,所以想要文字的正中心和DrawTextView的中心重合,baseline就不能和getHeight()/2重合,而且baseline要在getHeight()/2下方。
但要在下方多少?就是2号线和3号线之间的距离。

|ascent|=descent+ 2 * ( 2号线和3号线之间的距离 )

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //文字的x轴坐标
    float stringWidth = mPaint.measureText(text);
    float x = (getWidth() - stringWidth) / 2;
    //文字的y轴坐标
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    float y = getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2;
    canvas.drawText(text, x, y, mPaint);
}
正中心

这里只需要自己在画图板自己画一次,就差不多可以理解。

文字在中心需求已经完成。


3.其他的方法

3.1 setTextAlign(Align align) 设置对齐方式

  1. Paint.Align.LEFT 左对齐
  2. Paint.Align.CENTER 中心对齐,绘制从
  3. Paint.Align.RIGHT 右对齐

这个方法影响的是两端的绘制起始。LEFT就是从左端开始,所以使用这三个属性时,在drawText(test,x,y,paint);要注意x坐标,否则,绘制会出现错乱

LEFT对应0CENTER对应getWidth()/2RIGHT对应getWidth()


3.2 setTypeface(Typeface typeface)设置字体

系统提供了五种字体:DEFAULTDEFAULT_BOLDSANS_SERIFSERIFMONOSPACE,除了粗体,没看出有太大区别

这个对象可以用来加载自定义的字体

  • Typeface createFromAsset(AssetManager mgr, String path)assets资源中加载字体

  • Typeface createFromFile(String path)通过路径加载字体文件

  • Typeface createFromFile(File file)通过指定文件加载字体


也可以通过Typefacecreate(Typeface family, int style)方法拿到字体样式

Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
textPaint.setTypeface( font );
  • Typeface.NORMAL 默认
  • Typeface.BOLD 粗体
  • Typeface.ITALIC 斜体
  • Typeface.BOLD_ITALIC 粗斜体

3.3 setStyle 设置画笔样式

  • Paint.Style.FILL 实心充满

    FILL

  • Paint.Style.STROKE

    STROKE

  • Paint.Style.FILL_AND_STROKE 这个暂时没发现和FILL有啥不同


3.4 setFlags(int flags) 设置画笔的flag

  • ANTI_ALIAS_FLAG 抗锯齿
  • DITHER_FLAG 防抖动

其他还有一堆,试了试,没看出太大区别。常见的就是抗锯齿,遇到特殊的需求,再来深入了解

也可以直接在Paint的构造方法中指定


3.5PathEffect setPathEffect(PathEffect effect)设置路径效果

<p>
这个方法感觉不应该放在本篇,应该算作图像。不过,代码写好了,也就放在这了。

7种路径效果
public class DrawTextView extends View {
   
    private Paint textPaint;
    private Paint pathPaint;
    private Path mPath;
    private String[] pathEffectName = {
            "默认", "CornerPathEffect", "DashPathEffect", "PathDashPathEffect",
            "SumPathEffect", "DiscretePathEffect", "ComposePathEffect"
    };
    private PathEffect[] mPathEffect;

    public DrawTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    /**
     * 初始化画笔设置
     */
    private void initPaint() {
        //文字画笔
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(40f);
        //路径画笔
        pathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        pathPaint.setColor(Color.parseColor("#FF4081"));
        pathPaint.setStrokeWidth(5F);
        pathPaint.setStyle(Paint.Style.STROKE);
        mPath = new Path();
        //设置起点
        mPath.moveTo(0, 0);
        //路径连接的点
        for (int i = 1; i < 37; i++) {
            mPath.lineTo(i * 30, (float) (Math.random() * 100));
        }
        //初始化PathEffect
        mPathEffect = new PathEffect[7];
        mPathEffect[0] = new PathEffect();//默认
        mPathEffect[1] = new CornerPathEffect(10f);
        mPathEffect[2] = new DashPathEffect(new float[]{10f, 5f, 20f, 15f},10);
        mPathEffect[3] = new PathDashPathEffect(new Path(), 10, 10f, PathDashPathEffect.Style.ROTATE);
        mPathEffect[4] = new SumPathEffect(mPathEffect[1], mPathEffect[2]);
        mPathEffect[5] = new DiscretePathEffect(5f, 10f);
        mPathEffect[6] = new ComposePathEffect(mPathEffect[3], mPathEffect[5]);
        
    }

    /**
     * 绘制路径
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i < mPathEffect.length; i++) {
            pathPaint.setPathEffect(mPathEffect[i]);
            canvas.drawPath(mPath,pathPaint);
            canvas.drawText(pathEffectName[i], 0, 130, textPaint);//每个画布的最上面,就是y轴的0点
            // 每绘制一条将画布向下平移180个像素
            canvas.translate(0, 180);//控件的高度要足够大才能平移
        }
        invalidate();//绘制刷新
    }

    /**
     * 测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, hSpecSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
        }
    }
}

这里借鉴了爱哥的思路,把7种效果都画了出来,还加上了名字。

需要注意的是,绘制路径时,pathPaint.setStyle(Paint.Style.STROKE)画笔的风格要空心,否则,后果画出的不是线,而是一个不规则的区域。

这7种路径效果,暂时还不能区分,先暂时知道有这么7种效果,等到实现具体需求了再深入了解


9月8号补充

  • CornerPathEffect 拐角处变圆滑
  • DashPathEffect 可以用来绘制虚线,用一个数组来设置各个点之间的间隔,phase控制绘制时数组的偏移量
  • PathDashPathEffectDashPathEffect类似 ,可以设置显示的点的图形,例如圆形的点
  • DisCreatePathEffect 线段上会有许多杂点
  • ComposePathEffect 组合两个PathEffect,将两个组合成一个效果

从Android群英传摘抄


3.6breakText(CharSequence text, int start, int end,boolean measureForwards,float maxWidth, float[] measuredWidth)

Measure the text, stopping early if the measured width exceeds maxWidth.
Return the number of chars that were measured, and if measuredWidth is
not null, return in it the actual width measured.
@param text The text to measure. Cannot be null.
@param start The offset into text to begin measuring at
@param end The end of the text slice to measure.
@param measureForwards If true, measure forwards, starting at start.Otherwise, measure backwards, starting with end.
@param maxWidth The maximum width to accumulate.
@param measuredWidth Optional. If not null, returns the actual width measured.
@return The number of chars that were measured. Will always be <= abs(end - start).

这个方法,暂时没有测试出啥效果。

先引用爱哥的描述:

这个方法让我们设置一个最大宽度在不超过这个宽度的范围内返回实际测量值否则停止测量,参数很多但是都很好理解,text表示我们的字符串,start表示从第几个字符串开始测量,end表示从测量到第几个字符串为止,measureForwards表示向前还是向后测量,maxWidth表示一个给定的最大宽度在这个宽度内能测量出几个字符,measuredWidth为一个可选项,可以为空,不为空时返回真实的测量值。同样的方法还有breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)和breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)。这些方法在一些结合文本处理的应用里比较常用,比如文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串,这就派上用场了~~~


3.7 其余

剩下的方法,试一下就晓得效果了

  • setTextScaleX(float f) 设置缩放,0f到1f为缩小,大于1f为放大
  • setUnderlineText(booelan b) 设置下划线
  • setStrikeThruText (boolean strikeThruText) 设置文本删除线
  • setTextSize(float f) 设置文字字体大小
  • getFontSpacing()得到行间距
  • descent()得到descent的值
  • ascent() 得到asccent的值
  • getLetterSpacing() 字母间距

关于字体的常用的方法差不多就这些了。漏掉的,用到了再补充。


4.最后

重点在于FontMetrics的学习。先用画图板画出来,个人感觉比较有助于记忆。然后,尝试用代码在程序中把各条线画出来。

下篇学习Paint类中关于画图像的属性方法。

Android自定义View
Web note ad 1