自定义view---强大的密码输入框
来源:互联网 发布:陕西软件测试培训 编辑:程序博客网 时间:2024/06/01 15:19
>>转自:rokudol博客
自定义view---强大的密码输入框
我司之前有个需求,要求类似支付宝那样的密码支付,产品要求输入的当前字符需要是明文密码,1s后转换为圆点,原本想网上那么多密码输入框,肯定没问题,结果UI一出图就懵逼了,翻遍了各个角落,都找不到类似的密码输入框,没办法,自己写吧。
使用方法,在gradle中添加:compile ‘com.rokudoll:PswText:1.0.0’即可使用
当然绘制思路参照了其他大佬的思路,言归正传,先来看看效果图:
需求分析
拿到效果图,再结合产品的要求,整理出以下要求:
1、类似EditText的password模式,输入密码时,输入的当前密码为明文,而之前的密码变为圆点,1s后当前密码也变为圆点
2、整体密码框为一个颜色,而已输入或当前输入密码位置的密码框为另一个颜色
3、有阴影
代码实现
自定义view的流程就不再赘述了,不太清楚的可以去看看鸿洋和郭林的教程,先写好自定义属性,按照之前分析的结果以及自己的一点想法,为了更方便的使用,就有了以下的自定义属性,目前没有使用枚举,全部都是以boolean值来规定使用哪种模式,不过之后会换成枚举,那么先来看看有哪些自定义属性:
attrs
设置好自定义属性后,就开始实现这个自定义控件了!首先肯定是计算宽高
onMeasure
作为一个数学很差的人,计算这个的过程确实是比较糟心的。。直接看图吧
图中说明两处:
1、spacingWidth:为什么在宽度已知的情况下,spacingWidth = borderWidth / 4:
很简单,因为用边框的宽度除以4得到的大小刚好是能让我接受的大小,且在pswLength = 7、8、9、10的时候,宽度也比较合适,说白了就是一点点凑出来的
2、borderWidth:为什么在宽度已知的情况下,borderWidth = (width 4) / ((5 pswLength) - 1),
这个其实很好理解,当宽度已知的时候,borderWidth = width / 6 - spacingWidth,即宽度除以6减去一个间隙的宽度就等于一个边框的宽度,而间隙的宽度 = borderWidth /4,那么我们换算成一个方程式来看看,就一目了然了,如果这个方程式还看不懂,那就去请教一下初中数学老师吧。。
来看看完整代码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpec = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpec = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpec == MeasureSpec.AT_MOST) { if (heightSpec != MeasureSpec.AT_MOST) {//高度已知但宽度未知时 spacingWidth = heightSize / 4; widthSize = (heightSize * pswLength) + (spacingWidth * (pswLength - 1)); borderWidth = heightSize; } else {//宽度,高度都未知时 widthSize = (borderWidth * pswLength) + (spacingWidth * (pswLength - 1)); heightSize = (int) (borderWidth + ((borderPaint.getStrokeWidth()) * 2)); } } else { //宽度已知但高度未知 if (heightSpec == MeasureSpec.AT_MOST) { borderWidth = (widthSize * 4) / (5 * pswLength); spacingWidth = borderWidth / 4; heightSize = (int) (borderWidth + ((borderPaint.getStrokeWidth()) * 2)); } } setMeasuredDimension(widthSize, heightSize); }
解释一下为什么高度要加上borderPaint.getStrokeWidth()2,很简单,如果直接取每个密码框的宽度作为高度的话,会出现密码框上下两根线绘制不完全的问题,因为画笔的宽度也是占了一定的大小的,所以我们在绘制高度时要留足够的高度来绘制整个view,为什么是2呢?因为上下两根线。。当然要加两次,所以是乘以2
按这种算法绘制出的view,其实右边还多了一个spacingWidth的宽度,因为我们给每一个边框都减去了一个spacingWidth的宽度,所以会多出一个spacingWidth的宽度,不过并不影响,我们可以在onDraw里把每一个密码框往右移0.5个spacingWidth的宽度,这样左右都空出了一小段空隙,视觉效果上也好看一点,直接来看看onDraw
onDraw
绘制pswLength数量个密码框
先说说思路,我们用for循环的方式,循环绘制出pswLength个密码框,用canvas.drawRoundRect绘制密码框,如果使用图片的话,就绘制图片,那么来看看canvas.drawRoundRect需要传递的参数:
drawRoundRect(RectF rect, float rx, float ry, Paint paint)
rect:RectF对象,边框的具体坐标
rx,ry:圆角x,y轴的半径
paint:画笔
那我们先来计算边框的具体坐标,来看看草稿:
没错,这草稿就是这么low,数学差。。就是这么心酸,不过已经可以看出规律,前面说过,我们是用for循环的方式循环绘制出各个密码框,那么总结一下:
top = 0 + (borderPaint.getStrokeWidth())
bottom = height - (borderPaint.getStrokeWidth())
left = i (borderWidth + spacingWidth)
right = borderWidth + i (borderWidth + spacingWidth)
= ((i + 1) borderWidth) + (i spacingWidth)
为什么top加上了borderPaint.getStrokeWidth(),而bottom又减去了borderPaint.getStrokeWidth()?
还记得我们在onMeasure处计算高度时,增加了borderPaint.getStrokeWidth()*2吗,我们的密码框在绘制时,为了把整个view往下移一个画笔宽度的位置,就需要在top出加上一个画笔宽度的大小。
而为什么bottom又要减去一个画笔宽度呢?仔细想想看,我们的bottom是用view的高度-一个画笔的宽度,而height是在边框的宽度基础上加上了两个画笔的宽度的,减去一个就正好是把整个view往下移了一个画笔的宽度
但是这样绘制出来的密码框,就像前面提过的,右边会多出一个spacingWidth,所以我们left和right的坐标需要再改进一下:
int left = (int) ( (i (borderWidth + spacingWidth)) + (0.5 spacingWidth));
int right = (int) (((i + 1) borderWidth) + (i spacingWidth) + (0.7 * spacingWidth));
有同学要问了,为毛right要乘以0.7而不是0.5,因为乘以0.5边框位置还是不对呀!微调一下,乘以0.7刚刚好。
到这里并没有结束,为啥?因为说好的可以用图片来绘制边框,我们还没有实现这一步,不过也很简单,不需要重新计算坐标,直接贴该部分的完整代码:
private void drawBorder(Canvas canvas, int height) { Bitmap bitmap = BitmapFactory.decodeResource(getResources(), borderImg); Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); for (int i = 0; i < pswLength; i++) { int left = (int) ( (i * (borderWidth + spacingWidth)) + (0.5 * spacingWidth)); int right = (int) (((i + 1) * borderWidth) + (i * spacingWidth) + (0.7 * spacingWidth)); if (isBorderImg) { Rect dst = new Rect(left, 0, right, height); canvas.drawBitmap(bitmap, src, dst, borderPaint); } else { borderRectF.set(left, 0, right, height); canvas.drawRoundRect(borderRectF, borderRadius, borderRadius, borderPaint); } } bitmap.recycle(); }
只绘制明文密码模式
这个很简单,就不多做解释了
private void drawText(Canvas canvas, String num, int i) { Rect mTextBound = new Rect(); pswTextPaint.getTextBounds(num, 0, num.length(), mTextBound); Paint.FontMetrics fontMetrics = pswTextPaint.getFontMetrics(); float textX = (float) ((i * (borderWidth + spacingWidth)) + (borderWidth / 2 - mTextBound.width() / 2) + (0.5 * spacingWidth)); float textY = (getMeasuredHeight() - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top; if (saveResult != 0 || saveResult < result.size()) { canvas.drawText(num, textX, textY, pswTextPaint); }}
说明一点,为什么在textX最后要加0.5*spacingWidth,因为我们绘制密码框的时候就多增加了这么多的宽度,所以绘制文字也要做同样的操作,同理,绘制圆点时也是一样,后面不多做赘述
只绘制圆点模式
for (int i = 0; i < result.size(); i++) { float circleX = (float) ((i * (borderWidth + spacingWidth)) + (borderWidth / 2) + (0.6 * spacingWidth)); float circleY = borderWidth / 2; int left = (int) (i * (borderWidth + spacingWidth) + (0.5 * spacingWidth)); int right = (int) (((i + 1) * borderWidth) + (i * spacingWidth) + (0.7 * spacingWidth)); drawBitmapOrBorder(canvas, left, right, height); canvas.drawCircle(circleX, circleY, dotRadius, pswDotPaint); }
绘制输入密码时密码框变换样式:
private void drawBitmapOrBorder(Canvas canvas, int left, int right, int height) { if (isBorderImg) { Bitmap bitmap = BitmapFactory.decodeResource(getResources(), inputBorderImg); Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); Rect dst = new Rect(left, 0, right, height); canvas.drawBitmap(bitmap, src, dst, inputBorderPaint); bitmap.recycle(); } else { borderRectF.set(left, 0, right, height); canvas.drawRoundRect(borderRectF, borderRadius, borderRadius, inputBorderPaint); }}
输入密码时,输入的当前密码为明文,1s后变为圆点模式,即默认模式
if (invalidated) { drawDelayCircle(canvas, height, dotRadius); return; } for (int i = 0; i < result.size(); i++) { //密码明文 String num = result.get(i) + ""; //圆点坐标 float circleX = (float) (((i - 1) * (borderWidth + spacingWidth)) + (borderWidth / 2) + (0.6 * spacingWidth)); float circleY = borderWidth / 2; //密码框坐标 int left = (int) (i * (borderWidth + spacingWidth) + (0.5 * spacingWidth)); int right = (int) (((i + 1) * borderWidth) + (i * spacingWidth) + (0.7 * spacingWidth)); drawBitmapOrBorder(canvas, left, right, height); drawText(canvas, num, i); //当输入位置 = 输入的长度时,即判断当前绘制的位置是否为当前密码位置,若是则延迟1s后绘制圆点 if (i + 1 == result.size()) { handler.sendEmptyMessageDelayed(1, delayTime); } //若按下back键保存的密码 > 输入的密码长度,则只绘制圆点 //即按下back键时,不绘制明文密码 if (!isShowTextPsw) { if (saveResult > result.size()) { canvas.drawCircle((float) ((i * (borderWidth + spacingWidth)) + (borderWidth / 2 + (0.6 * spacingWidth))), circleY, dotRadius, pswDotPaint); } } //当输入第二个密码时,才开始绘制圆点 if (i >= 1) { canvas.drawCircle(circleX, circleY, dotRadius, pswDotPaint); } } }
可能有同学注意到,这里我们计算的圆点坐标跟之前计算的不一样,为什么呢?因为在这里,我们计算的圆点坐标分为两种情况:
1、输入密码时,已输入的密码变为圆点,那么我们的圆点坐标就应该是用(i - 1)的方式去计算,这样才能实现输入到第二个密码时,第一个密码变为圆点
2、输入密码时,延迟1s后当前明文密码变为圆点,那么圆点坐标就应该是(i + 1)的方式计算
延迟绘制圆点
private void drawDelayCircle(Canvas canvas, int height, int dotRadius) { invalidated = false; for (int i = 0; i < result.size(); i++) { float circleX = (float) (((i - 1) * (borderWidth + spacingWidth)) + (borderWidth / 2) + (0.6 * spacingWidth)); float circleY = borderWidth / 2; int left = (int) (i * (borderWidth + spacingWidth) + (0.5 * spacingWidth)); int right = (int) (((i + 1) * borderWidth) + (i * spacingWidth) + (0.7 * spacingWidth)); canvas.drawCircle(circleX, circleY, dotRadius, pswDotPaint); drawBitmapOrBorder(canvas, left, right, height); } canvas.drawCircle((float) ((float) (((result.size() - 1) * (borderWidth + spacingWidth)) + (borderWidth / 2)) + (0.6 * spacingWidth)), borderWidth / 2, dotRadius, pswDotPaint); handler.removeMessages(1);}
以上就是全部的绘制逻辑,计算坐标这类似乎没什么好解释的,接下来就是自定义键盘,界面不需要我们自己再重新设计,用系统的就好,来看看代码
键盘
class NumKeyListener implements OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.isShiftPressed()) {//处理*#等键 return false; } if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {//只处理数字 if (result.size() < pswLength) { result.add(keyCode - 7); invalidate(); ensureFinishInput(); } return true; } if (keyCode == KeyEvent.KEYCODE_DEL) { if (!result.isEmpty()) {//不为空,删除最后一个 saveResult = result.size(); result.remove(result.size() - 1); invalidate(); } return true; } if (keyCode == KeyEvent.KEYCODE_ENTER) { ensureFinishInput(); return true; } } return false; }
说明一下只处理数字那部分,为什么是keyCode - 7,我们点进源码看看
可以看出,我们用keyCode - 7就刚好等于输入的数字
最终效果:
结语
以上就是该自定义view的全部说明,并未贴出全部的完整代码,如有需要可到GitHub查看完整源码,如果喜欢或觉得该控件对你有帮助还请点个star,文中如有错误还请指出,谢谢
- 自定义view---强大的密码输入框
- Android自定义View之密码输入框
- 自定义view之支付密码输入框
- 自定义view实现密码输入框
- 自定义view仿支付宝密码输入框
- Android自定义view,PasswordView,交易密码输入框
- 安卓/Android 模仿支付宝/微信 支付密码输入框的自定义View
- kotlin实现的简单个人账户管理APP(三) 自定义View仿支付宝的密码输入框/密码相关逻辑
- 自定义密码输入框
- 自定义密码输入框
- 自定义View之自定义支付宝密码输入控件
- 自定义的密码输入判断
- 自定义view,仿微信、支付宝密码输入控件的源码实现
- 自定义密码输入框,无圆角
- Android自定义密码输入框
- 自定义查看密码输入框
- 自定义输入支付密码框
- 自定义密码输入框(passwordEditText)
- Java线程池
- 控制文本输入框最多输入10个字符长度(即五个汉字)
- Safari避开百度云客户端下载大文件
- 虚拟机centos添加分辨率
- Spiral Matrix:旋转打印矩阵
- 自定义view---强大的密码输入框
- 获取表单内部元素的N种方法
- ORACLE sql 根据in查询里面数据的顺序进行排序 ORDER BY 自定义结果排序查询
- OSI七层协议
- Hadoop 2.6 使用Map Reduce实现矩阵相乘1 矩阵转置
- 记录 jvm 信息
- anaconda使用
- postgresql 的序列
- Ubuntu-10.10如何给用户添加sudo权限