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

自定义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类中关于画图像的属性方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容