最近打算好好看一下自定义View这块的东西,刚好无意(毕竟AFK好久了)看到手机里的战网安全令牌,心想这东西既不难也不简单,刚好适合来练习一下。于是开始尝试写一个类似的东西。好吧,先看一下官方的:
先来分析一波:
首先一个大饼,(其实整个屏幕显示都可以做成一块儿,主要是背影图片与大饼的颜色关联,我这里单独提取出大饼来),然后是外圈的一个圆环,没有走过的进度显示为黑色,而走过的进度大致可以分成三种颜色(不知道是不是我色盲,感觉它这三种颜色应该是做了过度的)。我这里直接分为绿色、橙色、红色好了。要注意的是最后五秒的时候会有一个小圆里有5秒的倒计时。
先画饼:
```
//大圆半径
final int centerX=getWidth()/2,centerY=getWidth()/2;
final int radius=Math.min(centerX,centerY)-getPaddingLeft();
//画最大的实心圆
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#434343"));
canvas.drawCircle(centerX, centerY, radius, mPaint);
//画外圈圆环
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(20);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(centerX, centerY, radius -30, mPaint);
//画进度圆环
mProgressPaint.setAntiAlias(true);
mProgressPaint.setStrokeWidth(25);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setColor(mProgressColor);
RectF oval =new RectF(centerX - radius+30, centerY - radius+30, centerX + radius-30, centerY+ radius-30);
canvas.drawArc(oval, -90, mProgress, false, mProgressPaint);
```
这里的动画刚开始的时候我是用子线程通过sleep一定时间来更新mProgress,然后invalidate()方法进行刷新,后来觉得既然是动画那不是应该用Android自带的各种Animator嘛,使用Thread.sleep()会不会比较奇怪?然后果断换成了ValueAnimator,感觉效果挺好。
最后5秒显示小圆里的倒计时,这个一看就需要计算最后一段弧上的坐标参数了,简单的正弦余弦定理,不多说。因为我们的刷新周期是30秒,所以最后5秒的时候还有60度的圆环没走完。注意令数字居中显示即可。
```
//画倒计时5秒
if(mProgress>=300){
mProgressPaint.setStyle(Paint.Style.FILL);
int minRadius=getPaddingLeft();
double angle=Math.toRadians(360f-mProgress);
float minCenterX=(float) (centerX-(Math.sin(angle)*(radius-30)));
float minCenterY=(float) (centerY-(Math.cos(angle)*(radius-30)));
canvas.drawCircle(minCenterX,minCenterY,minRadius,mProgressPaint);
mPaint.setTextSize(DisplayUtil.sp2px(mContext, mTextSize)-30);
mPaint.setStrokeWidth(5);
int lastSecond=5;
Rect tempRect=new Rect();
mPaint.getTextBounds(lastSecond+"",0,1,tempRect);
lastSecond=(int)(360-mProgress)/12+1;
canvas.drawText(lastSecond+"",minCenterX,minCenterY+tempRect.height()/2,mPaint);
}
```
最后动态密码下面还有一个可以点击的字符串提示“复制密码”,它与上面的动态密码中间有一定的间隔,要注意的是这个间隔在点击事件上算在了“复制密码”这个字符串上面。当开始的时候我的Touch事件是严格限定在“复制密码”这个Rect上面的,但后来测试发现手指常常点不到?这不科学呀,我这个字体不比官方的小啊,为啥官方的从来没有这种情况。后来我仔细测试了一下官方的这个功能,发现我还是天真了,官方的并没有将touch事件严格限定在“复制密码”上面,原来是连上面的空隙一起来算的,我晕,这样的话就简单了。
```
mPaint.setTextSize(DisplayUtil.sp2px(mContext,16));
mPaint.getTextBounds(mCopyText,0,mCopyText.length(),mCopyRect);
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.BLUE);
canvas.drawText(mCopyText,centerX,centerY+mTextRect.height(),mPaint);
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
float x=motionEvent.getX(),y=motionEvent.getY();
Point p1=new Point(getWidth()/2-mCopyRect.width()/2,getHeight()/2);
Point p2=new Point(getWidth()/2+mCopyRect.width()/2,getHeight()/2+2*mCopyRect.height());
if(motionEvent.getAction()== MotionEvent.ACTION_UP) {
if (mState== State.START&&x>p1.x&&xp1.y&&y
ClipboardManagercm= (ClipboardManager)mContext.getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(null,mText));
Toast.makeText(mContext,mContext.getString(R.string.BallentVerifierView_copyover_test),Toast.LENGTH_SHORT).show();
}
}
return true;
}
```
来看一下效果:
下面那张图是开始的时候显示的界面,当有验证请求的话会自动切换到显示密码的状态。我们这里只是简单的做一下界面工作。
```
//画显示器
RectF rect=new RectF(centerX/3*2+centerX/12,centerY/3+centerY/12,centerX/3*4-centerX/12,centerY/3*2+centerY/12);
mPaint.setStrokeWidth(10);
mPaint.setColor(Color.BLUE);
canvas.drawRoundRect(rect,20,20,mPaint);
mPaint.setStrokeWidth(80);
canvas.drawLine(centerX,centerY/3*2+centerY/12,centerX,centerY/3*2+centerY/12*2,mPaint);
mPaint.setStrokeWidth(10);
canvas.drawLine(centerX-70,centerY/3*2+centerY/12*2,centerX+70,centerY/3*2+centerY/12*2,mPaint);
//画ZZZ
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(50);
mPaint.getTextBounds("Z Z Z", 0, "Z Z Z".length(), mTextRect);
if(mProgress<1){
canvas.drawText("Z ",centerX,centerY/3*2-mTextRect.height()/2,mPaint);
}else if(mProgress<2){
canvas.drawText("Z Z ",centerX,centerY/3*2-mTextRect.height()/2,mPaint);
}else{
canvas.drawText("Z Z Z",centerX,centerY/3*2-mTextRect.height()/2,mPaint);
}
//开始游戏吧
mPaint.setTextSize(60);
mPaint.setStrokeWidth(3);
mText=mContext.getString(R.string.BallentVerifierView_nologin_test);
mPaint.getTextBounds(mText,0,mText.length(),mTextRect);
canvas.drawText(mText,centerX,centerY+mTextRect.height(),mPaint);
int tempHeight=mTextRect.height();
mText=mContext.getString(R.string.BallentVerifierview_startgame_test);
mPaint.setTextSize(45);
mPaint.getTextBounds(mText,0,mText.length(),mTextRect);
canvas.drawText(mText,centerX,centerY+tempHeight+mTextRect.height()*2,mPaint);
```
其实在真实的应用场景中,这个view需要考虑的东西还有很多,比如30秒的周期,不可能每次进入密码界面都是从0度开始计时。它一定是需要与产生密码的服务器进行时间上的同步的。当然,这里可以优化的东西其实也有很多,比如界面刷新问题,毕竟它从来只是进行局部刷新,没有必要每次都让所有视图元素进行重绘,尽管这可能也浪费不了多少系统资源。
这个简单的自定义view做到这里其实也有一些其它的疑问,比如一般我们会不会尽量使用一个画笔,还是根据需要new出多个Paint,同样,在什么情况下我们会需要一块新的画布,还是说一般而言只要ondraw()里的canvas就足够了。在自定义view中,动画的制作一般采用Android自带的Animation还是根据需要简单的new一个Thread然后sleep接着invalidate()也可以,它们这间有没有明显的优劣呢。