炫酷的空调调节器控件 - AirConditionerView

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012401802/article/details/78543322

介绍

最近工作有一些闲暇时间,本身项目并不是自己的,看着控件挺有意思的,而且效果市面上比较少见.抱着学习态度,撸了这个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

猜你喜欢

转载自blog.csdn.net/u012401802/article/details/78543322