一种拍摄驾驶证Ui的精妙实现

canvas绘制

Posted by River on April 15, 2022

前言

最近review驾驶证的上传模块业务,里面部分实现写的不错;效果如下,特此记录。

驾驶证拍摄效果图

1、视图的结构

    
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/cert_camera_textureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

 

    <com.xxx.view.RecordMaskView
        android:id="@+id/cert_mask"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        

</RelativeLayout>

2、TextureView的使用,扩大到全屏

简化了视图接口的定义,只拿一个举例子

     cert_camera_textureView.surfaceTextureListener =
            object : TextureView.SurfaceTextureListener {
                override fun onSurfaceTextureAvailable(
                    surface: SurfaceTexture?,
                    width: Int,
                    height: Int
                ) {
                    if (hasVideoRecordPermission()) {
                        startPreview()
                    }

                }

                override fun onSurfaceTextureSizeChanged(
                    surface: SurfaceTexture?,
                    width: Int,
                    height: Int
                ) {
                }

                override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
                    return true
                }

                override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
                }
            }
    
    fun startPreview(holder: SurfaceTexture, width: Int, height: Int) {
        initCamera(width, height)
        try {
            mCamera.setPreviewTexture(holder)
            mCamera.startPreview()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

3、 底部视图的精美实现

涉及到不少画布的操纵,实现确实比较精美

   class RecordMaskView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var currentStatus = DEFAULT

    private val blackColor by lazy {
        ContextCompat.getColor(context, R.color.sc_common_color_000000)
    }


    private val whiteColor by lazy {
        ContextCompat.getColor(context, R.color.color_white_bg)
    }

    private val grayColor by lazy {
        ContextCompat.getColor(context, R.color.sc_common_color_879394)
    }


    private val bigDottedBoxBitmap by lazy {
        BitmapFactory.decodeResource(resources, R.drawable.sc_mask_big_dotted_box)
    }

    private val smallDottedBoxBitmap by lazy {
        BitmapFactory.decodeResource(resources, R.drawable.sc_mask_small_dotted_box)
    }

    companion object {
        const val DEFAULT = 0
        const val PREVIEW = 1
        const val PLAYING = 2
    }

    private var paint: Paint = Paint()

    init {
        paint.isAntiAlias = true
    }


    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.let {
            drawByStatus(it)
        }
    }


    private fun drawByStatus(canvas: Canvas) {
        when (currentStatus) {
            DEFAULT -> drawDefaultView(canvas)
            PREVIEW -> drawPreviewView(canvas)
            PLAYING -> drawPlayingView(canvas)
            else -> {
            }
        }
    }


    fun showDefault() {
        currentStatus = DEFAULT
        postInvalidate()
    }


    fun showPreview() {
        currentStatus = PREVIEW
        postInvalidate()
    }

    fun showPlaying() {
        currentStatus = PLAYING
        postInvalidate()
    }


    /**
     * 默认样式
     */
    private fun drawDefaultView(canvas: Canvas) {
        drawBlackBg(canvas)
        paint.color = grayColor
        val transparentX = dip(16F).toFloat()
        val transparentY = dip(151F).toFloat()
        val transparentWidth = width - 2 * dip(16F).toFloat()
        val transparentHeight = dip(251F).toFloat()
        canvas.drawRoundRect(
            transparentX, transparentY,
            transparentX + transparentWidth,
            transparentY + transparentHeight,
            dip(8F).toFloat(),
            dip(8F).toFloat(),
            paint
        )
        drawVirtualBox(canvas)
    }

    /**
     * 预览样式
     */
    private fun drawPreviewView(canvas: Canvas) {
        val layer = canvas.saveLayer(
            0f, 0f, width.toFloat(), height.toFloat(),
            null, Canvas.ALL_SAVE_FLAG
        )
        drawBlackBg(canvas)
        drawTranCenter(canvas)
        canvas.restoreToCount(layer)
        drawVirtualBox(canvas)
    }

    /**
     * 播放样式
     */
    private fun drawPlayingView(canvas: Canvas) {
        val layer = canvas.saveLayer(
            0f, 0f, width.toFloat(), height.toFloat(),
            null, Canvas.ALL_SAVE_FLAG
        )
        drawBlackBg(canvas)
        drawTranCenter(canvas)
        canvas.restoreToCount(layer)
    }

    private fun drawTranCenter(canvas: Canvas) {
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
        val transparentX = dip(16F).toFloat()
        val transparentY = dip(151F).toFloat()
        val transparentWidth = width - 2 * dip(16F).toFloat()
        val transparentHeight = dip(251F).toFloat()
        canvas.drawRoundRect(
            transparentX, transparentY,
            transparentX + transparentWidth,
            transparentY + transparentHeight,
            dip(8F).toFloat(),
            dip(8F).toFloat(),
            paint
        )
        paint.xfermode = null
    }

    /**
     * 虚线框&&文字
     */
    private fun drawVirtualBox(canvas: Canvas) {
        //外部虚线矩形
        paint.alpha = 0xFF
        val outBoxX = dip(34F).toFloat()
        val outBoxY = dip(173F).toFloat()
        val outBoxWidth = width - 2 * dip(34F)
        val outBoxHeight = dip(207F)

        val outMatrix = Matrix()
        outMatrix.postScale(
            outBoxWidth / (bigDottedBoxBitmap.width * 1.0f),
            outBoxHeight / (bigDottedBoxBitmap.height * 1.0f)
        )
        outMatrix.postTranslate(outBoxX, outBoxY)
        canvas.drawBitmap(bigDottedBoxBitmap, outMatrix, paint)
        //内部虚线矩形
        val innerBoxX = dip(125F).toFloat()
        val innerBoxY = dip(292F).toFloat()
        val innerBoxWidth = width - 2 * dip(125F).toFloat()
        val innerBoxHeight = dip(42F).toFloat()
        val innerMatrix = Matrix()
        innerMatrix.postScale(
            innerBoxWidth / (smallDottedBoxBitmap.width * 1.0f),
            innerBoxHeight / (smallDottedBoxBitmap.height * 1.0f)
        )
        innerMatrix.postTranslate(innerBoxX, innerBoxY)
        canvas.drawBitmap(smallDottedBoxBitmap, innerMatrix, paint)
        //文字
        paint.textAlign = Paint.Align.CENTER
        paint.color = whiteColor
        paint.textSize = sp(16).toFloat()
        val fontMetrics = paint.fontMetrics
        val distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
        val baseline = innerBoxY + innerBoxHeight / 2 + distance
        canvas.drawText(
            context.getString(R.string.sc_cert_mask_tip),
            width / 2f,
            baseline,
            paint
        )
    }

    /**
     * 黑色背景
     */
    private fun drawBlackBg(canvas: Canvas) {
        paint.color = blackColor
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
    }


}

4、总结

纸上得来终觉浅,绝知此事要躬行。

  1. View基础绘制中画布,画笔的使用需要勤加练习方能掌握;
  2. View的自定义控件实在不是一件简单的事儿。