具体代码地址:https://github.com/Wcuren/figure_password_layout
效果图如下:
主要思想:
1、自定义View,重写onDraw,onMeasure,这个自定义View用来显示单个输入的数字
2、自定义一个父布局继承LinearLayout用来容纳显示数字密码的自定义View
3、处理键盘输入的监听事件
下面是主要代码:
单个显示密码的自定义View
class NumberView: View{
val TAG = "NumberView"
val DEFAULT_SIZE = 40 //单个显示密码的自定义View的默认大小
var mContext: Context = context
var isInputState: Boolean = false
var isShowRemindLine: Boolean = false
var mDrawRemindLineState: Boolean = false
var isDrawText: Boolean = false
var mPaint: Paint = Paint()
var mShowPassType: Int = 0
var mPasswordText: String = ""
var mInputStateColor: Int = 0
var mNoInputStateColor: Int = 0
var mInputTextColor: Int = 0
var mRemindLineColor: Int = 0
var mTextSize: Int = 0
var mBoxLineSize: Int = 0
constructor(context: Context, attrs : AttributeSet?) : super(context, attrs) {
Log.d(TAG, "inited!")
}
/**
* 画边框
*/
fun drawInputBox(canvas: Canvas?) {
mPaint.reset()
if (isInputState) {
mPaint.color = ContextCompat.getColor(mContext, mInputStateColor)
} else {
mPaint.color = ContextCompat.getColor(mContext, mNoInputStateColor)
}
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = mBoxLineSize.toFloat()
mPaint.isAntiAlias = true
val rect = RectF(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat())
canvas?.drawRect(rect, mPaint)
}
/**
* 画光标(根据需要)
*/
fun drawRemindLine(canvas: Canvas?) {
mPaint.reset()
if (mDrawRemindLineState && isShowRemindLine) {
val lineHeight = measuredHeight / 2
mPaint.style = Paint.Style.FILL
mPaint.color = ContextCompat.getColor(mContext, mRemindLineColor)
canvas?.drawLine((measuredWidth / 2).toFloat(),
(measuredHeight / 2 - lineHeight / 2).toFloat(),
(measuredWidth / 2).toFloat(),
(measuredHeight / 2 + lineHeight / 2).toFloat(), mPaint)
}
}
/**
* 画密码,有三种选择,分别是:. 或者 * 或者直接显示数字
*/
fun drawPassword(canvas: Canvas?) {
if (isDrawText) {
mPaint.reset()
mPaint.color = ContextCompat.getColor(mContext, mInputTextColor)
mPaint.style = Paint.Style.FILL
mPaint.isAntiAlias = true
when (mShowPassType) {
0 // .
-> canvas?.drawCircle((measuredWidth / 2).toFloat(), (measuredHeight / 2).toFloat(), (measuredWidth / 4).toFloat(), mPaint)
1 // *
-> {
mPaint.textSize = (measuredWidth / 2 + 10).toFloat()
val strWidth = mPaint.measureText("*")
val baseY = measuredHeight / 2 - (mPaint.descent() + mPaint.ascent()) / 2 + strWidth / 3
val baseX = measuredWidth / 2 - strWidth / 2
canvas?.drawText("*", baseX, baseY, mPaint)
}
2 //figure
-> {
mPaint.textSize = mContext.resources.getDimensionPixelOffset(R.dimen.size_figure_pwd_text).toFloat()
val strWidth2 = mPaint.measureText(mPasswordText)
val baseY2 = measuredHeight / 2 - (mPaint.descent() + mPaint.ascent()) / 2 + strWidth2 / 5
val baseX2 = measuredWidth / 2 - strWidth2 / 2
canvas?.drawText(mPasswordText, baseX2, baseY2, mPaint)
}
}
}
}
override fun onDraw(canvas: Canvas?) {
Log.d(TAG, "onDraw!")
super.onDraw(canvas)
drawInputBox(canvas)
drawRemindLine(canvas)
drawPassword(canvas)
}
/**
* 计算自定义view的宽高尺寸,通过MeasureSpec辅助计算,MeasureSpec是size和mode通过位运算得到的一个整型值,
* 其中mode有三种值:UNSPECIFIED,EXACTLY,AT_MOST
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measureSize(widthMeasureSpec)
val height = measureSize(heightMeasureSpec)
setMeasuredDimension(width, height)
}
/**
* UNSPECIFIED:表示父布局对子view不限制大小
* EXACTLY:表示父view对子view的大小设置具体的值,一般layout_width和layout_height为match_parent或固定值时,这里的mode为EXACTLY
* AT_MOST:表示父view对子view的大小限定不能超过某个最大值,一般layout_width和layout_height为wrap_content时,这里的mode为AT_MOST
*/
fun measureSize(measureSpec: Int) : Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
when (mode) {
MeasureSpec.AT_MOST -> return DEFAULT_SIZE
MeasureSpec.EXACTLY -> return size
MeasureSpec.UNSPECIFIED -> return DEFAULT_SIZE
else -> {
return DEFAULT_SIZE
}
}
}
}
父布局代码:
class NumberLayout: LinearLayout {
val TAG = "NumberLayout"
val DEFAULT_SIZE = 0
var maxLength: Int = 0
var mIsShowInputLine: Boolean = false
var itemWidth: Int = 0
var itemHeight: Int = 0
var inputColor: Int = 0
var noinputColor: Int = 0
var lineColor: Int = 0
var txtInputColor: Int = 0
var interval: Int = 0
var txtSize: Int = 0
var boxLineSize: Int = 0
var drawType: Int = 0
var showPassType: Int = 0
var mFigureCursor: Int = -1
var mFigurePwdViews: MutableList = mutableListOf()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
Log.d(TAG, "NumberLayout inited!")
val types = context.obtainStyledAttributes(attrs, R.styleable.NumberLayoutStyle)
inputColor = types.getResourceId(R.styleable.NumberLayoutStyle_box_input_color, R.color.colorPrimary)
noinputColor = types.getResourceId(R.styleable.NumberLayoutStyle_box_no_input_color, R.color.text_color_99)
lineColor = types.getResourceId(R.styleable.NumberLayoutStyle_input_line_color, R.color.text_color_99)
txtInputColor = types.getResourceId(R.styleable.NumberLayoutStyle_text_input_color, R.color.black)
drawType= types.getInt(R.styleable.NumberLayoutStyle_box_draw_type, 0)
interval = types.getInt(R.styleable.NumberLayoutStyle_interval_width, 10)
maxLength = types.getInt(R.styleable.NumberLayoutStyle_pass_leng, 6)
itemWidth = types.getInt(R.styleable.NumberLayoutStyle_item_width, 40)
itemHeight = types.getInt(R.styleable.NumberLayoutStyle_item_height, 40)
showPassType = types.getInt(R.styleable.NumberLayoutStyle_pass_tips_type, 0)
txtSize = types.getInt(R.styleable.NumberLayoutStyle_draw_txt_size, 18)
boxLineSize = types.getInt(R.styleable.NumberLayoutStyle_draw_box_line_size, 4)
mIsShowInputLine = types.getBoolean(R.styleable.NumberLayoutStyle_is_show_input_line, true)
types.recycle()
initView(context)
}
fun initView(context: Context) {
mFigurePwdViews.clear()
for (i in 0 until maxLength) {
var view = NumberView(context,null)
var lp = LayoutParams(DensityUtil.dp2px(context, itemWidth.toFloat()),
DensityUtil.dp2px(context, itemHeight.toFloat()))
if (i != 0) {
lp.setMarginStart(DensityUtil.dp2px(context, interval.toFloat()))
}
view.mInputStateColor = inputColor
view.mNoInputStateColor = noinputColor
view.mInputTextColor = txtInputColor
view.mRemindLineColor = lineColor
view.mShowPassType = showPassType
view.mTextSize = txtSize
view.mBoxLineSize = boxLineSize
view.isShowRemindLine = mIsShowInputLine
mFigurePwdViews.add(view)
addView(view, lp)
}
setOnClickListener({layoutClicked()})
setOnKeyListener(CustomFigureKeyListener())
}
/**
* 点击弹出键盘
*/
private fun layoutClicked() {
setFocusable(true)
setFocusableInTouchMode(true)
requestFocus()
val inputMethodManager: InputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
/**
* 这个函数的作用是建立view与输入法的联系
*/
override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection {
outAttrs?.inputType = InputType.TYPE_CLASS_NUMBER //这一步是用来限定显示数字键盘
outAttrs?.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
return CustomInputConnection(this, true)
}
fun delItemPassword() {
if (mFigureCursor > -1) {
mFigurePwdViews.get(mFigureCursor).isDrawText = false
mFigurePwdViews.get(mFigureCursor).invalidate()
mFigureCursor --
mFigureChangeListener?.onDeleteFigure()
}
}
fun setItemPassword(password: String) {
if (mFigureCursor < (mFigurePwdViews.size - 1)) {
mFigureCursor++
mFigurePwdViews.get(mFigureCursor).isDrawText = true
mFigurePwdViews.get(mFigureCursor).mPasswordText = password
mFigurePwdViews.get(mFigureCursor).invalidate()
mFigureChangeListener?.onAddFigure(password)
}
}
/**
* 定义这个内部类的原因是:有的输入法在onkeyListener中无法监听到Delete这个键,所以在发送这个键的地方做监听处理
*/
inner class CustomInputConnection(targetView: View, fullEditor: Boolean):
BaseInputConnection(targetView, fullEditor) {
override fun sendKeyEvent(event: KeyEvent?): Boolean {
if (event?.action == KeyEvent.ACTION_DOWN &&
event?.keyCode == KeyEvent.KEYCODE_DEL) {
delItemPassword()
return true
}
return super.sendKeyEvent(event)
}
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
if (mFigureCursor > -1 ){
return sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
&& sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP,KeyEvent.KEYCODE_DEL))
} else {
return super.deleteSurroundingText(beforeLength, afterLength)
}
}
}
inner class CustomFigureKeyListener(): OnKeyListener {
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
if (event?.action == KeyEvent.ACTION_DOWN) {
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
setItemPassword((keyCode - 7).toString())
return true
} else if (keyCode == KeyEvent.KEYCODE_DEL) {
delItemPassword()
return true
} else if (keyCode == KeyEvent.KEYCODE_ENTER) {
mFigureChangeListener?.onEnter()
return true
}
}
return false
}
}
var mFigureChangeListener: FigureChangeListener ?= null
interface FigureChangeListener {
fun onAddFigure(figure: String)
fun onDeleteFigure()
fun onEnter()
}
}