自定义Android滑块拼图验证控件

自定义Android滑块拼图验证控件

1、继承自AppCompatImageView,兼容ImageView的scaleType设置,可设置离线/在线图片。
2、通过设置滑块模型(透明背景的图形块)设置滑块(和缺省块)样式,可修改缺省块颜色。
效果图

拼图认证视图

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

    private var mState = STATE_IDEL //当前状态

    // right bottom 禁用
    private var piercedPositionInfo: RectF? = null //拼图缺块阴影的位置

    // right bottom 禁用
    private var thumbPositionInfo: RectF? = null //拼图缺块的位置
    private var thumbBlock: Bitmap? = null //拼图缺块Bitmap
    private var piercedBlock: Bitmap? = null
    private var thumbPaint: Paint? = null//绘制拼图滑块的画笔
    private var piercedPaint: Paint? = null//绘制拼图缺块的画笔
    private var startTouchTime: Long = 0 //滑动/触动开始时间
    private var looseTime: Long = 0 //滑动/触动松开时间
    private var blockSize = DEF_BLOCK_SIZE
    private var mTouchEnable = true //是否可触动
    private var callback: Callback? = null
    private var mStrategy: CaptchaStrategy? = null
    private var mMode = Captcha.MODE_BAR //Captcha验证模式
    private val xModeDstIn = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
    private var isReversal = false
    private var middlewarePaint: Paint? = Paint()
    private val srcRect = Rect()
    private val dstRect = RectF()

    override fun onDetachedFromWindow() {
        mStrategy?.onDetachedFromWindow()
        thumbBlock?.recycle()
        piercedBlock?.recycle()
        thumbBlock = null
        thumbPaint = null
        piercedPositionInfo = null
        thumbPositionInfo = null
        callback = null
        piercedPaint = null
        middlewarePaint = null
        super.onDetachedFromWindow()
    }

    interface Callback {
        fun onSuccess(time: Long)
        fun onFailed()
    }

    private var tempX = 0f
    private var tempY = 0f
    private var downX = 0f
    private var downY = 0f

    init {
        setCaptchaStrategy(DefaultCaptchaStrategy(context))
    }

    private fun initDrawElements() {
        // 创建缺省镂空位置
        piercedPositionInfo ?: mStrategy?.getPiercedPosition(width, height, blockSize)
            ?.also {
                piercedPositionInfo = it
                thumbPositionInfo =
                    mStrategy?.getThumbStartPosition(width, height, blockSize, mMode, it)
            }

        // 创建滑块
        thumbBlock ?: createBlockBitmap().apply {
            thumbBlock = this
        }
    }

    private fun getBlockWidth() = if (isReversal) blockSize.height else blockSize.width
    private fun getBlockHeight() = if (isReversal) blockSize.width else blockSize.height

    private fun getRealBlockWidth() =
        getBlockWidth() + (mStrategy?.getThumbShadowInfo()?.size?.toFloat() ?: 0f)

    private fun getRealBlockHeight() =
        getBlockHeight() + (mStrategy?.getThumbShadowInfo()?.size?.toFloat() ?: 0f)

    /**
     * 生成拼图滑块和阴影图片
     */
    private fun createBlockBitmap(): Bitmap {
        // 获取背景图
        val origBitmap = getOrigBitmap()

        // 获取滑块模板
        val templateBitmap = getTempBitmap()

        if (blockSize.width != blockSize.height) {
            isReversal = templateBitmap.width == blockSize.height.toInt()
        }

        val resultBmp = Bitmap.createBitmap(
            getBlockWidth().toInt(),
            getBlockHeight().toInt(),
            Bitmap.Config.ARGB_8888
        )

        // 创建滑块画板
        middlewarePaint?.run {
            reset()
            isAntiAlias = true

            val canvas = Canvas(resultBmp)

            // 裁剪镂空位置
            val cropLeft = ((piercedPositionInfo?.left)?.toInt() ?: 0)
            val cropTop = ((piercedPositionInfo?.top)?.toInt() ?: 0)

            srcRect.set(
                cropLeft,
                cropTop,
                cropLeft + getBlockWidth().toInt(),
                cropTop + getBlockHeight().toInt()
            )
            dstRect.set(0f, 0f, getBlockWidth(), getBlockHeight())

            // 从原图上rect区间裁剪与画板上rectR区域重叠
            canvas.drawBitmap(
                origBitmap,
                srcRect,
                dstRect,
                this
            )
            srcRect.set(0, 0, getBlockWidth().toInt(), getBlockHeight().toInt())
            // 选择交集取上层图片
            xfermode = xModeDstIn
            // 绘制底层模板dst
            canvas.drawBitmap(
                templateBitmap,
                srcRect,
                dstRect,
                this
            )
        }

        return getRealThumbBitmap(resultBmp).apply {
            createPiercedBitmap(templateBitmap)
            origBitmap.recycle()
        }
    }

    // 获取缺省模板模型
    private fun getTempBitmap() = mStrategy?.getThumbBitmap(blockSize)
        ?: Utils.getBitmap(R.drawable.capt_def_puzzle, blockSize)

    private fun getOrigBitmap() =
        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
            val canvasOrig = Canvas(this)
            // 复原ImageView中显示操作 防止缺省位置错位
            canvasOrig.concat(imageMatrix)
            drawable.draw(canvasOrig)
        }

    /**
     * 设置带阴影的滑块
     */
    private fun getRealThumbBitmap(resultBmp: Bitmap) =
        mStrategy?.getThumbShadowInfo()?.run {
            Utils.addShadow(resultBmp, this)
        } ?: resultBmp

    /**
     * 获取滑块图片
     */
    private fun createPiercedBitmap(templateBitmap: Bitmap) {
        piercedBlock = (mStrategy?.piercedColor() ?: Color.TRANSPARENT).let {
            if (it == Color.TRANSPARENT) {
                templateBitmap
            } else {
                createColorBitmap(templateBitmap, it)
            }
        }
    }

    /**
     * 获取滑块模型形状的纯色图片
     */
    private fun createColorBitmap(templateBitmap: Bitmap, color: Int, isRecycle: Boolean = true) =
        Bitmap.createBitmap(
            getBlockWidth().toInt(),
            getBlockHeight().toInt(),
            Bitmap.Config.ARGB_8888
        ).apply {
            val c = Canvas(this)
            c.drawColor(color)
            middlewarePaint?.run {
                reset()
                xfermode = xModeDstIn
                // 从原图上rect区间裁剪与画板上rectR区域重叠
                c.drawBitmap(
                    templateBitmap,
                    srcRect,
                    dstRect,
                    this
                )
                if (isRecycle) {
                    templateBitmap.recycle()
                }
            }
        }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        initDrawElements()
        if (mState != STATE_ACCESS) {
            // 绘制缺块位置
            piercedPaint?.runWith(piercedPositionInfo, piercedBlock) { p, i, b ->
                if (mStrategy?.drawPiercedBitmap(canvas, p, i, b) != true) {
                    canvas.drawBitmap(b, i.left, i.top, p)
                }
            }
        }
        if (mState == STATE_MOVE || mState == STATE_IDEL || mState == STATE_DOWN || mState == STATE_UNACCESS) {
            // 绘制滑块
            thumbPaint?.runWith(thumbPositionInfo, thumbBlock) { p, i, b ->
                if (mStrategy?.drawThumbBitmap(canvas, p, i, b) != true) {
                    val offset = (mStrategy?.getThumbShadowInfo()?.size?.toFloat()) ?: 0f
                    canvas.drawBitmap(
                        b,
                        (i.left - offset).coerceAtLeast(0f),
                        (i.top - offset).coerceAtLeast(0f),
                        p
                    )
                }
            }
        }
    }

    private fun Paint.runWith(
        t: RectF?,
        bm: Bitmap?,
        block: (Paint, RectF, Bitmap) -> Unit
    ): Paint {
        return this.also { p ->
            t?.let { rect ->
                bm?.let { b ->
                    if (!b.isRecycled) {
                        block(p, rect, b)
                    }
                }
            }
        }
    }

    /**
     * 按下滑动条(滑动条模式)
     */
    fun down(progress: Int) {
        if (isEnabled) {
            startTouchTime = System.currentTimeMillis()
            mState = STATE_DOWN
            thumbPositionInfo?.left = progress / 100f * (width - getRealBlockWidth())
            invalidate()
        }
    }

    /**
     * 触动拼图块(触动模式)
     */
    private fun downByTouch(x: Float, y: Float) {
        if (isEnabled) {
            mState = STATE_DOWN
            thumbPositionInfo?.run {
                left = x - getRealBlockWidth() / 2f
                top = y - getRealBlockHeight() / 2f
            }
            startTouchTime = System.currentTimeMillis()
            invalidate()
        }
    }

    /**
     * 移动拼图缺块(滑动条模式)
     */
    fun move(progress: Int) {
        if (isEnabled) {
            mState = STATE_MOVE
            thumbPositionInfo?.left = progress / 100f * (width - getRealBlockWidth())
            invalidate()
        }
    }

    /**
     * 触动拼图缺块(触动模式)
     */
    private fun moveByTouch(offsetX: Float, offsetY: Float) {
        if (isEnabled) {
            mState = STATE_MOVE
            thumbPositionInfo?.run {
                left = (left + offsetX.toInt()).coerceAtMost(width - getRealBlockWidth())
                top = (top + offsetY.toInt()).coerceAtMost(height - getRealBlockHeight())
            }
            invalidate()
        }
    }

    /**
     * 松开
     */
    fun loose() {
        if (isEnabled) {
            mState = STATE_LOOSEN
            looseTime = System.currentTimeMillis()
            checkAccess()
            invalidate()
        }
    }

    /**
     * 复位
     */
    fun reset() {
        mState = STATE_IDEL
        thumbPositionInfo = null
        thumbBlock?.recycle()
        thumbBlock = null
        piercedBlock?.recycle()
        piercedBlock = null
        isReversal = false
        piercedPositionInfo = null
        invalidate()
    }

    fun unAccess() {
        mState = STATE_UNACCESS
        invalidate()
    }

    fun access() {
        mState = STATE_ACCESS
        invalidate()
    }

    fun callback(callback: Callback?) {
        this.callback = callback
    }

    fun setCaptchaStrategy(strategy: CaptchaStrategy) {
        mStrategy = strategy
        thumbPaint = strategy.thumbPaint
        piercedPaint = strategy.piercedPaint
        setLayerType(LAYER_TYPE_SOFTWARE, thumbPaint)
        if (!isInLayout) {
            invalidate()
        }
    }

    fun setBlockSize(size: SizeF) {
        blockSize = size
        reset()
    }

    fun setBitmap(bitmap: Bitmap?) {
        setImageBitmap(bitmap)
    }

    override fun setImageBitmap(bm: Bitmap?) {
        super.setImageBitmap(bm)
        reset()
    }

    override fun setImageDrawable(drawable: Drawable?) {
        super.setImageDrawable(drawable)
        reset()
    }

    override fun setImageURI(uri: Uri?) {
        super.setImageURI(uri)
        reset()
    }

    override fun setImageResource(resId: Int) {
        super.setImageResource(resId)
        reset()
    }

    fun setMode(@Captcha.Mode mode: Int) {
        mMode = mode
        isEnabled = true
        reset()
    }

    fun setTouchEnable(enable: Boolean) {
        mTouchEnable = enable
    }

    private fun getFaultTolerant() = (mStrategy?.getFaultTolerant()) ?: DEF_TOLERANCE

    /**
     * 检测是否通过
     */
    private fun checkAccess() {
        thumbPositionInfo?.let { info ->
            piercedPositionInfo?.run {
                val faultTolerant = getFaultTolerant()
                if (abs(info.left - left) < faultTolerant && abs(
                        info.top - top
                    ) < faultTolerant
                ) {
                    access()
                    callback?.onSuccess(looseTime - startTouchTime)
                } else {
                    unAccess()
                    callback?.onFailed()
                }
            }
        }
    }

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        //触动模式下,点击超出拼图缺块的区域不进行处理
        thumbPositionInfo?.let {
            if (event.action == MotionEvent.ACTION_DOWN
                && mMode == Captcha.MODE_NONBAR
                && (event.x < it.left || event.x > it.left + getRealBlockWidth() || event.y < it.top || event.y > it.top + getRealBlockHeight())
            ) {
                return false
            }
        }
        return super.dispatchTouchEvent(event)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (mMode == Captcha.MODE_NONBAR && mTouchEnable && isEnabled) {
            thumbBlock?.run {
                val x = event.x
                val y = event.y
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        downX = x
                        downY = y
                        downByTouch(x, y)
                    }

                    MotionEvent.ACTION_UP -> loose()
                    MotionEvent.ACTION_MOVE -> {
                        val offsetX = x - tempX
                        val offsetY = y - tempY
                        moveByTouch(offsetX, offsetY)
                    }
                }
                tempX = x:
                tempY = y
            }
        }
        return true
    }

    companion object {
        //状态码
        private const val STATE_DOWN = 1
        private const val STATE_MOVE = 2
        private const val STATE_LOOSEN = 3
        private const val STATE_IDEL = 4
        private const val STATE_ACCESS = 5
        private const val STATE_UNACCESS = 6
        internal const val DEF_TOLERANCE = 10 //验证的最大容差
        internal val DEF_BLOCK_SIZE = SizeF(50f, 50f) //验证的最大容差
    }
}

默认策略

class DefaultCaptchaStrategy(ctx: Context) : CaptchaStrategy(ctx) {
    private val degreesList = arrayListOf(0, 90, 180, 270)
    private val defBound: ShadowInfo =
        ShadowInfo(SizeUtils.dp2px(3.0f), Color.BLACK,SizeUtils.dp2px(2.0f).toFloat())

    // 滑块模型
    override fun getThumbBitmap(blockSize: SizeF): Bitmap {
        return Utils.getBitmap(
            R.drawable.capt_def_puzzle,
            blockSize,
            getDegrees()
        )
    }

    override fun getThumbShadowInfo() = defBound // 滑块阴影信息

    // 缺省位置
    override fun getPiercedPosition(width: Int, height: Int, blockSize: SizeF): RectF {
        val random = Random()
        val size =
            blockSize.width.coerceAtLeast(blockSize.height).toInt() + getThumbShadowInfo().size
        val left = (random.nextInt(width - size)
            .coerceAtLeast(size)).toFloat()
        val top = (random.nextInt(height - size)
            .coerceAtLeast(getThumbShadowInfo().size)).toFloat()
        return RectF(left, top, 0f, 0f)
    }

    private fun getDegrees(): Int {
        val random = Random()
        return degreesList[random.nextInt(degreesList.size)]
    }

    // 滑块初始位置
    override fun getThumbStartPosition(
        width: Int,
        height: Int,
        blockSize: SizeF,
        mode: Int,
        thumbPosition: RectF
    ): RectF {
        var left = 0f
        val top: Float
        val maxSize = blockSize.width.coerceAtLeast(blockSize.height).toInt()
        if (mode == Captcha.MODE_BAR) {
            top = thumbPosition.top
        } else {
            val random = Random()
            val size = maxSize + getThumbShadowInfo().size
            left = (random.nextInt(width - size)
                .coerceAtLeast(getThumbShadowInfo().size)).toFloat()
            top = (random.nextInt(height - size)
                .coerceAtLeast(getThumbShadowInfo().size)).toFloat()
        }
        return RectF(left, top, 0f, 0f)
    }

    override val thumbPaint: Paint
        get() = Paint().apply {
            isAntiAlias = true
        }

    override val piercedPaint: Paint
        get() = Paint().apply {
            isAntiAlias = true
        }

    override fun drawThumbBitmap(canvas: Canvas, paint: Paint, info: RectF, src: Bitmap): Boolean {
        return false
    }

    override fun drawPiercedBitmap(
        canvas: Canvas,
        paint: Paint,
        info: RectF,
        src: Bitmap
    ): Boolean {
        return false
    }

    // 缺省块颜色
    override fun piercedColor(): Int {
        return ResourcesUtils.getColor(R.color.black_a6)
    }

   // 验证可冗余空间
    override fun getFaultTolerant(): Int {
        return SizeUtils.dp2px(10.0f)
    }
}

工具类

object Utils {

    /**
     * 获取指定大小、指定旋转角度的图片
     */
    @JvmStatic
    fun getBitmap(@DrawableRes resId: Int, size: SizeF, degrees: Int = 0): Bitmap {
        val options = BitmapFactory.Options()
        options.inMutable = true
        val newWidth = size.width.toInt()
        val newHeight = size.height.toInt()
        return ImageUtils.scale(
            BitmapFactory.decodeResource(
                ResourcesUtils.getResources(),
                resId,
                options
            ), newWidth, newHeight, true
        ).let {
            if (degrees > 0) ImageUtils.rotate(
                it,
                degrees, newWidth / 2f, newHeight / 2f, true
            ) else it
        }
    }

    /**
     * 给图片添加阴影
     */
    @JvmStatic
    fun addShadow(
        srcBitmap: Bitmap,
        info: ShadowInfo
    ): Bitmap? {
        val w = 2 * info.size + info.dx.toInt()
        val h = 2 * info.size + info.dy.toInt()
        val dstWidth = srcBitmap.width + w
        val dstHeight = srcBitmap.height + h
        val mask = Bitmap.createBitmap(dstWidth, dstHeight, Bitmap.Config.ALPHA_8)
        val scaleToFit = Matrix()
        val src = RectF(0f, 0f, srcBitmap.width.toFloat(), srcBitmap.height.toFloat())
        val dst = RectF(
            info.size.toFloat(),
            info.size.toFloat(),
            dstWidth - info.size - info.dx,
            dstHeight - info.size - info.dy
        )
        scaleToFit.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER)
        val dropShadow = Matrix(scaleToFit)
        dropShadow.postTranslate(info.dx, info.dy)
        val maskCanvas = Canvas(mask)
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        maskCanvas.drawBitmap(srcBitmap, scaleToFit, paint)
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)
        maskCanvas.drawBitmap(srcBitmap, dropShadow, paint)
        //设置阴影
        val filter = BlurMaskFilter(info.size.toFloat(), BlurMaskFilter.Blur.NORMAL)
        paint.reset()
        paint.isAntiAlias = true
        paint.color = info.color
        paint.maskFilter = filter
        paint.isFilterBitmap = true
        val ret = Bitmap.createBitmap(dstWidth, dstHeight, Bitmap.Config.ARGB_8888)
        val retCanvas = Canvas(ret)
        //绘制阴影
        retCanvas.drawBitmap(mask, 0f, 0f, paint)
        retCanvas.drawBitmap(srcBitmap, scaleToFit, null)
        mask.recycle()
        return ret
    }
}

参考

Android拼图滑块验证码控件:http://blog.csdn.net/sdfsdfdfa/article/details/79120665
关于android:绘制图像时绘制外部阴影:https://www.codenong.com/17783467/
Paint API之—— Xfermode与PorterDuff详解:https://www.kancloud.cn/kancloud/android-tutorial/87249

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值