Android干货整理

Android 拼图验证码

2020-08-18  本文已影响0人  吾乃韩小呆

公司提了个需求,要求弄个图片验证码代替之前登陆界面手输验证码,对于用户确实很方便,杠杠的提升用户体验,但是对于开发者来说,嘿嘿,恶意满满。折腾一番弄出了一个 Kotlin 版本的简单实现 对了目前还得依靠一下 glide 待过些日子不忙的时候再进行完善吧。投一下效果如下:


效果展示

一、绘制滑块

1、绘制凸起和凹槽

下面的操作,可以绘制出滑块上面的小凸起,或者小凹槽,类似于一个工具类,之前原版是 Java 版本的,我给弄成了 Kotlin 了,算是我已经迈向 Kotlin 的一次声明展示吧,掌握的 Kotlin 还不是十分好,可能可以写的更好,各位看官可以随时指出。

  companion object {
        fun drawPartCircle(start: PointF, end: PointF, path: Path, outer: Boolean) {
            //贝赛尔曲线系数
            val c = 0.551915024494f
            // 中点
            val middle = PointF(start.x + (end.x - start.x) / 2, start.y + (end.y - start.y) / 2)
            //半径
            // kotlin 内求平方根的方法表示为 sqrt() 代替了 Java 内 Math.sqrt()
            // kotlin 的 n次方 表示方式为 pow(n) Java 的 n 次方表示方式为  Math.pow(参数,n 次方)
            val r1 =  sqrt((((middle.x - start.x).toDouble()).pow(2) + (middle.y - start.y).pow(2))).toFloat()
            //gap值
            val gap = r1 * c

            if (start.x == end.x) {
                //绘制竖直方向的
                //是否是从上到下
                val flag = if (end.y - start.y > 0) 1 else -1
                if (outer) {
                    //凸的 两个半圆
                    path.cubicTo(start.x + gap * flag, start.y, middle.x + r1 * flag,
                            middle.y - gap * flag, middle.x + r1 * flag, middle.y)

                    path.cubicTo(middle.x + r1 * flag, middle.y + gap * flag, 
                            end.x + gap * flag, end.y, end.x, end.y)
                } else {
                    //凹的 两个半圆
                    path.cubicTo(start.x - gap * flag, start.y, middle.x - r1 * flag,
                            middle.y - gap * flag, middle.x - r1 * flag, middle.y)

                    path.cubicTo(middle.x - r1 * flag, middle.y + gap * flag,
                            end.x - gap * flag, end.y, end.x, end.y)
                }

            } else {
                //绘制水平方向的
                //是否是从左到右
                val flag = if (end.x - start.x > 0) 1 else -1
                if (outer) {
                    //凸的 两个半圆
                    path.cubicTo(start.x, start.y - gap * flag, middle.x - gap * flag,
                            middle.y - r1 * flag, middle.x, middle.y + -r1 * flag)

                    path.cubicTo(middle.x + gap * flag, middle.y - r1 * flag,
                            end.x, end.y - gap * flag, end.x, end.y)
                } else {
                    //凹 两个半圆
                    path.cubicTo(start.x, start.y + gap * flag, middle.x - gap * flag,
                            middle.y + r1 * flag, middle.x, middle.y + r1 * flag)

                    path.cubicTo(middle.x + gap * flag, middle.y + r1 * flag, end.x,
                            end.y + gap * flag, end.x, end.y)
                }
            }
        }
    }
2、绘制滑块阴影位置

通过如下操作进行滑块绘制,这里,我觉得可以使用高阶函数进行操作,但是如果那样的话,应该有部分人看的不是很明白了,所以还是繁琐一点比较好。

  private fun createCaptchaPath() {
        //随机生成 gap
        var gap = mRandom.nextInt(mSvcWidth / 2)
        // 宽度/3  获取更好展示效果
        gap = mSvcWidth / 3
        //随机产生 缺口部分左上角 x,y 坐标
        mCaptchaX = mRandom.nextInt(mWidth - mSvcWidth - gap)
        mCaptchaY = mRandom.nextInt(mHeight - mSvcHeight - gap)

        mCaptchaPath.apply {
            reset()
            lineTo(0f, 0f)
            //左上角开始 绘制一个不规则的阴影
            moveTo(mCaptchaX.toFloat(), mCaptchaY.toFloat())
            lineTo((mCaptchaX + gap).toFloat(), mCaptchaY.toFloat())
            //draw一个随机凹凸的圆
            SvcSizeUtils.drawPartCircle(PointF((mCaptchaX + gap).toFloat(), mCaptchaY.toFloat()),
                    PointF((mCaptchaX + gap * 2).toFloat(), mCaptchaY.toFloat()),
                    mCaptchaPath,
                    mRandom.nextBoolean())
            //右上角
            lineTo((mCaptchaX + mSvcWidth).toFloat(), mCaptchaY.toFloat())
            lineTo((mCaptchaX + mSvcWidth).toFloat(), (mCaptchaY + gap).toFloat())
            //draw一个随机凹凸的圆
            SvcSizeUtils.drawPartCircle(PointF((mCaptchaX + mSvcWidth).toFloat(),
                    (mCaptchaY + gap).toFloat()),
                    PointF((mCaptchaX + mSvcWidth).toFloat(), (mCaptchaY + gap * 2).toFloat()),
                    mCaptchaPath,
                    mRandom.nextBoolean())
            //右下角
            lineTo((mCaptchaX + mSvcWidth).toFloat(), (mCaptchaY + mSvcHeight).toFloat())
            lineTo((mCaptchaX + mSvcWidth - gap).toFloat(), (mCaptchaY + mSvcHeight).toFloat())
            //draw 一个随机的 凹凸圆
            SvcSizeUtils.drawPartCircle(PointF((mCaptchaX + mSvcWidth - gap).toFloat(),
                    (mCaptchaY + mSvcHeight).toFloat()),
                    PointF((mCaptchaX + mSvcWidth - gap * 2).toFloat(),
                            (mCaptchaY + mSvcHeight).toFloat()),
                    mCaptchaPath,
                    mRandom.nextBoolean())
            //左下角
            lineTo(mCaptchaX.toFloat(), (mCaptchaY + mSvcHeight).toFloat())
            lineTo(mCaptchaX.toFloat(), (mCaptchaY + mSvcHeight - gap).toFloat())
            //draw 一个随机的 凹凸圆
            SvcSizeUtils.drawPartCircle(PointF(mCaptchaX.toFloat(),
                    (mCaptchaY + mSvcHeight - gap).toFloat()),
                    PointF(mCaptchaX.toFloat(), (mCaptchaY + mSvcHeight - gap * 2).toFloat()),
                    mCaptchaPath,
                    mRandom.nextBoolean())
            close()
        }
    }
3、绘制滑块

这里就可以进行各种操作了,包括重新生成滑块,提示失败动画等等。并且这里需要将阴影目的位置的图片移动到起始点。

  /**
     * 生成滑块
     */
    private fun createMask() {
        mMaskBitmap = getMaskBitmap(drawable.toBitmap(), mCaptchaPath)
        //滑块阴影
        mMaskShadowBitmap = mMaskBitmap.extractAlpha()
        //重置拖动位移
        mSliderOffset = 0
        //绘制失败 标志闪烁动画
        isDrawMask = true
    }
  /**
     * 抠图操作
     */
    private fun getMaskBitmap(toBitmap: Bitmap, mCaptchaPath: Path): Bitmap {
        //按照控件的宽高 创建一个 bitmap
        val tempBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888)
        //将 创建 bitmap 作为画板
        val c = Canvas(tempBitmap)
        //抗锯齿
        c.drawFilter = PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
        //绘制用于遮罩的圆形
        c.drawPath(mCaptchaPath, mMaskPaint)
        //设置遮罩模式
        mMaskPaint.xfermode = mPorterDuffXmlFerMode
        //考虑 scaleType 等因素 ,要用 Matrix 对 Bitmap 进行缩放
        c.drawBitmap(toBitmap, imageMatrix, mMaskPaint)
        mMaskPaint.xfermode = null
        return tempBitmap
    }

二、绘制

在 draw内进行必要的绘制,相对简单

  override fun draw(canvas: Canvas?) {
        super.draw(canvas)
        //在 ImageView 上绘制验证码相关部分
        if (isMatchMode) {
            //绘制阴影部分
            canvas?.drawPath(mCaptchaPath, mPaint)
            //绘制滑块
            // isDrawMask  绘制失败闪烁动画用
            if (isDrawMask) {
                canvas?.apply {
                    // 先绘制阴影
                    drawBitmap(mMaskShadowBitmap,
                            (-mCaptchaX + mSliderOffset).toFloat(),
                            0f,
                            mMaskShadowPaint)

                    drawBitmap(mMaskBitmap, (-mCaptchaX + mSliderOffset).toFloat(), 0f, null)
                }
            }

            //验证成功 白光闪烁
            if (isSuccessShow) {
                canvas?.apply {
                    translate(mSuccessAnimOffset.toFloat(), 0f)
                    drawPath(mSuccessPath, mSuccessPaint)
                }
            }

        }
    }

剩下的就是一些动画,和基础的操作。不在详细说明了,可以去源码中查看;

三、引入操作

这是我第一次生成轮子,各位如果有需要可以引用下哈。

1、依赖引入
   implementation 'com.github.xiangshiweiyu:svc:1.0.3'
2、xml 配置
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <com.hxd.svc.SvcView
        android:id="@+id/sv_main"
        android:layout_width="match_parent"
        android:layout_height="140dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="100dp"
        android:layout_marginEnd="20dp"
        app:layout_constraintTop_toTopOf="parent" />

    <SeekBar
        android:id="@+id/sb_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="20dp"
        android:layout_marginTop="50dp"
        android:layout_marginEnd="20dp"
        android:thumb="@drawable/selector_button"
        app:layout_constraintTop_toBottomOf="@+id/sv_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
3、kt 文件编写
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        sv_main.setOnSvcVerificationListener(object : OnSvcVerificationListener {
            override fun onSuccess(svcView: SvcView) {
                sb_main.isEnabled = false
            }

            override fun onFailed(svcView: SvcView) {
                svcView.reSliderSize()
                sb_main.progress = 0
            }
        })


        sb_main.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
                sv_main.setSliderSize(p1)
            }

            override fun onStartTrackingTouch(p0: SeekBar?) {
                sb_main.max = sv_main.maxSliderSize()
            }

            override fun onStopTrackingTouch(p0: SeekBar?) {
                sv_main.checkCaptcha()
            }
        })

        Glide.with(this).asBitmap().load(R.mipmap.ic_bg)
                .into(object : SimpleTarget<Bitmap>() {

                    override fun onResourceReady(resource: Bitmap,
                                                 transition: Transition<in Bitmap>?) {
                        sv_main.setImageBitmap(resource)
                        sv_main.createCaptcha()
                    }
                })
    }
}
4、源码位置

源码位置

欢迎关注 码虫韩小呆

参考:大佬博客

上一篇下一篇

猜你喜欢

热点阅读