美文网首页Android自定义Android自定义View
Android 为控件增加数字提示,DrawText 方法解析

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

作者: hewenyu | 来源:发表于2018-03-10 12:34 被阅读193次

    摘要

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


    微信消息数量 当当购物车的数量

    分析

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

    效果演示

    先上个效果图:


    并排显示 上下显示 上下显示2 上下显示3

    实现过程

    1. 新建一个类继承我们的目标控件(这里我们选用TextView 自定义控件
    2. 自定义属性
      这里我们需要对圆形的背景颜色,半径,以及文本的颜色,大小等属性进行定义。
     <declare-styleable name="BadgeTextView">
           <attr name="badgeColor" format="color" />
           <attr name="badgeRadius" format="dimension" />
           <attr name="badgeNumber" format="integer" />
           <attr name="badgeNumberColor" format="color" />
           <attr name="badgeNumberSize" format="dimension" />
    </declare-styleable>
    
    
    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一些参数后调用。
    • 设置文本的对齐方式,具体的三种在注释上已经写明了,具体使用我们将会下一个模块分析;
    • 计算文本绘制的基线,我们也将在下一个模块来分析;
    • 最后就是我们的文本绘制了;
      到这里,我们的代码层面基本算是完成了,只需要调用并设置值就可以达到上面截图的效果了,关于文本的对齐方式,基线的寻找我们接下来会详细分析;


      布局文件代码

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

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


    drawText()

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

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

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

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

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

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

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

    相关文章

      网友评论

        本文标题:Android 为控件增加数字提示,DrawText 方法解析

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