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、源码位置
欢迎关注 码虫韩小呆参考:大佬博客