自定义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 //拼图缺块Bitmapprivate var piercedBlock: Bitmap? = nullprivate var thumbPaint: Paint? = null//绘制拼图滑块的画笔private var piercedPaint: Paint? = null//绘制拼图缺块的画笔private var startTouchTime: Long = 0 //滑动/触动开始时间private var looseTime: Long = 0 //滑动/触动松开时间private var blockSize = DEF_BLOCK_SIZEprivate var mTouchEnable = true //是否可触动private var callback: Callback? = nullprivate var mStrategy: CaptchaStrategy? = nullprivate var mMode = Captcha.MODE_BAR //Captcha验证模式private val xModeDstIn = PorterDuffXfermode(PorterDuff.Mode.DST_IN)private var isReversal = falseprivate var middlewarePaint: Paint? = Paint()private val srcRect = Rect()private val dstRect = RectF()override fun onDetachedFromWindow() {mStrategy?.onDetachedFromWindow()thumbBlock?.recycle()piercedBlock?.recycle()thumbBlock = nullthumbPaint = nullpiercedPositionInfo = nullthumbPositionInfo = nullcallback = nullpiercedPaint = nullmiddlewarePaint = nullsuper.onDetachedFromWindow()}interface Callback {fun onSuccess(time: Long)fun onFailed()}private var tempX = 0fprivate var tempY = 0fprivate var downX = 0fprivate var downY = 0finit {setCaptchaStrategy(DefaultCaptchaStrategy(context))}private fun initDrawElements() {// 创建缺省镂空位置piercedPositionInfo ?: mStrategy?.getPiercedPosition(width, height, blockSize)?.also {piercedPositionInfo = itthumbPositionInfo =mStrategy?.getThumbStartPosition(width, height, blockSize, mMode, it)}// 创建滑块thumbBlock ?: createBlockBitmap().apply {thumbBlock = this}}private fun getBlockWidth() = if (isReversal) blockSize.height else blockSize.widthprivate fun getBlockHeight() = if (isReversal) blockSize.width else blockSize.heightprivate 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 = trueval 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// 绘制底层模板dstcanvas.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()) ?: 0fcanvas.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_DOWNthumbPositionInfo?.left = progress / 100f * (width - getRealBlockWidth())invalidate()}}/*** 触动拼图块(触动模式)*/private fun downByTouch(x: Float, y: Float) {if (isEnabled) {mState = STATE_DOWNthumbPositionInfo?.run {left = x - getRealBlockWidth() / 2ftop = y - getRealBlockHeight() / 2f}startTouchTime = System.currentTimeMillis()invalidate()}}/*** 移动拼图缺块(滑动条模式)*/fun move(progress: Int) {if (isEnabled) {mState = STATE_MOVEthumbPositionInfo?.left = progress / 100f * (width - getRealBlockWidth())invalidate()}}/*** 触动拼图缺块(触动模式)*/private fun moveByTouch(offsetX: Float, offsetY: Float) {if (isEnabled) {mState = STATE_MOVEthumbPositionInfo?.run {left = (left + offsetX.toInt()).coerceAtMost(width - getRealBlockWidth())top = (top + offsetY.toInt()).coerceAtMost(height - getRealBlockHeight())}invalidate()}}/*** 松开*/fun loose() {if (isEnabled) {mState = STATE_LOOSENlooseTime = System.currentTimeMillis()checkAccess()invalidate()}}/*** 复位*/fun reset() {mState = STATE_IDELthumbPositionInfo = nullthumbBlock?.recycle()thumbBlock = nullpiercedBlock?.recycle()piercedBlock = nullisReversal = falsepiercedPositionInfo = nullinvalidate()}fun unAccess() {mState = STATE_UNACCESSinvalidate()}fun access() {mState = STATE_ACCESSinvalidate()}fun callback(callback: Callback?) {this.callback = callback}fun setCaptchaStrategy(strategy: CaptchaStrategy) {mStrategy = strategythumbPaint = strategy.thumbPaintpiercedPaint = strategy.piercedPaintsetLayerType(LAYER_TYPE_SOFTWARE, thumbPaint)if (!isInLayout) {invalidate()}}fun setBlockSize(size: SizeF) {blockSize = sizereset()}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 = modeisEnabled = truereset()}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.xval y = event.ywhen (event.action) {MotionEvent.ACTION_DOWN -> {downX = xdownY = ydownByTouch(x, y)}MotionEvent.ACTION_UP -> loose()MotionEvent.ACTION_MOVE -> {val offsetX = x - tempXval offsetY = y - tempYmoveByTouch(offsetX, offsetY)}}tempX = x:tempY = y}}return true}companion object {//状态码private const val STATE_DOWN = 1private const val STATE_MOVE = 2private const val STATE_LOOSEN = 3private const val STATE_IDEL = 4private const val STATE_ACCESS = 5private const val STATE_UNACCESS = 6internal 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().sizeval 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 = 0fval top: Floatval maxSize = blockSize.width.coerceAtLeast(blockSize.height).toInt()if (mode == Captcha.MODE_BAR) {top = thumbPosition.top} else {val random = Random()val size = maxSize + getThumbShadowInfo().sizeleft = (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: Paintget() = Paint().apply {isAntiAlias = true}override val piercedPaint: Paintget() = 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 {/*** 获取指定大小、指定旋转角度的图片*/@JvmStaticfun getBitmap(@DrawableRes resId: Int, size: SizeF, degrees: Int = 0): Bitmap {val options = BitmapFactory.Options()options.inMutable = trueval 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}}/*** 给图片添加阴影*/@JvmStaticfun 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 + wval dstHeight = srcBitmap.height + hval 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 = truepaint.color = info.colorpaint.maskFilter = filterpaint.isFilterBitmap = trueval 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