仿支付宝支付键盘

时间:2023-02-06 00:35:55

第一次拿到这个需求,第一个想法,各种控件嵌套+监听 解决问题。后来想想,这么个东西用这么多控件有点大材小用了,于是就自定义了。
前沿:由于大部分程序员的特性以及工作性质都属于拿来主义者。特此说明,本文章只提供解决思路和关键性代码,不会附带全部代码。

由于只是已Demo方式呈现,并不是一个成熟的自定义控件,好多属性都没有抽离出来,项目写死了。当然也好改。
第一步:构造。这个没什么可说的,在里面初始化一些东西。

 public PasswordView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(final Context context, AttributeSet attrs) {
//初始化画笔相关

int textColor = 0x66666666;
int forgetColor = 0xFF0000FF;
linePaint = PaintFactory.createAntAndDitherPaint(lineWidth, textColor);
//创建一个粗体TextPaint
textPaint = PaintFactory.createBoldTextPaint(lineWidth, textColor);
textPaint.setTextSize(50);
//创建一个忘记密码的Paint
forgetPasswordPaint = PaintFactory.createNormalTextPaint(lineWidth, forgetColor);
forgetPasswordPaint.setTextSize(30);
//输入框 外表 shape相关
inputRoundPaint = PaintFactory.createStrokeRoundPaint(lineWidth, textColor);

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordView);


a.recycle();
}

这个不是ViewGroup不需要去排版直接走onMeasure。处于测试阶段,按照作者的手机屏幕来的。实际情况,应当获取到具体值。如果你要问,作者为啥不写。答复:故意的。(不要查水表)。重点就那么几点,知道MeasureSpec的几种常量值,明白他是干嘛的。获取总高度,这里面肯定有坑,而且不少,我建议亲自尝试。我的坑并不一定是你的坑,你的坑我并不一定遇到,就是这样。

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (heightMeasureSpec == MeasureSpec.EXACTLY) {
//给width赋值
//尽量用width:match height:wrap
} else {
// WindowManagerager manager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
// Display display = manager.getDefaultDisplay();
// display.getSize(point);
// totalWidth = point.x;
// TODO: 2017/5/26 测试这么写 ,正式用上面4行代码
totalWidth = 720;

keyboardHeight = (totalWidth - lineWidth >> 1) / 3;
}
//getHeaderTotalHeight()+getKeyboardPreLineHeight()<<2
int measureHeight = (getKeyboardPreLineHeight() << 2) //键盘高度
+ getHeaderTotalHeight()//输入支付密码高度
+getPasswordTotalHeight()//密码总高度
+getTextHeight(forgetPasswordPaint)+forgetPasswordMarginBottom;//忘记密码总高度
Log.d("PasswordView", "===width:" + totalWidth + " height:" + measureHeight);
setMeasuredDimension(totalWidth, measureHeight);

}

那么大头来了,onDraw();这个方法是主要工作内容之一。主要要求熟练掌握 Rect RectF Canvas Paint 的api。Canvas有许多draw方法,如有不熟悉的,自行查询。
分析,功能
仿支付宝支付键盘
这是我们的功能界面。
简单分析一下,有哪些绘制点。
1、有一个标题。
2、密码显示区域。
3、忘记密码。
4、键盘输入界面。
对没有太多自定义控件绘制经验的人来说,drawText可能会比较蛋疼,特别是涉及到偏移量的计算,把握不好。
那么你只需要把你要绘制区域的4点坐标封装在Rect /RectF里然后 测量文字高度很容易得出一个正确的偏移量。
比如:你想绘制 Top 高度 0 Bottom 值 200,想要让文居中。
那么文字居中的计算方式是:
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
top+bottom-fontMetricsInt.bottom-fontMetricsInt.top>>1 的计算结果就是高度的偏移量。
下面就是绘制各种区域:

 @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//绘制顶部View
drawTopValue(canvas);
//绘制输入密码框
drawPasswordLine(canvas);
//绘制忘记密码
drawForgetPassword(canvas);
//绘制键盘
drawKeyboard(canvas);
}

其中绘制 键盘逻辑比较重要,我给出我的绘制思路,有更好的欢迎交流

/**
* 绘制键盘
*/

private KeyboardBean[] keyboards=new KeyboardBean[12];//存放keyboard数组
private Rect keyboardTotalArea;//键盘界面总大小
private void drawKeyboard(Canvas canvas) {
int startY=forgetRect.bottom+ forgetPasswordMarginBottom;
//绘制4条横线
for (int i=0;i<4;i++) {
int horizontalLineStarY=startY+getKeyboardPreLineHeight()*i;
canvas.drawLine(0,horizontalLineStarY,totalWidth,horizontalLineStarY+lineWidth,linePaint);
}
for (int i=0;i<2;i++) {
int x_offset =totalWidth/3*(i+1);
canvas.drawLine(x_offset,startY,x_offset+1,getHeight(),linePaint);
}
//将所有keyboard存入数组
if (keyboards[0] == null) {
String[] drawValue=new String[]{"1","2","3","4","5","6","7","8","9","","0","<--"};
int preWidth=totalWidth/3;
for (int i=0;i<12;i++) {
int startX=i%3*preWidth;
int endX=startX+preWidth;
int y_star=startY+i/3*(keyboardHeight+lineWidth);
int y_end=y_star+keyboardHeight+lineWidth;
Rect rect=new Rect(startX,y_star,endX,y_end);
boolean isFunction=false;
if (i == 9 | i == 11) {
isFunction=true;
}
keyboards[i]=new KeyboardBean(isFunction,rect,PaintFactory.createPaint(lineWidth),drawValue[i]);
}
keyboardTotalArea=new Rect(0,startY,totalWidth,getHeight());
}

//去绘制所有背景
for (KeyboardBean b:keyboards) {
Rect rect = b.getRect();
canvas.drawRect(rect, b.getBackgroundPaint());
canvas.drawText(
b.getDrawText(),
rect.centerX()-(measureText(textPaint,b.getDrawText())>>1),
getTextOffsetTop(textPaint,rect),
textPaint);
}
}
private int measureText(TextPaint paint, String value) {
return (int) paint.measureText(value);
}
/**
* 获取文本居中偏移量
*/

private int getTextOffsetTop(TextPaint paint,Rect targetRect) {
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
return targetRect.bottom+targetRect.top-fontMetricsInt.bottom-fontMetricsInt.top>>1;
}
/**
* 键盘管理实体
*/

private class KeyboardBean {
//在onDraw的时候会用到是否是按下状态会有不同颜色显示
private boolean isPressed =false;
//功能键只有2个 分别为 空白内容 和 删除
private boolean isFunctionKey=false;
//记录每一个item的绘制区域
private Rect rect;
//画笔
private Paint paint;
//绘制文字
private String drawText;
//构造

private KeyboardBean( boolean isFunctionKey, Rect rect, Paint paint,String drawText) {
this.isFunctionKey = isFunctionKey;
this.rect = rect;
this.paint = paint;
this.drawText=drawText;
}

//根据不同性质获取不同背景画笔
private Paint getBackgroundPaint() {
//按下状态同意设置为这个颜色
if (isPressed) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(0x6F666666);
} else {
//功能型按键设置这个颜色
if (isFunctionKey) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(0x6F999999);
} else {
//其他建设置为空白
paint.setColor(Color.TRANSPARENT);
}
}

return paint;
}

//其他一些set和get方法 不再额外添加注释

private Rect getRect() {
return rect;
}

public void setPressed(boolean pressed) {
isPressed = pressed;
}

private String getDrawText() {
return drawText;
}

@Override
public String toString() {
return "KeyboardBean{" +
"isPressed=" + isPressed +
", isFunctionKey=" + isFunctionKey +
", rect=" + rect +
", paint=" + paint +
", drawText='" + drawText + '\'' +
'}';
}
}

其他的请小伙伴自己根据自己的业务需求去做处理。
其实还有一个重头戏,事件处理。其实不客气的讲,让一个Android开发者自己设计一个OnClickListener的实现,大部分人是实现不了的,有的可能会有一些bug处理不到,当然我的也不一定正确,我没有看过关于onClickListener的实现原理代码,只是凭借我的想法去做这个事情,如果有不同意见,请交流。
先一点一点来。事件主要处理三种 down move 和up
我把这部分所有代码都先放出来

**
* 分别是 按下动作 x,y值 移动操作x,y值 抬起动作x,y值
*/
float[] touchData=new float[]{0,0,0,0,0,0};
//分别是 按下 移动 抬起 如果没有设置-1
private int defaultIndex=-1;
int[] keyboardIndex=new int[]{defaultIndex,defaultIndex,defaultIndex};
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下
touchData[0]=event.getX();
touchData[1]=event.getY();
onDownEvent();
break;
case MotionEvent.ACTION_UP:
//抬起
touchData[4]=event.getX();
touchData[5]=event.getY();

onUpEvent();
break;
case MotionEvent.ACTION_MOVE:
//移动
touchData[2]=event.getX();
touchData[3]=event.getY();

onMoveEvent();
break;

default:
break;
}
return true;
}
/**
* 按下事件响应
*/

private void onDownEvent() {
float x=touchData[0];
float y=touchData[1];
//判断区域,并且赋值
action=getAreaFlagByXY(x,y);

// if (isKeyboardArea(y)) {
//
// } else if (isForgetPasswordArea(x, y)) {
//
// }

if (action == ACTION_KEYBOARD) {
int index=getIndexByXY(x,y);
keyboards[index].setPressed(true);
keyboardIndex[0]=index;
invalidate();
}
}

/**
* 抬起动作
*/

private void onUpEvent() {
float x=touchData[4];
float y=touchData[5];
Log.i("up x y","===x:"+x+" y"+y);
//判断键盘区域
if (isKeyboardArea(y)&&action==ACTION_KEYBOARD) {
int index=getIndexByXY(x,y);
//记录抬起下标值
keyboardIndex[2]=index;
//按下下标跟抬起下标不相等,置缺省
if (keyboardIndex[0] != keyboardIndex[2]) {
keyboardIndex[0] = defaultIndex;

} else {
//判断是空白还有删除
if (index == 9 || index == 11) {
if (index == 11) {
//如果是删除就删除
deletePassword();
}//过滤掉是9 没有东西的情况
} else {
//如果不是删除就是增加,以下为增加逻辑
if (password.length() < passwordCount) {
addPassword(keyboards[index].getDrawText());
}

}
//设置Key对象为 抬起状态,刷新UI的时候用到
keyboards[index].setPressed(false);
//刷新
invalidate();
}

}
//校验 点击忘记密码区域
if (isForgetPasswordArea(x, y) && action == ACTION_FORGET_PASSWORD) {
if (mListener != null) {
mListener.onForgetPasswordClick();
}
}
//置为缺省值
action=ACTION_EMPTY_AREA;
}

//移动
private void onMoveEvent() {
float x=touchData[2];
float y=touchData[3];
if (action == ACTION_KEYBOARD) {
KeyboardBean bean= keyboards[keyboardIndex[0]];
if (!bean.getRect().contains((int) x, (int) y)) {
keyboards[keyboardIndex[0]].setPressed(false);
action=ACTION_EMPTY_AREA;
invalidate();
}
}
}

主要解决问题:抬起和按下的区域不是有效区域,比如 在键盘1按下,在键盘数字4抬起,那么这个触碰事件是无效的。移动过程中移出有效区域,比如按下是键盘数字1 移动到2了 那么此时,1的背景应该不是点击时候的背景,而是默认背景。在抬起动作相应点击事件。
就先写到这里吧。给同学们一个思路,遇到问题可以尝试着换种解决方式,我可以 放心大胆的讲:效率上,这种方式完爆 10几个控件去组合。虽然现在手机好看不出来。。。。。。