自定义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