前言
闲来无事做,于是就自己写了个仿魅蓝安全中心,检测时进度组件。首先啥也不说,贴图来吸引注意力。
呃。用的是模拟器进行录的屏,所以这个分辨率有点让我尴尬,效果不是很好。。。
正文
首先重写onMeasure方法进行测量这个组件的大小,贴下代码。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int resWidth = 0;
int resHeight = 0;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//宽高模式是否精度模式
if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
resWidth = getSuggestedMinimumWidth();
resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;
resHeight = getSuggestedMinimumHeight();
resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
} else {
resWidth = resHeight = Math.min(widthSize, heightSize);
}
//高是宽的1/2倍
setMeasuredDimension(resWidth, resHeight / 2);
}
流程就是通过穿过来的一个测量数据进行获取,这个组件的宽高的模式和宽高的大小,然后判断宽高是否为写死的数值。如果是写死的数值,那么就直接看高度和宽度的最小值。然后赋值给宽高。如果不是定死的数值,那么直接获取屏幕的宽度。使其高度等于宽度。最后调用setMeasuredDimension方法进行设置组件的宽高。在这里高度我取的是宽度的二分之一。
ok,我们来看下一键检测时那个跟雷达一样,逐渐扩散效果是怎么实现的。先说下思路,扩散,从上面的动图可以看出来,是两个圆一前一后的向外逐渐变大后逐渐消失,因此我们可以先绘制两个圆出来,然后不停的改变其半径的大小这样是不是就可以实现其扩散的效果了呢?
看代码:
/**
* 初始状态
*
* @param width 组件宽
* @param height 组件高
* @param canvas 画布
*/
public void initialState(int width, int height, Canvas canvas) {
String color = null;
//下面的判断是用于进行颜色逐渐透明
int colorSie = 60 - i;
if (colorSie < 10) {
color = "#0" + colorSie + "0FCA41";
} else {
color = "#" + colorSie + "0FCA41";
}
paint.setColor(Color.parseColor(color));
paint.setStrokeWidth(5);
//绘制两个逐渐变大的圆
canvas.drawCircle(width / 2, height / 2, width / 5 + i, paint);
canvas.drawCircle(width / 2, height / 2, width / 5 + j, paint);
j = j + 0.5f;
i++;
if (i > 60 || j > 30) {
i = 0;
j = 0;
}
//如果进行循环到一就让他阻塞一段时间,模拟呼吸节奏
if (i == 1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//绘制中间的圆
paint.setColor(Color.parseColor("#FF0FCA41"));
paint.setStrokeWidth(5);
canvas.drawCircle(width / 2, height / 2, width / 5, paint);
//圆中间的字
paint.setColor(Color.WHITE);
//字体大小,算法为整个宽度/5*2=圆的直径;圆的直径/2=字体总长度;字体总长度/4=每个字体大小;
int textSize = width / 5 * 2 / 2 / 4;
paint.setTextSize(textSize);
//整个宽度/5*2=圆的直径;圆的直径/4=距离圆左边顶点距离;然后整个宽度一半减去顶点距离就为X轴坐标
int x = width / 2 - width / 5 * 2 / 4;
//整个高度/2+字体高度的一半为Y轴坐标
int y = height / 2 + textSize / 2;
canvas.drawText("一键检测", x, y, paint);
//20毫秒刷新一次
postInvalidateDelayed(20);
}
从上面前几行代码可以看出,两个圆的颜色是逐渐透明的,半径也是逐渐变大的,然后大到一定程度就返回为0然后睡眠500毫秒再进行变大。从上面的代码可以看出最大的圆比中间高亮圆大了60,小的圆比高亮圆大了30.要想这两个圆在变大过程中不重合,那么增速是要一样的,因此大的每次绘制+1f,小的每次0.5f.通过调用postInvalidateDelayed(20);这个方法来进行没20毫秒进行刷新组件,重新绘制。
ok,这个向外扩散的功能是做完了。那么再来看下第二个界面,圆弧围绕着圆进行旋转,且尾巴是逐渐透明的。圆弧旋转有个很淡的轨迹。
实现思路,中间高亮圆和文字没什么变化,一个套路就是文字大小,位置有点变化,且文字动态改变,这没什么可说的把,改变的文字弄个变量就行了。
先说圆弧轨迹,跟上面功能一样,只是绘制了颜色比高亮圆淡一些,且比高亮圆半径大了20左右。
带着逐渐透明尾巴的旋转圆弧,实现思路,绘制多个圆弧且第一个末尾跟第二个开头衔接,如第一个圆弧从0度开始绘制20度,第二个圆弧则从20度绘制20度,第三个圆弧从40绘制20度。这样推下去。然后就动态的改变他们绘制的起始点就可以实现旋转了。
代码如下:
/**
* 进度状态
*
* @param width 组件宽
* @param height 组件高
* @param canvas 画布
*/
public void progressState(int width, int height, Canvas canvas) {
//绘制圆弧轨道
paint.setColor(Color.parseColor("#250FCA41"));
paint.setStrokeWidth(5);
canvas.drawCircle(width / 2, height / 2, width / 5 + 20, paint);
//绘制旋转的圆弧
paint.setStrokeWidth(5);
int xMin = width / 2 - width / 5 - 20;
int xMax = width / 2 + width / 5 + 20;
int yMin = height / 2 - width / 5 - 20;
int yMax = height / 2 + width / 5 + 20;
rectF.right = xMax;
rectF.left = xMin;
rectF.top = yMin;
rectF.bottom = yMax;
//绘制多个圆弧只用于颜色逐渐变淡
angle = angle + 10;
paint.setColor(Color.parseColor("#200FCA41"));
canvas.drawArc(rectF, angle, 20, true, paint);
paint.setColor(Color.parseColor("#350FCA41"));
canvas.drawArc(rectF, angle + 20, 20, true, paint);
paint.setColor(Color.parseColor("#500FCA41"));
canvas.drawArc(rectF, angle + 40, 20, true, paint);
paint.setColor(Color.parseColor("#650FCA41"));
canvas.drawArc(rectF, angle + 60, 20, true, paint);
if (angle > 360) {
angle = 0;
}
//绘制中间的高亮圆
paint.setColor(Color.parseColor("#FF0FCA41"));
paint.setStrokeWidth(5);
canvas.drawCircle(width / 2, height / 2, width / 5, paint);
//圆中间的字
paint.setColor(Color.WHITE);
//字体大小,算法为整个宽度/5*2=圆的直径;圆的直径/1=字体总长度;字体总长度/4=每个字体大小;
int textSize = width / 5 * 2 / 1 / 4;
paint.setTextSize(textSize);
String text = progress + "分";
//这个地方没办法算法太差了所以这样来适配下文字居中(别嘲笑我)
int len = text.length();
if (len == 4) {
len = 3;
} else {
len = 4;
}
//整个宽度/5*2=圆的直径;圆的直径/text.length()=距离圆左边顶点距离;然后整个宽度一半减去顶点距离就为X轴坐标
int x = width / 2 - width / 5 * 2 / len;
//整个高度/2+字体高度的一半为Y轴坐标
int y = height / 2 + textSize / 2;
canvas.drawText(text, x, y, paint);
//50毫秒刷新一次
postInvalidateDelayed(50);
}
从上面代码可以看出,我绘制了4个圆弧,刷新时间为50毫秒。
两个界面都给绘制好了,那么,上面动图点击进行切换界面的效果是怎么实现的呢?先来看下onDraw方法是怎么弄的吧。
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
//判断是否为进度状态
if (flag) {
progressState(width, height, canvas);
} else {
initialState(width, height, canvas);
}
}
从代码可以看出就是定义了个flag,进行判断绘制那个界面就可以了。很简单,但是这个点击事件可不简单了,因为,点击高亮圆以外不会响应点击事件,从高亮圆以外按下移动到高亮圆以内抬起也不会响应点击事件,点击高亮圆以内移动到高亮圆以外也不会响应点击事件。
因此,解决思路就来了,判断按下抬起时坐标位置也就可以实现了。是的,首先按下抬起的取值范围就是高亮圆的上下左右四个顶点所构成的矩形。这就是响应点击事件的范围。
代码比较有说服力,不那么空洞。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (flag) {
return false;
}
int action = event.getAction();
//判断是抬起还是按下,这样判断就能使如果是范围外按下到范围内就不会响应点击事件,如果是范围内按下,移除范围外也不会响应点击事件
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP) {
int width = getWidth();
int height = getHeight();
//组件中心点加减圆的半径就是点击范围
int xMin = width / 2 - width / 5;
int xMax = width / 2 + width / 5;
int yMin = height / 2 - width / 5;
int yMax = height / 2 + width / 5;
//按下时相对于组件原点的坐标
float x = event.getX();
float y = event.getY();
//判断在中间圆形范围内才响应点击事件否则不传递事件
if (xMax > x && x > xMin && yMax > y && y > yMin) {
//调用super使得点击事件会响应
super.dispatchTouchEvent(event);
//判断是否为抬起事件,此处判断是否为抬起事件,表示为可以响应点击事件了,因此,我们将重新绘制布局了
if (action == MotionEvent.ACTION_UP) {
flag = true;
}
//返回true表示事件可以向下传递,且会响应其他事件如抬起或者滑动事件
return true;
}
}
//返回false表示事件将不会向下传递,如按下后返回false。移动和抬起事件将不会被响应
return false;
}
流程,判断如果是在进度界面,那么点击事件不往下传递return false。如果是雷达界面,那么判断按下位置,和抬起位置是否在响应的范围内,如果按下不在范围内直接不传递事件return false,如果在范围内,判断是否是抬起事件,不是则不改变flag绘制进度界面且return true,即可以向下传递事件,即响应move和up事件。
那么判断抬起时候是否在范围内,如果不在,return false,如果在,再判断是否抬起事件,是抬起事件改变flag 进行改变成进度界面。
为什么要在这个地方加个是否是抬起事件呢?因为onclick响应事件是在抬起事件响应后面。
这里说明下在响应点击事件前一定要调用 super.dispatchTouchEvent(event);不然你不管return false还是true。都不会响应点击事件。
ok,主要功能都说完了,补全下代码吧。
//第一层扩散的圆的多余的半径
private int i = 0;
//第二层扩散的圆的多余的半径
private float j = 0;
//圆弧角度
private int angle = 0;
//画笔
private Paint paint;
//是否开始绘制进度条
private boolean flag = false;
//得分
private int progress = 100;
//绘制圆弧所需要存储矩形的上下左右坐标
private RectF rectF;
public MyView(Context context) {
super(context);
init();
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 获得默认该layout的尺寸
*
* @return
*/
private int getDefaultWidth() {
WindowManager wm = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
}
/**
* 初始化画笔
*/
private void init() {
paint = new Paint();
rectF = new RectF();
}
/**
* 设置进度值
*
* @param progress
*/
public void setProgress(int progress) {
this.progress = progress;
}
/**
* 关闭进度状态
*/
public void close() {
flag = false;
}
以上是全局变量,以及一些方法。复制以上全部代码可跑起来,此类继承的是View。