“我正在参加「启航计划」”

前段时间做了一个圆形物品扫码相关的需求,要求做一个扫描动画,扫描线只在圆形区域显现 效果图如下:

Android自定义View扫码ui控件效果

Android自定义View扫码ui控件效果

思路如下:

看到这东西,第一反响便是运用自定义view

  1. 画出一个蒙层,该蒙层能够运用 PorterDuff.Mode的clear
  2. 画一个圆形白色圆框
  3. 制作中心的扫描线(扫描线运用的是bitmap图片),给扫描线做动画使其上下移动
  4. 因为扫描线是矩形图,如何使扫描线只在白色圆框中显现,就又要运用PorterDuff.Mode利用他的色彩图层的规矩,关于图层交互规矩可检查文档:Google 的官方文档,这儿根据文档的规矩我运用了PorterDuff.Mode.DST_IN,//只在源图画和方针图画相交的当地制作方针图画
  5. 这儿所以我画了一个特殊的形状的bitmap,可检查下面办法createCircularBitmap,白色圆形上面是一个矩形,避免我的扫描线bitmap超出我的制作区域。
  6. 需要注意的是bitmap必须要有内容填充,因为PorterDuff.Mode其实是对图画内容的处理

源码如下:注释写的很清楚


/**
 * 识别蒙层
 * 参阅:https://www.jianshu.com/p/2feac6a535a3
 * https://www.jianshu.com/p/134cd2dbb43b
 */
class CoinScanMaskView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    private var circleRadius = 0f
    private var width = 0f
    private var height = 0f
    private var circleDrawLeft = 0f
    private var circleDrawTop = 0f
    private val mPaint = Paint()
    private val mTextPaint = Paint().apply {
        color = resources.getColor(R.color.white)
        textSize = 14.dp.toFloat()
        isAntiAlias = true
        isDither = true
        style = Paint.Style.FILL_AND_STROKE
        textAlign = Paint.Align.CENTER
    }
    private val clearMode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
    private val circleBitmap: Bitmap =
        BitmapFactory.decodeResource(resources, R.drawable.ic_coin_recognition_circle)
    private var tipText = "Ready in a moment…"
    // 扫描线
    private val scanBitmap: Bitmap =
        BitmapFactory.decodeResource(resources, R.drawable.identify_ic_camera_scan_line)
    private var mScanLineTop = 0f
    private val mScanLineRectF = RectF()
    private val clipScanClearMode =
        PorterDuffXfermode(PorterDuff.Mode.DST_IN)//只在源图画和方针图画相交的当地制作方针图画
    private var clipBitmap: Bitmap
    private var needCanvasScan = false
    init {
        clipBitmap = createCircularBitmap()
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        circleRadius = ((circleBitmap.width / 2)).toFloat()
        width = measuredWidth.toFloat()
        height = measuredHeight.toFloat()
        circleDrawLeft = (width - circleRadius * 2) / 2
        circleDrawTop = (height - circleRadius * 2) / 2
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        mPaint.reset()
        val layerID = canvas.saveLayer(0f, 0f, width, height, mPaint)
        // 蒙层背景
        mPaint.color = 0x66000000
        canvas.drawRect(0f, 0f, width, height, mPaint)
        // 中心透明区域
        mPaint.xfermode = clearMode
        mPaint.color = 0x00000000
        canvas.drawCircle(
            width / 2, height / 2, circleRadius,
            mPaint
        )
        canvas.restoreToCount(layerID)
        mPaint.reset()
        // 提示文字
        canvas.drawText(
            tipText,
            (width / 2).toFloat(),
            height / 2 + circleRadius + 32.dp,
            mTextPaint
        )
        //圆形边框
        canvas.drawBitmap(
            circleBitmap,
            circleDrawLeft,
            circleDrawTop,
            mPaint
        )
        //铺开该行代码可检查自己制作的 形状,与下面的scanBitmap扫描的相交
//        canvas.drawBitmap(
//            clipBitmap, circleDrawLeft,
//            circleDrawTop - scanBitmap.height, mPaint
//        )
        //再画上结果
        if (needCanvasScan) {
            val layerScanId =
                canvas.saveLayer(0f, 0f, width, height, null, Canvas.ALL_SAVE_FLAG)
            mScanLineRectF.set(
                circleDrawLeft, circleDrawTop + mScanLineTop - scanBitmap.height,
                circleDrawLeft + clipBitmap.width,
                circleDrawTop + mScanLineTop
            )
            //制作第一层,方针图画,在基层
            canvas.drawBitmap(
                scanBitmap,
                null,
                mScanLineRectF,
                mPaint
            )
            mPaint.xfermode = clipScanClearMode
            //在圆形上面制作一个画布,避免扫描线bitmap透出显现,源图画,第二层盖在了方针图画上
            canvas.drawBitmap(
                clipBitmap, circleDrawLeft,
                circleDrawTop - scanBitmap.height, mPaint
            )
            mPaint.xfermode = null
            canvas.restoreToCount(layerScanId)
            moveScanLine()
        }
    }
    /**
     * 移动扫描线
     */
    private fun moveScanLine() {
        mScanLineTop += 2.dp
        if (mScanLineTop > circleBitmap.height) {
            mScanLineTop = 0f
        }
        postInvalidateDelayed(16, 0, 0, measuredWidth, measuredHeight)
    }
    private fun createCircularBitmap(): Bitmap {
        // 创立一个长方形的 Bitmap
        val size = 190.dp
        // 设置 Bitmap 巨细
        val bitmap = Bitmap.createBitmap(size, size + scanBitmap.height, Bitmap.Config.ARGB_8888)
        // 创立一个 Canvas 目标,将 bitmap 作为制作方针
        val canvas = Canvas(bitmap)
        // 创立一个 Paint 目标,用于设置制作特点
        val paint = Paint().apply {
            color = Color.RED // 设置圆形区域的色彩,必须要有色彩不然组成的时候无效
        }
        // 创立一个圆形途径
        val centerX = size / 2f
        val centerY = size / 2f
        val radius = size / 2f
        //制作一个圆形,因为只想要圆形区域,所以只制作圆形区域
        val path = Path().apply {
            addCircle(centerX, centerY + scanBitmap.height, radius, Path.Direction.CW)
        }
        //在圆形上面制作一个长方形,避免扫描线bitmap透出显现
        //铺开该行代码可检查自己制作的 长方形形状,与下面的scanBitmap扫描的相交
//        path.addRect(0f, 0f, size.toFloat(), scanBitmap.height.toFloat(), Path.Direction.CW)
        // 在 Canvas 上制作圆形途径
        canvas.drawPath(path, paint)
        return bitmap
    }
    override fun onVisibilityChanged(changedView: View, visibility: Int) {
        super.onVisibilityChanged(changedView, visibility)
        needCanvasScan = visibility == View.VISIBLE
        if (needCanvasScan) {
            mScanLineTop = 0f
        }
    }
}

参阅:www.jianshu.com/p/134cd2dbb…