Android 为控件增加数字提示,DrawText 方法解析

摘要

Android 开发的过程中,经常会遇到一些数量统计然后使用一个角标来给用户提示数量,例如微信的消息数量,当当的购物车商品数量等;


Android 为控件增加数字提示,DrawText 方法解析_第1张图片
微信消息数量

Android 为控件增加数字提示,DrawText 方法解析_第2张图片
当当购物车的数量

分析

实现的方式有很多种,这里采用自定义控件的方式来实现(可以在所需要用到此功能的控件上进行扩展)。
我们可以自定义一个控件,继承自我们需要用到的控件(RadioButton,TextView等),然后我们只需要重写 onDraw(Canvas canvas) 方法,当然如果你想在布局文件中就对数字提示进行一些初始化的操作(背景颜色,位置,文本颜色等),我们可以通过自定义属性,在 attr 文件里面声明然后在 在含有AttributeSet参数的构造方法里面获取自定义属性的相关的值。
重写 onDraw(Canvas canvas) 的关键在于找到需要绘制圆形(也可以是其它形状,使用canvas.drawPath())的圆心,然后就是如何将文本绘制在我们绘制的圆的中间位置,这里我们使用的是canvas.drawText(String text, float x, float y, Paint paint) 这个方法,具体使用下面会详细分析。

效果演示

先上个效果图:


Android 为控件增加数字提示,DrawText 方法解析_第3张图片
并排显示

Android 为控件增加数字提示,DrawText 方法解析_第4张图片
上下显示
Android 为控件增加数字提示,DrawText 方法解析_第5张图片
上下显示2
Android 为控件增加数字提示,DrawText 方法解析_第6张图片
上下显示3

实现过程

  1. 新建一个类继承我们的目标控件(这里我们选用TextView
    自定义控件
  2. 自定义属性
    这里我们需要对圆形的背景颜色,半径,以及文本的颜色,大小等属性进行定义。
 
       
       
       
       
       


  1. 获取之定义属性的值
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BadgeTextView);
    mBadgeColor = array.getColor(R.styleable.BadgeTextView_badgeColor, DEFAULT_BADGE_COLOR);
    mBadgeRadius = (int) array.getDimension(R.styleable.BadgeTextView_badgeRadius, 0);
    mBadgeNumber = array.getInt(R.styleable.BadgeTextView_badgeNumber, 0);
    mBadgeNumberColor = array.getColor(R.styleable.BadgeTextView_badgeNumberColor, DEFAULT_BADGE_NUMBER_COLOR);
    mBadgeNumberSize = (int) array.getDimension(R.styleable.BadgeTextView_badgeNumberSize, 0);
        // 及时回收资源
    array.recycle();
  1. 重写 onDraw(Canvas canvas) 方法
    关于在 super.onDraw(); 前面的一堆代码,主要是让 TextView 的 drawable 图片以及文本在控件的上下左右居中显示,具体的我都在代码上写明了注释,大致的意思就是计算文本的内容以及图片的尺寸,然后对 Canvas 画布对象进行 tanslate 平移操作,使内容在控件允许显示的范围内居中,我们直接看代码:
    @Override
    protected void onDraw(Canvas canvas) {
        // 获取控件的宽高
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();

        // badge 圆心的坐标
        int cx = 0, cy = 0;

        // 获取 TextView 的Drawable 对象,这里我们需要通过计算,得到 drawable 的高度/宽度
        Drawable[] drawables = getCompoundDrawables();
        if (drawables != null) {
            Drawable drawable;
            if ((drawable = drawables[0]) != null) { // drawableLeft
                // 设置文本垂直对齐
                setGravity(Gravity.CENTER_VERTICAL);
                // 左边的 Drawable 不为空时,计算需要绘制的内容的宽度
                float textWidth = getPaint().measureText(getText().toString());
                int drawablePadding = getCompoundDrawablePadding();
                int drawableWidth = drawable.getIntrinsicWidth();
                // 计算总内容的宽度
                float bodyWidth = textWidth + drawablePadding + drawableWidth;
                // 移动画布
                canvas.translate((getWidth() - bodyWidth) / 2, 0);

                // 计算圆心的位置,(注:这里可以根据需求,移动圆心的位置,也可以设置成一个参数来调整位置,这样就不需要翻代码了)
                cx = drawableWidth;
                cy = mHeight / 2 - drawable.getIntrinsicHeight() / 2;
            } else if ((drawable = drawables[1]) != null) { // drawableRight
                // 设置文本水平对齐
                setGravity(Gravity.CENTER_HORIZONTAL);
                // 上面的drawable 不为空时,计算需要绘制的内容的高度
                Rect rect = new Rect();
                getPaint().getTextBounds(getText().toString(), 0, getText().toString().length(), rect);
                int textHeight = rect.height();
                int drawablePadding = getCompoundDrawablePadding();
                int drawableHeight = drawable.getIntrinsicHeight();
                // 计算总内容的高度
                float bodyHeight = textHeight + drawablePadding + drawableHeight;
                canvas.translate(0, (getHeight() - bodyHeight) / 2);
                // 计算圆心的位置,(注:这里可以根据需求,移动圆心的位置,也可以设置成一个参数来调整位置,这样就不需要翻代码了)
                cx = (mWidth + drawable.getIntrinsicWidth()) / 2;
                cy = mBadgeRadius / 2;
            }
        }

        super.onDraw(canvas);
        drawBadge(canvas, cx, cy);

    }

    /**
     * 绘制 Badge
     *
     * @param canvas
     * @param cx
     * @param cy
     */
    private void drawBadge(Canvas canvas, int cx, int cy) {
        if (mBadgeNumber <= 0) {    // 如果显示的数量 < 1,则不需要绘制
            return;
        }
        // 设置画圆的颜色
        mPaint.setColor(mBadgeColor);
        // 绘制圆
        canvas.drawCircle(cx, cy, mBadgeRadius, mPaint);

        // 将需要绘制的文本转换成字符串,如果超过三位数,则使用省略号替代
        String badgeContent = mBadgeNumber < 100 ? String.valueOf(mBadgeNumber) : "···";
        // 设置文本的大小
        mPaint.setTextSize(mBadgeNumberSize);
        // 设置文本的颜色
        mPaint.setColor(mBadgeNumberColor);
        // 计算包裹此字符串的矩形
        Rect rect = new Rect();
        mPaint.getTextBounds(badgeContent, 0, badgeContent.length(), rect);

        // 设置文本的对齐方式(Paint.Align.LEFT, Paint.Align.CENTER, Paint.Align.RIGHT)
        mPaint.setTextAlign(Paint.Align.CENTER);

        // 计算文本的基线
        Paint.FontMetrics metrics = mPaint.getFontMetrics();
        float ascent = metrics.ascent;
        float descent = metrics.descent;
        double centY = (descent - ascent) / 2;
        float baseLineY = (float) (ascent + centY);

        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(1);

        // 绘制文本
        canvas.drawText(badgeContent, cx, cy - baseLineY, mPaint);
    }

代码的主要功能差不多都打了注释,差不多就是让 TextView 的 drawable 图片同文本一起居中显示(这里主要写了 drawableLeft()drawableTop() 这两个基本上算是 TextView 里面用的比较多的),然后就是计算 圆心的位置,这里需要注意的是 Canvas 对象有过 translate(x,y) 平移操作,此时,屏幕的左上角坐标不再是(0, 0),而是(-x,-y),因此计算圆心是基于平移以后的坐标。
接下来就是绘制 Badge 相关的操作:

  • 首先需要判断数字如果 < 1 则直接返回,不需要任何绘制,其次,数字如果是 > 99,即三位数,我们应当使用符号来替代 可以使用 99+ 或者 ··· 来表示;
  • 绘制圆形背景,这个简单,圆心我们已经计算好了;
  • 拿到要绘制的文本后我们需要对文本的宽高进行计算,Paint 类里面给我们提供了一个方法 mPaint.getTextBounds(String text, int start, int end, Rect bounds) 计算的结果会保存在我们传入的一个 矩形(Rect) 中,调用此方法需要在设置好text一些参数后调用。
  • 设置文本的对齐方式,具体的三种在注释上已经写明了,具体使用我们将会下一个模块分析;
  • 计算文本绘制的基线,我们也将在下一个模块来分析;
  • 最后就是我们的文本绘制了;
    到这里,我们的代码层面基本算是完成了,只需要调用并设置值就可以达到上面截图的效果了,关于文本的对齐方式,基线的寻找我们接下来会详细分析;


    Android 为控件增加数字提示,DrawText 方法解析_第7张图片
    布局文件代码

drawText(String text, float x, float y, Paint paint)

这个方法相信很多人都用过,但是经常用的很头疼,如果对参数不了解的经常会遇到绘制的结果与预想的出入很大,接下来我们重点来看下这个方法;
先看下google提供的参数说明:


Android 为控件增加数字提示,DrawText 方法解析_第8张图片
drawText()

第一个第四个参数肯定没有问题,分别是需要绘制的文本和绘制文本的画笔,我们看下其它两个参数:

  • x: 文本绘制的原点的 x 坐标
  • y: 文本绘制的基线的 y 坐标

卧槽原点是啥,基线又是啥 @A@;

我们先来解释下 x 参数,细心的朋友可能已经注意到了前面我们使用到过一个方法mPaint.setTextAlign(),同样我们看下 google 给我们提供的文档:

Android 为控件增加数字提示,DrawText 方法解析_第9张图片
文字对齐

大致的意思是说 设置需要绘制的文本的对齐方式,他控制了文本相对于其原点的位置, 左对齐表示所有的文本都会被绘制在原点的右边(即,原点决定了文本的左边缘)等; 很显然这个方法已经告诉我们原点是什么,差不多就是一个绘制时我们需要对齐的参照点,接下来我们再看下方法的参数,Paint.Align :

 /**
     * Align specifies how drawText aligns its text relative to the
     * [x,y] coordinates. The default is LEFT.
     */
    public enum Align {
        /**
         * The text is drawn to the right of the x,y origin
         */
        LEFT    (0),
        /**
         * The text is drawn centered horizontally on the x,y origin
         */
        CENTER  (1),
        /**
         * The text is drawn to the left of the x,y origin
         */
        RIGHT   (2);

        private Align(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;
    }

源码很简单就是一个枚举类型,而且只有三个实例,分别表示原点为字符串的左侧,中间,右侧;三种对齐方式我们都测试一遍:
新建一个文件 DrawView ,直接继承自 View,设置 把 x 值都设置为控件的中心,只改变对齐方式:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();

        // 在水平和垂直方向上分别绘制中线
        mPaint.setColor(Color.BLACK);
        canvas.drawLine(mWidth / 2, 0, mWidth / 2, mHeight, mPaint);
        canvas.drawLine(0, mHeight / 2, mWidth, mHeight / 2, mPaint);
        // 绘制文本
        mPaint.setColor(Color.RED);
        String text = "Thinking In Java";
        canvas.drawText(text, mWidth / 2, mHeight / 2, mPaint);
    }
Android 为控件增加数字提示,DrawText 方法解析_第10张图片
setTextAlign

显然 x 参数就是表明了要绘制文本的对齐方式,而且使用方式非常的简洁;

关于 y 参数,我们先来看一张图片


英文书写规范

有莫有一种很熟悉的感觉,一般来说每个英文对应着四条横线,而且都是以第三条线条为基准,然后在根据每个字符的规则写上对应的字符,这条线(第三条线)就类似我们的第三个参数 y(baseline),那么我们该如何寻找到这条线,先来看一个 Paint 的静态内部类, FontMetrics,我们可以通过我们定义的 Paint 来获取此对象:

  /**
     * 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;
    }

源码非常简单,我们主要看下 ascentdescent 两个成员变量的注释,大致上的意思是 根据基线,系统推荐的顶部间距和底部间距,看到这里我们在结合之前的英文字母书写规范来理解下这两个参数,ascent 类似于第一条线,descent 类似于第四条线,我们在屏幕上绘制一下这两条线,同时获取下这两个参数的值

metrics-value

可以看出 ascent < 0 , descent > 0,根据显示器的坐标规则,我们就可以理解为什么了,在测量的时候我们没有指定 baseline 的值,系统默认为0, ascent 位于baseline的上面,因此是负数,同理 descent就为正数。
我们在对齐方式的 onDraw() 方法最下面添加以下几行代码:

@Override
    protected void onDraw(Canvas canvas) {
    
        // ... 省略对齐方式中的代码
        
        Paint.FontMetrics metrics = mPaint.getFontMetrics();
        float ascent = metrics.ascent;
        float descent = metrics.descent;
        Log.e("TAG", "ascent = " + ascent + "   descent = " + descent);
        mPaint.setStrokeWidth(3);
        mPaint.setColor(Color.BLUE);
        // 这里我们指定了 baseline 为 mHeight/2,因此 ascent 需要加上 mHeight/2,descent 同理
        canvas.drawLine(0, ascent + mHeight / 2, mWidth, ascent + mHeight / 2, mPaint);
        mPaint.setColor(Color.GREEN);
        canvas.drawLine(0, descent + mHeight / 2, mWidth, descent + mHeight / 2, mPaint);
    }

我们再来看下效果图,嗯哼,好像是那么一回事了:

Android 为控件增加数字提示,DrawText 方法解析_第11张图片
metrics-image

到这里,我想计算出 baseline 的值应该就不是什么难事了,当然 FontMetrics 类里面还有两个参数, topbottom,指的是允许绘制的最大高度和最大的底部,个人理解是数字和英文字符使用 ascentdescent这两个值就够了:
distanceY = (descent - ascent)/ 2 + ascent = (descent + ascent) / 2;
或:
distanceY = (bottom- top)/ 2 + top= (bottom+ top) / 2;
此时的distanceY<0;
通过打印的日志我们可以看出,上面公式计算出来的是 ascentdescent 两条线围成的矩形的中心点到 baseline 的距离,而且是个负数,回到我们最开始的需求,在圆内绘制文本,这里的中心肯定就是我们的圆心,中心点的坐标我们已经得到,那么 基线的坐标就等于我们计算出来的|distanceY|+圆心的坐标,即:
baseline = cy + |distanceY|;
或:
baseLine = cy - distanceY;
如果看到这里,那么恭喜你,我扯淡结束了 @A@!

你可能感兴趣的:(Android 为控件增加数字提示,DrawText 方法解析)