美文网首页Android TechAndroid安卓高级进阶Android自定义控件
Android 自定义View学习(三)——Paint 绘制文字

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

作者: 英勇青铜5 | 来源:发表于2016-09-02 11:45 被阅读10340次

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

    相关文章

      网友评论

      • 吉凶以情迁:动态插入的方式 服务器给的y坐标不是高度的顶端而是那个ascent什么线,那该如何是好。
      • Flipped199:stroke是空心,stroke and fill是实心加描边,fill不描边,你把描边宽度设置大一点就看得出来了
        英勇青铜5: @牛掰啊牛掰 嗦嘎,多谢指出疑问。还有很多类似的问题不懂呢
      • Rc在努力:|ascent|=descent+ 2 * ( 2号线和3号线之间的距离 ) 这个没看明白............
        英勇青铜5:@人九 你的说法确实更好理解些
        人九:换一种方式思考:2号线和3号线之间的距离 = (|ascent| + desent) / 2 - desent,这样是不是更好理解,当然,楼主的计算方法也是没问题的
        英勇青铜5: @Rc在努力 这里最好自己动笔在纸上画画,也许这里我个人的理解的方式不好
      • 904c679224d2:上面说到 |top|=|ascent|+|leading|,但是紧接着log 打出来的信息中并不满足这个关系
        英勇青铜5: @Turisla 多谢指出错误,不过只能过几天改啦😂
      • 7509f3251f4b:研究的很透彻,图文讲解啊:smile:
        英勇青铜5: @枯鱼之泣 @枯鱼之泣 😃😃😃😃
      • 奋斗小青年Jerome:总结的挺好,最近刚好需要用到Path,谢谢!!
        英勇青铜5: @vx王剑锋 😁😁😁😄😄😄
      • 鱼丸粗面X:“baseline上为正,下为负”??作者手滑了,应该是baseline上为负,下为正
        英勇青铜5:@野生代码搬运工 :scream: 多谢指出错误
      • 囧_囧:佩服!
      • 皮球二二:请问那个计算baseLine位置的是公式还是什么?我不知道怎么得来的
        英勇青铜5:@r17171709 应该可以当做公式用。计算getHeight()线到baseline的线的距离,主要利用是字体中心点所在y轴的那条横线,也就是getHeight()那条线,到aecent和descent线的距离相等。
      • 捡淑:马克
      • 陆地蛟龙:不错
        英勇青铜5:@胡髭蛤蟆 :smile::smile:
      • dodo_lihao:整理和叙述得很好,学习了
        英勇青铜5:@NobugException 是不是可以用来格式化一行文字?有些应用为了让文字显示更加美观会进行一些符号空格处理
        NoBugException:breakText我知道, 我在整理博客, Paint里面的东西太多了, 我估计要整理个几天, breakText我刚好研究过, 可以用作换行操作。
        英勇青铜5:@dodo_lihao 共勉

      本文标题:Android 自定义View学习(三)——Paint 绘制文字

      本文链接:https://www.haomeiwen.com/subject/evacettx.html