Android 文字绘制和Camera几何变换

文字绘制

文字绘制的位置是基于基线(BaseLine)

// 0F, 0F 指的是基线的位置, 而不是文字的左上角位置, 基线在文字左下角偏上一点点的位置
canvas.drawText(mText, 0F, 0F, mTextPaint)

TextView 内容准确定位

  • getTextBounds

    mTextPaint.getTextBounds 可以获取到这一行文字的准确坐标, 然后根据这个计算基线的位置

    // 绘制文字
    mTextPaint.getTextBounds(mText, 0, mText.length, mTextBounds)
    val offset = (mTextBounds.top + mTextBounds.bottom) / 2F
    canvas.drawText(
        mText,
        width / 2F - (mTextBounds.left + mTextBounds.right) / 2F,
        height / 2F - offset,
        mTextPaint
    )
    
  • FontMetrics

    mTextPaint.getFontMetrics(mFontMetrics)
    mTextPaint.getTextBounds(mText, 0, mText.length, mTextBounds)
    val offset = (mFontMetrics.ascent + mFontMetrics.descent) / 2F
    canvas.drawText(
        mText,
        width / 2F - (mTextBounds.left + mTextBounds.right) / 2F,
        height / 2F - offset,
        mTextPaint
    )
    

绘制多行文字

  • StaticLayout

    val staticLayout = StaticLayout(
        mStr, mTextPaint, width, Layout.Alignment.ALIGN_NORMAL,
        // 行间距是否加倍: 大于1变大,小于1变小
        1.2F,
        // 行间距是否添加固定值增大
        0F,
        // 是否要上下额外的空余
        false
    )
    staticLayout.draw(canvas)
    
  • 通过 breakText 找到换行的位置

    // 绘制全部
    var startIndex = 0
    var count: Int
    var yOffset = mPaint.fontSpacing
    while (startIndex < mStr.length) {
        count = mPaint.breakText(mStr, startIndex, mStr.length, true, width.toFloat(), measuredWidth)
        canvas.drawText(mStr, startIndex, startIndex + count, 0F, yOffset, mPaint)
        startIndex += count
        yOffset += mPaint.fontSpacing
    }
    
  • 图文混排

    // 绘制图片
    canvas.drawBitmap(mBitmap, width - IMAGE_WIDTH, IMAGE_PADDING, mPaint)
    // 绘制文字
    val measuredWidth = floatArrayOf(0F)
    var startIndex = 0
    var count: Int
    var yOffset = mPaint.fontSpacing
    while (startIndex < mStr.length) {
        val textTop = yOffset + mMetrics.ascent
        val textBottom = yOffset + mMetrics.descent
        val usableWidth = if ((textTop > IMAGE_PADDING && textTop < IMAGE_PADDING + IMAGE_WIDTH)
            || (textBottom > IMAGE_PADDING && textBottom < IMAGE_PADDING + IMAGE_WIDTH)
        ) {
            width - IMAGE_WIDTH
        } else {
            width.toFloat()
        }
        count = mPaint.breakText(mStr, startIndex, mStr.length, true, usableWidth, measuredWidth)
        canvas.drawText(mStr, startIndex, startIndex + count, 0F, yOffset, mPaint)
        startIndex += count
        yOffset += mPaint.fontSpacing
    }
    

Canvas 裁切

这里裁切并没有移动坐标系, 裁切的时候要注意 save()restore()

// 裁切
canvas.save()
canvas.clipRect(
    IMAGE_PADDING,
    IMAGE_PADDING,
    IMAGE_PADDING + IMAGE_WIDTH,
    IMAGE_PADDING + IMAGE_WIDTH
)
canvas.drawBitmap(mBitmap, IMAGE_PADDING, IMAGE_PADDING, mPaint)
canvas.restore()
canvas.drawBitmap(mBitmap, 0F, 0F, mPaint)

Canvas 平移, 旋转

这里操作的对象是坐标系, 而不是绘制的内容, 所以要反着写

canvas.translate(IMAGE_PADDING, 0F)
canvas.rotate(45F, IMAGE_PADDING, IMAGE_PADDING)
canvas.drawBitmap(mBitmap, 0F, 0F, mPaint)

Camera 坐标系

  • Android 中的 2D 坐标系

    原点: 屏幕左上角为(0, 0)
    X 轴: 沿着屏幕左上角向右为 X 轴正方向
    Y 轴: 沿着屏幕左上角向下为 Y 轴正方向
    
  • Camera 3D 坐标系

    原点: 屏幕左上角为(0, 0, 0)
    X 轴: 沿着屏幕左上角向右为 X 轴正方向
    Y 轴: 沿着屏幕左上角向上为 Y 轴正方向
    Z 轴: 沿着屏幕左上角向里为 Z 轴正方向, 就是拿手指往屏幕里面戳的那个方向
    

Camera3D坐标系

Camera 旋转

canvas.save()
canvas.translate(IMAGE_WIDTH / 2F, IMAGE_WIDTH / 2F)

mCamera.save()
mCamera.rotateZ(30F)
mCamera.applyToCanvas(canvas)
mCamera.restore()

canvas.translate(-IMAGE_WIDTH / 2F, -IMAGE_WIDTH / 2F)
canvas.drawBitmap(mBitmap, 0F, 0F, mPaint)
canvas.restore()
  • mCamera.rotateX(30F): 意思是绕 X 轴 顺时针 旋转 30 度. 如果物体中间线和 X 轴重合的话,绕 X 轴 顺时针 旋转 30 度就是指物体上半部分向里翻转,下半部分向外翻转;

  • mCamera.rotateY(30F): 意思是绕 Y 轴 顺时针 旋转 30 度. 如果物体中间线和 Y 轴重合的话,绕 Y 轴 顺时针 旋转 30 度就是指物体左半部分向外翻转,右半部分向里翻转;

  • mCamera.rotateZ(30F): 意思是绕 Z 轴 逆时针 旋转 30 度. 如果物体中间线和 Z 轴重合的话,绕 Z 轴 逆时针 旋转 30 度就是指物体上半部分向左旋转,下半部分向右旋转;

    /**
    * 斜着切
    *
    * 这里的代码要倒着理解
    * 正着看是在移动坐标系,不好理解
    * 倒着看是在移动绘制的内容,好理解
    */
    private fun obliqueCut(canvas: Canvas) {
       // 绘制上半部分
       canvas.save()
       canvas.translate(IMAGE_PADDING + IMAGE_WIDTH / 2F, IMAGE_PADDING + IMAGE_WIDTH / 2F)
       canvas.rotate(-30F)
       canvas.clipRect(-IMAGE_WIDTH, -IMAGE_WIDTH, IMAGE_WIDTH, 0F)
       canvas.rotate(30F)
       canvas.translate(-(IMAGE_PADDING + IMAGE_WIDTH / 2F), -(IMAGE_PADDING + IMAGE_WIDTH / 2F))
       canvas.drawBitmap(mBitmap, IMAGE_PADDING, IMAGE_PADDING, mPaint)
       canvas.restore()
    
       // 绘制下半部分
       canvas.save()
       // 把图片挪回去
       canvas.translate(IMAGE_PADDING + IMAGE_WIDTH / 2F, IMAGE_PADDING + IMAGE_WIDTH / 2F)
       // 二维旋转回去
       canvas.rotate(-30F)
    
       // 三维旋转
       mCamera.save()
       mCamera.rotateX(45F)
       mCamera.applyToCanvas(canvas)
       mCamera.restore()
    
       // 因为旋转之后,范围变大了,所以裁切范围也要变大
       canvas.clipRect(-IMAGE_WIDTH, 0F, IMAGE_WIDTH, IMAGE_WIDTH)
       // 二维顺时针旋转
       canvas.rotate(30F)
       // 先把图片中心移动到原点
       canvas.translate(-(IMAGE_PADDING + IMAGE_WIDTH / 2F), -(IMAGE_PADDING + IMAGE_WIDTH / 2F))
       canvas.drawBitmap(mBitmap, IMAGE_PADDING, IMAGE_PADDING, mPaint)
       canvas.restore()
    }
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值