介绍
最近工作有一些闲暇时间,本身项目并不是自己的,看着控件挺有意思的,而且效果市面上比较少见.抱着学习态度,撸了这个View.
转载请标明出处:
http://blog.csdn.net/u012401802/article/details/78543322来自卡斯迪奥-北京的博客
github下载地址:https://github.com/liujiayu5566/AirConditionerView
看一下UI提供的效果
先看一下最终效果
代码实现
1.res/values/attr.xml
自定义属性: #
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CirqueView">
<attr name="temperature_min" format="integer">10</attr>
<attr name="temperature_max" format="integer">30</attr>
<attr name="time_left" format="integer">10</attr>
<attr name="time_right" format="integer">30</attr>
</declare-styleable>
</resources>
简单介绍一下:
temperature_min: 温度范围最小值.
temperature_max: 温度范围最大值.
time_left: 时间范围左侧数值.
time_right: 时间范围右侧数值.
CirqueView
2.获取自定义属性
public CirqueView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
initPaint();
startAnim();
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CirqueView, defStyleAttr, 0);
minTxt = ta.getInteger(R.styleable.CirqueView_temperature_min, 10);
maxTxt = ta.getInteger(R.styleable.CirqueView_temperature_max, 30);
timeMinTxt = ta.getInteger(R.styleable.CirqueView_time_left, 10);
timeMaxTxt = ta.getInteger(R.styleable.CirqueView_time_right, 30);
ta.recycle();
timeInitial = 165;
}
3.重写onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec));
}
private int measure(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//这样,当时用wrap_content时,View就获得一个默认值200px,而不是填充整个父布局。
result = DensityUtil.dip2px(context, 200);
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
很通用的代码.
4.重写onSizeChanged方法
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
radius = (getWidth() / 2) - DensityUtil.dip2px(context, 20);
oval.set(getWidth() / 2 - radius - defaultValue, getHeight() / 2 - radius - defaultValue,
getWidth() / 2 + radius + defaultValue, getHeight() / 2 + radius + defaultValue);
mSweepGradient = new SweepGradient(getWidth() / 2, getHeight() / 2, colors, floats);
cirque.setShader(mSweepGradient);
}
radius计算半径,oval.set()设置绘制的范围,设置渐变颜色.
5.重写onDraw方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//背景圆以及背景虚化
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius + DensityUtil.dip2px(context, 5), circularBackground);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, circularPaint);
//弧形背景
canvas.drawArc(oval, 195, 150, false, cirqueBackground);//小弧形
canvas.drawArc(oval, 15, 150, false, cirqueBackground);//小弧形
//温度绘制弧形
if (mCurrentAngle != 0) {
canvas.drawArc(oval, 195, mCurrentAngle, false, cirque);//小弧形
}
//时间绘制弧形
if (mTimeCurrentAngle != 0) {
canvas.drawArc(oval, 165, -mTimeCurrentAngle, false, timecirque);//小弧形
}
//文字以及图标绘制
int v = maxTxt - minTxt;
int timeV = timeMaxTxt - timeMinTxt;
text = Math.round(mCurrentAngle / (150f / v)) + minTxt + "℃";
timeText = Math.round(mTimeCurrentAngle / (150f / timeV)) + timeMinTxt + "min";
textPaint.setColor(colors[(int) (mCurrentAngle / (15 + 0.5f))]);
canvas.drawText(text, getWidth() / 2 - textPaint.measureText(text) / 2, getHeight() / 2 - DensityUtil.dip2px(context, 5), textPaint);
canvas.drawText(timeText, getWidth() / 2 - timeTextPaint.measureText(timeText) / 2, getHeight() / 2 + DensityUtil.dip2px(context, 20), timeTextPaint);
canvas.drawText(timeMinTxt + "", getWidth() / 2 - radius - defaultValue - timeTextPaint.measureText(timeMinTxt + "") * 1 / 3 + value, getHeight() / 2 + DensityUtil.dip2px(context, 17), unitPaint);
canvas.drawText(timeMaxTxt + "", getWidth() / 2 + radius + defaultValue - timeTextPaint.measureText(timeMaxTxt + "") * 1 / 3 - value, getHeight() / 2 + DensityUtil.dip2px(context, 17), unitPaint);
canvas.drawBitmap(snowflake, getWidth() / 2 - radius - defaultValue - snowflake.getWidth() / 2 + value, getHeight() / 2 - DensityUtil.dip2px(context, 17), bitmapPaint);
canvas.drawBitmap(sun, getWidth() / 2 + radius + defaultValue - sun.getWidth() / 2 - value, getHeight() / 2 - DensityUtil.dip2px(context, 17), bitmapPaint);
//刻度绘制竖线
linePaint.setColor(colors[(int) (mCurrentAngle / (15 + 0.5f))]);
canvas.rotate(mCurrentAngle + 15f, getWidth() / 2, getHeight() / 2);
canvas.drawLine(getWidth() / 2 - radius - defaultValue - DensityUtil.dip2px(context, 1), getHeight() / 2 - DensityUtil.dip2px(context, 1), getWidth() / 2 - radius * 3 / 4, getHeight() / 2, linePaint);
canvas.rotate(-mTimeCurrentAngle - 30f - mCurrentAngle, getWidth() / 2, getHeight() / 2);
canvas.drawLine(getWidth() / 2 - radius - defaultValue - DensityUtil.dip2px(context, 1), getHeight() / 2 + DensityUtil.dip2px(context, 1), getWidth() / 2 - radius * 3 / 4, getHeight() / 2, timeLinePaint);
if (txtFinishListener != null) { //监听
txtFinishListener.onFinish(text, timeText);
}
}
onDraw方法中,绘制整个View.
第5行根据UI图背景边缘有蓝色的虚化效果,绘制一个比背景大5dp的实体圆,通过Paint.setMaskFilter(new BlurMaskFilter(30, BlurMaskFilter.Blur.NORMAL))方法实现边缘虚幻.
第23/32行,计算温度展示以及圆弧垂直直线的颜色,加入0.5f防止越界.
6.重写onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
/*获取点击位置的坐标*/
float Action_x = event.getX();
float Action_y = event.getY();
/*根据坐标转换成对应的角度*/
float get_x0 = Action_x - getWidth() / 2;
float get_y0 = Action_y - getHeight() / 2;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (Action_y < getHeight() / 2) { //温度
tag = true;
} else {//时间
tag = false;
}
break;
case MotionEvent.ACTION_MOVE:
if (tag) { //温度
/*02:左上角区域*/
if (get_x0 <= 0 & get_y0 <= 0) {
float tan_x = get_x0 * (-1);
float tan_y = get_y0 * (-1);
double atan = Math.atan(tan_y / tan_x);
mCurrentAngle = (int) Math.toDegrees(atan);
}
/*03:右上角区域*/
if (get_x0 >= 0 & get_y0 <= 0) {
float tan_x = get_x0;
float tan_y = get_y0 * (-1);
double atan = Math.atan(tan_x / tan_y);
mCurrentAngle = (int) Math.toDegrees(atan) + 90f;
}
if (Math.abs(mCurrentAngle) <= 1 || (get_x0 <= 0 & get_y0 >= 0))
mCurrentAngle = 0;
if (Math.abs(mCurrentAngle) >= 149 || (get_x0 >= 0 & get_y0 >= 0))
mCurrentAngle = 150;
} else if (!tag) { //时间
/*01:左下角区域*/
if (get_x0 <= 0 & get_y0 >= 0) {
float tan_x = get_x0 * (-1);
float tan_y = get_y0;
double atan = Math.atan(tan_x / tan_y);
mTimeCurrentAngle = -(int) Math.toDegrees(atan) + 90f;
}
/*04:右下角区域*/
if (get_x0 >= 0 & get_y0 >= 0) {
float tan_x = get_x0;
float tan_y = get_y0;
double atan = Math.atan(tan_y / tan_x);
mTimeCurrentAngle = -(int) Math.toDegrees(atan) + 180f;
}
if (Math.abs(mTimeCurrentAngle) <= 1 || (get_x0 <= 0 & get_y0 <= 0))
mTimeCurrentAngle = 0;
if (Math.abs(mTimeCurrentAngle) >= 149 || (get_x0 >= 0 & get_y0 <= 0))
mTimeCurrentAngle = 150;
}
break;
case MotionEvent.ACTION_POINTER_UP:
break;
}
/*得到点的角度后进行重绘*/
invalidate();
return true;
}
onTouchEvent方法控制触摸事件.
但是原理很简单,简单看一下.
第4-8行:计算触摸点的位置,get_x0<0在触摸点控件左侧,反之在控件右侧.通过这种方式,将整个控件根据圆心分为4个区域.
第10行:根据按下操作,确定控制上下哪个方向.
第17行:计算滑动位置的角度.注意:滑动范围150°不能超出范围,以及滑动另一区域,也需要做对应处理.(34-37行,55-58行)
调用方法
CirqueView mCv = (CirqueView) findViewById(R.id.cv);
// mCv.setTemperaturemin(-30, 30); //设置温度范围 默认10-30
// mCv.setTime(0, 60); //设置时间范围 默认10-30
mCv.setDefault(27, 22); //添加默认数据--注:不能超出范围
mCv.setTxtFinishListener(new CirqueView.txtFinishListener() {
@Override
public void onFinish(String temperature, String time) {
Util.showToast(MainActivity.this, temperature + "//" + time);
}
});
回调返回String类型数值.
总结
完成时间大概是3天时间,之前对自定义View这块很薄弱,相关API记忆不是很深,查了一些相关博客,对自己有很大帮助.还是多撸撸代码成长很大(撸代码上班时间过的非常快^_^!).
相关博客:
http://blog.csdn.net/rhljiayou/article/details/7212620
http://blog.csdn.net/yanbober/article/details/50577855?locationNum=1&fps=1