一、基础准备
这一篇我们来学习一下绘制文字相关,先看一下 API 介绍 自定义 View 1-3 drawText() 文字的绘制。
二、圆环进度条,文字横向、纵向居中
如上图所示,上图的绘制可以分为三步:底圆环、进度条、中间文字。
圆环的绘制可以直接把 Paint 的边加粗然后绘制一个圆框即可即可:
Paint.setStyle(Paint.Style.STROKE) Paint.setStrokeWidth()
进度条的的绘制可以使用圆弧,并且需要设置两个端点的形状:
setStrokeCap(Paint.Cap cap)
对应平头、圆头、方头
接下来是中间的文字,绘制文字的api是:
drawText(String text, float x, float y, Paint paint)
方法的参数很简单:text 是文字内容,x 和 y 是文字的坐标。但需要注意:这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。大概在这里:
所以需要涉及到文字的横向和纵向居中。
横向居中:
setTextAlign(Paint.Align align)
设置文字的对齐方式。一共有三个值:LEFT、CETNER 和 RIGHT。默认值为 LEFT。分别对应下面的效果:
纵向居中:
纵向居中复杂一些,一种是 getTextBounds 获取文字区域占的矩形 Rect,另一种是 getFontMetrics() 获取文字线(有5条),如下图:
两种方式的代码实现如下:
//文字纵向居中 //方式1:getTextBounds,缺点是文字动态变化时,文字会跳动 mPaint.getTextBounds(CENTER_TEXT,0,CENTER_TEXT.length,mCenterTextBound) var offset = (mCenterTextBound.bottom + mCenterTextBound.top)/2 canvas.drawText(CENTER_TEXT,width/2f,height/2f - offset,mPaint) //方式2 var offset = (mFontMetrics.ascent + mFontMetrics.descent)/2 canvas.drawText(CENTER_TEXT,width/2f,height/2f - offset,mPaint)
完整代码如下:
class SportsView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
val RADIUS = Utils.dp2px(150)
val RING_WIDTH = Utils.dp2px(20)
val CIRCLE_COLOR = Color.parseColor("#9C9C9C")
val HIGHT_LIGHT_COLOE = Color.parseColor("#D81B60")
val CENTER_TEXT = "top1"
var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
var mCenterTextBound:Rect = Rect()
var mFontMetrics : Paint.FontMetrics
init {
mPaint.strokeWidth = RING_WIDTH
mPaint.textSize = Utils.dp2px(100)
//文字横向居中
mPaint.textAlign = Paint.Align.CENTER
mFontMetrics = mPaint.fontMetrics
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//圆环
mPaint.style = Paint.Style.STROKE
mPaint.color = CIRCLE_COLOR
canvas.drawCircle(width / 2f, height / 2f, RADIUS, mPaint)
//圆弧
mPaint.color = HIGHT_LIGHT_COLOE
mPaint.strokeCap = Paint.Cap.ROUND
canvas.drawArc(
width / 2f - RADIUS, height / 2f - RADIUS, width / 2f + RADIUS, height / 2f + RADIUS,
-90f, 225f, false, mPaint
)
mPaint.style = Paint.Style.FILL
//文字纵向居中
//方式1:getTextBounds,缺点是文字动态变化时,文字会跳动
// mPaint.getTextBounds(CENTER_TEXT,0,CENTER_TEXT.length,mCenterTextBound)
// var offset = (mCenterTextBound.bottom + mCenterTextBound.top)/2
// canvas.drawText(CENTER_TEXT,width/2f,height/2f - offset,mPaint)
//方式2
var offset = (mFontMetrics.ascent + mFontMetrics.descent)/2
canvas.drawText(CENTER_TEXT,width/2f,height/2f - offset,mPaint)
}
}
三、文字换行
3.1 自动换行
在 Android 中,Canvas.drawText(String) 超过一行时是不会自动换行的,想要换行可以使用 StaticLayout:
StaticLayout 的构造方法是 :
StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)
其中参数里:
- width 是文字区域的宽度,文字到达这个宽度后就会自动换行;
- align 是文字的对齐方向;
- spacingmult 是行间距的倍数,通常情况下填 1 就好;
- spacingadd 是行间距的额外增加值,通常情况下填 0 就好;
- includepad 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。
如果你需要进行多行文字的绘制,并且对文字的排列和样式没有太复杂的花式要求,那么使用 StaticLayout 就好。
class ImageTextView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
val TEXT = "广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android" +
"广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android" +
"广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android"
lateinit var mStaticLayout: StaticLayout
var mPaint = TextPaint()
init {
mPaint.textSize = Utils.dp2px(16)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mStaticLayout = StaticLayout(TEXT, mPaint, width, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
mStaticLayout.draw(canvas)
}
}
效果如下:
3.2 自定义换行
如果想要自己控制换行,就需要用到下面这个方法了:
breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
这个方法也是用来测量文字宽度的。但和 measureText() 的区别是, breakText() 是在给出宽度上限的前提下测量文字的宽度。如果文字的宽度超出了上限,那么在临近超限的位置截断文字。
比如我们想实现下面这个效果:
假如我们直接用这个方法:
canvas.drawText(TEXT, 0f, 50f, mPaint)
会出现下面的效果,文字绘制到屏幕外了:
完整的代码应该这么写:
class ImageTextView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
val BITMAP_WIDTH = Utils.dp2px(100)
val TEXT = "广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123" +
"广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123" +
"广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123" +
"广东省广州市深圳市 Android123广东省广州市深圳市 Android123广东省广州市深圳市 Android123"
var mPaint = TextPaint()
var mBitmap: Bitmap
var cutWidth = FloatArray(1)
init {
mPaint.textSize = Utils.dp2px(16)
mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, BITMAP_WIDTH.toInt())
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mBitmap, width - BITMAP_WIDTH, 100f, mPaint)
//第一行
var index = mPaint.breakText(TEXT, true, width.toFloat(), cutWidth)
canvas.drawText(TEXT, 0, index, 0f, 50f, mPaint)
//第二行
var oldIndex = index
index = mPaint.breakText(TEXT, index, TEXT.length, true, width.toFloat(), cutWidth)
canvas.drawText(TEXT, oldIndex, oldIndex + index, 0f, 50f + mPaint.fontSpacing, mPaint)
//第三行
oldIndex = index + oldIndex
index = mPaint.breakText(TEXT, index, TEXT.length, true, width - BITMAP_WIDTH, cutWidth)
canvas.drawText(TEXT, oldIndex, oldIndex + index, 0f, 50f + mPaint.fontSpacing * 2, mPaint)
}
}
当然,这里只是一个演示,实际项目中要复杂的多,需要自动去判断当前行的文字是否与图片覆盖来达到自动换行,这里就不再细说了。