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

摘要

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

8512eed2b611

微信消息数量

8512eed2b611

当当购物车的数量

分析

实现的方式有很多种,这里采用自定义控件的方式来实现(可以在所需要用到此功能的控件上进行扩展)。

我们可以自定义一个控件,继承自我们需要用到的控件(RadioButton,TextView等),然后我们只需要重写 onDraw(Canvas canvas) 方法,当然如果你想在布局文件中就对数字提示进行一些初始化的操作(背景颜色,位置,文本颜色等),我们可以通过自定义属性,在 attr 文件里面声明然后在 在含有AttributeSet参数的构造方法里面获取自定义属性的相关的值。

重写 onDraw(Canvas canvas) 的关键在于找到需要绘制圆形(也可以是其它形状,使用canvas.drawPath())的圆心,然后就是如何将文本绘制在我们绘制的圆的中间位置,这里我们使用的是canvas.drawText(String text, float x, float y, Paint paint) 这个方法,具体使用下面会详细分析。

效果演示

先上个效果图:

8512eed2b611

并排显示

8512eed2b611

上下显示

8512eed2b611

上下显示2

8512eed2b611

上下显示3

实现过程

新建一个类继承我们的目标控件(这里我们选用TextView)

8512eed2b611

自定义控件

自定义属性

这里我们需要对圆形的背景颜色,半径,以及文本的颜色,大小等属性进行定义。

获取之定义属性的值

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();

重写 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一些参数后调用。

设置文本的对齐方式,具体的三种在注释上已经写明了,具体使用我们将会下一个模块分析;

计算文本绘制的基线,我们也将在下一个模块来分析;

最后就是我们的文本绘制了;

到这里,我们的代码层面基本算是完成了,只需要调用并设置值就可以达到上面截图的效果了,关于文本的对齐方式,基线的寻找我们接下来会详细分析;

8512eed2b611

布局文件代码

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

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

先看下google提供的参数说明:

8512eed2b611

drawText()

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

x: 文本绘制的原点的 x 坐标

y: 文本绘制的基线的 y 坐标

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

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

8512eed2b611

文字对齐

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

}

8512eed2b611

setTextAlign

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

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

8512eed2b611

英文书写规范

有莫有一种很熟悉的感觉,一般来说每个英文对应着四条横线,而且都是以第三条线条为基准,然后在根据每个字符的规则写上对应的字符,这条线(第三条线)就类似我们的第三个参数 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;

}

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

8512eed2b611

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);

}

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

8512eed2b611

metrics-image

到这里,我想计算出 baseline 的值应该就不是什么难事了,当然 FontMetrics 类里面还有两个参数,top 和 bottom,指的是允许绘制的最大高度和最大的底部,个人理解是数字和英文字符使用 ascent 和 descent这两个值就够了:

distanceY = (descent - ascent)/ 2 + ascent = (descent + ascent) / 2;

或:

distanceY = (bottom- top)/ 2 + top= (bottom+ top) / 2;

此时的distanceY<0;

通过打印的日志我们可以看出,上面公式计算出来的是 ascent 和 descent 两条线围成的矩形的中心点到 baseline 的距离,而且是个负数,回到我们最开始的需求,在圆内绘制文本,这里的中心肯定就是我们的圆心,中心点的坐标我们已经得到,那么 基线的坐标就等于我们计算出来的|distanceY|+圆心的坐标,即:

baseline = cy + |distanceY|;

或:

baseLine = cy - distanceY;

如果看到这里,那么恭喜你,我扯淡结束了 @A@!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值