Android 文字绘制基础

11 篇文章 0 订阅

1、如何绘制文字居中

1.1 纵向居中——计算基准线

调用 Canvas.drawText() 可以在给定的 x、y 坐标使用给定的 Paint 对象绘制出给定的文字。其中 x 就是文字在 x 轴的起始坐标,而 y 则是文字基准线 baseline 的 y 轴坐标。

Android 的 Paint 提供了内部类 FontMetrics 用于描述文字高度范畴内的各种线的位置:

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

可以从注释中提取出下图信息:

请添加图片描述
如果想让文字显示在一个 View 的中间位置,可以参考上图右侧的推导公式,示例代码:

	private fun calculateBaseline(paint: Paint): Float {
        return height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2
    }

需要注意的是,用 decent - accent 求出的是正常文字高度,而 bottom - top 求出的是最大文字高度。

1.2 横向居中——两种方式

两种方式都需要通过 Paint 设置:

  1. 通过 drawText() 设置 x 轴的绘制起点为 View 的横向中点,同时设置 Paint 的对齐方式为 Paint.Align.CENTER:

    	private val mPaint = Paint()
        private val mText = "居中字体"
    
        // onDraw() 调用频繁,尽量不要在该方法内创建新的对象,会频繁 GC 导致卡顿
    	override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
    
            // 以中间对齐的方式,以 width / 2f 为 x 轴起点绘制文字
            mPaint.textAlign = Paint.Align.CENTER
            mPaint.textSize = 80f
    
            val baseline = calculateBaseline(paint)
            canvas?.drawText(mText, width / 2f, baseline, mPaint)
        }
    
  2. 通过 drawText() 设置 x 轴的绘制起点为 ViewWidth / 2 - TextWidth / 2,其中 TextWidth 可以通过 Paint.measureText() 测量出来:

    	private val mPaint = Paint()
     	private val mText = "居中字体"
    
    	override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
    
            mPaint.textSize = 80f
    
            val baseline = calculateBaseline(paint)
            val xOrigin = (width - calculateTextWidth(mPaint, mText)) / 2
            canvas?.drawText(mText, xOrigin, baseline, mPaint)
        }
    
    	private fun calculateBaseline(paint: Paint): Float {
            return height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2
        }
    

2、实现文字渐变效果

在开始实现文字渐变效果前,有一些 Canvas 的基础知识需要了解一下。

2.1 Canvas 相关知识

屏幕显示与 Canvas 的关系

Canvas 相当于一个透明层,每次 Canvas 通过 onDraw() 画图时都会产生一个透明层,在这个透明层上画图然后覆盖在屏幕上显示。例如 canvas.drawRect(rect1, paint_green):

请添加图片描述

Canvas 的保存与回滚

Canvas 还提供了保存和回滚属性的方法 save 和 restore,比如你可以用 save() 先保存目前画纸的位置,然后旋转 90 度,向下移动 100 像素后画一些图形,画完后调用 restore() 返回到刚才保存的位置。

save() 和 restore() 操作于栈内,如图:

请添加图片描述

Canvas 画布剪裁

Canvas 类提供了 clipRect() 用于对画布进行矩形剪裁,它会剪裁我们想要的绘制区域,我们要实现的文字渐变效果主要依赖于该方法:

请添加图片描述

2.2 具体实现

先来看一下实现效果:

请添加图片描述
基本的实现思路是,黑色字体作为背景字体,红色字体作为前景字体,绘制前后两层,红色字体在绘制时要通过 Canvas.clipRect() 剪裁 Canvas,通过动画不断增大右边界使得 Canvas 不断向右侧延长,这样在该 Canvas 上绘制出的红色字体也就会向右延伸。参考代码:

class MyTextView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
    AppCompatTextView(context, attrs, defStyleAttr) {

    private val mBackgroundPaint: Paint
    private val mForegroundPaint: Paint
    private val mText = "岩烧店的烟味弥漫 隔壁是国术馆"

    // 要绘制的文字的基准线 y 轴坐标
    private var mBaseline = 0f
    
    // 要绘制的文字的起点 x 轴坐标
    private var mXOrigin = 0f
    
    // 文字宽度
    private var mTextWidth = 0f
    
    // 前景文字宽度展示比例
    private var textWidthPercent = 0f

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    init {
        mBackgroundPaint = Paint()
        mBackgroundPaint.textSize = 70f

        mForegroundPaint = Paint(mBackgroundPaint)
        mForegroundPaint.color = Color.RED

        mTextWidth = calculateTextWidth(mForegroundPaint, mText)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        mBaseline = calculateBaseline(paint)
        mXOrigin = (width - mTextWidth) / 2
        drawBackgroundText(canvas)
        drawForegroundText(canvas)
    }

    /**
     * 画背景字体
     */
    private fun drawBackgroundText(canvas: Canvas?) {
        canvas?.drawText(mText, mXOrigin, mBaseline, mBackgroundPaint)
    }

    /**
     * 画前景字体
     */
    private fun drawForegroundText(canvas: Canvas?) {
        canvas?.save()
        val top = mBaseline + mForegroundPaint.fontMetrics.ascent
        val bottom = mBaseline + mForegroundPaint.fontMetrics.descent
        var right = mXOrigin + textWidthPercent * mTextWidth

        // 将 canvas 按照四个方向边界截出一个矩形,将前景字画在该矩形上
        canvas?.clipRect(mXOrigin, top, right, bottom)
        canvas?.drawText(mText, mXOrigin, mBaseline, mForegroundPaint)
        canvas?.restore()
    }

    /**
     * 计算文字基准线纵坐标
     */
    private fun calculateBaseline(paint: Paint): Float {
        return height / 2 - (paint.fontMetrics.ascent + paint.fontMetrics.descent) / 2
    }

    /**
     * 计算文字宽度
     */
    private fun calculateTextWidth(paint: Paint, text: String): Float {
        return paint.measureText(text)
    }

    /**
     * textWidthPercent 属性的 getter 与 setter,供属性动画使用
     */
    fun setTextWidthPercent(percent: Float) {
        textWidthPercent = percent
        invalidate()
    }

    fun getTextWidthPercent() = textWidthPercent
}

然后可以在 Activity 中启动属性动画,让前景文字展示的宽度比例递增:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val handler = Handler(Looper.getMainLooper())
        handler.postDelayed({ startTextAnimator() }, 2000)
    }

    private fun startTextAnimator() {
        val textView = findViewById<TextView>(R.id.text_view)
        val animator = ObjectAnimator.ofFloat(textView, "textWidthPercent", 0f, 1f)
        animator.setDuration(3000)
        animator.start()
    }
}

以上代码就能完成这个功能,最后我们做一点优化,就是文字实际上用一层就能实现,当前用前景和背景绘制了两层,属于过度绘制了。因此我们需要调整一下背景文字绘制的起始点坐标:

	override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        mBaseline = calculateBaseline(paint)
        mXOrigin = (width - mTextWidth) / 2
        mTop = mBaseline + mForegroundPaint.fontMetrics.ascent
        mBottom = mBaseline + mForegroundPaint.fontMetrics.descent
        mForegroundRight = mXOrigin + textWidthPercent * mTextWidth

        drawBackgroundText(canvas)
        drawForegroundText(canvas)
    }

    /**
     * 画背景字体
     */
    private fun drawBackgroundText(canvas: Canvas?) {
        canvas?.save()
        // 截取一个左边界不断右移的矩形作为背景字体的画布
        canvas?.clipRect(mForegroundRight, mTop, mXOrigin + mTextWidth, mBottom)
        canvas?.drawText(mText, mXOrigin, mBaseline, mBackgroundPaint)
        canvas?.restore()
    }

Demo 代码可参考GitHub

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值