package com.example.arc.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.SweepGradient; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.View.MeasureSpec; import android.widget.Toast; /** * Android custom View AirConditionerView hacking * * 声明: * 本人在知乎看到Android非常漂亮的自定义View文章后,因为对其绘图部分运作机制好 * 奇,于是叫程梦真帮忙一起分析其中代码运作机制,花了两个半小时才将其整体hacking完。 * * 2016-1-1 深圳 南山平山村 曾剑锋 * * * 一、程序来源: * 1. 这种ui谁能给点思路? * https://m.zhihu.com/question/38598212#answer-26400956 * 2. github:mutexliu/ZhihuAnswer * https://github.com/mutexliu/ZhihuAnswer * * 二、参考文档: * 1. 为什么安卓android变量命名多以小写"m"开头 ? * http://www.01yun.com/mobile_development_question/20130303/194172.html * 2. 自定义View之onMeasure() * http://blog.csdn.net/pi9nc/article/details/18764863 * 3. MeasureSpec介绍及使用详解 * http://www.cnblogs.com/slider/archive/2011/11/28/2266538.html * 4. SweepGradient扫描渲染 * http://blog.csdn.net/q445697127/article/details/7867506 * 5. SweepGradient * http://developer.android.com/reference/android/graphics/SweepGradient.html * 6. 覆写onLayout进行layout,含自定义ViewGroup例子 * http://blog.csdn.net/androiddevelop/article/details/8108970 * 7. View.MeasureSpec * http://developer.android.com/reference/android/view/View.MeasureSpec.html#getMode(int) * * 三、绘图的三个步骤: * 1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec); * 2. protected void onLayout(boolean changed, int left, int top, int right, int bottom); * View中不需要实现。 * 3. protected void onDraw(Canvas canvas); * */ public class AirConditionerView extends View { // 设置当前的温度 // 这个值只能在16-28度之间,下面的private void checkTemperature(float t)方法中有限制 private float mTemperature = 24f; // 画圆的paint private Paint mArcPaint; // 画上线的paint private Paint mLinePaint; // 写字的paint private Paint mTextPaint; // 这里是将控件宽度分为600份,mMinSize代表其中一份 private float mMinSize; // 设置空心边框的宽度,其实就是圆弧的宽度 private float mGapWidth; // 内圆半径 private float mInnerRadius; // 外圆半径 private float mRadius; // 中线点 private float mCenter; // 圆弧矩形 private RectF mArcRect; // 渐变渲染 private SweepGradient mSweepGradient; // 接下来的三个构造函数是固定的写法,最后调用自己定义的的init方法 public AirConditionerView(Context context) { super(context, null); } public AirConditionerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AirConditionerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ // Paint.Style.FILL : 填充内部 // Paint.Style.FILL_AND_STROKE : 填充内部和描边 // Paint.Style.STROKE : 仅描边 // 画圆弧的笔 mArcPaint = new Paint(); mArcPaint.setStyle(Paint.Style.STROKE); // 设置抗锯齿 mArcPaint.setAntiAlias(true); // 画圆弧上的线的笔 mLinePaint = new Paint(); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setAntiAlias(true); // 设置笔的颜色 mLinePaint.setColor(0xffdddddd); // 写字的笔 mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); mTextPaint.setColor(0xff64646f); } private void initSize(){ //寬度 mGapWidth = 56*mMinSize; //内圆半径 mInnerRadius = 180*mMinSize; mCenter = 300*mMinSize; //外圆半径 mRadius = 208*mMinSize; // 包含圆弧的矩形 mArcRect = new RectF(mCenter-mRadius, mCenter-mRadius, mCenter+mRadius, mCenter+mRadius); // 设置渐变色、渐变位置 /** * A subclass of Shader that draws a sweep gradient around a center point. * * Parameters * cx The x-coordinate of the center * cy The y-coordinate of the center * colors The colors to be distributed between around the center. There must be at least 2 colors in the array. * positions May be NULL. The relative position of each corresponding color in the colors array, beginning with 0 and ending with 1.0. If the values are not monotonic, the drawing may produce unexpected results. If positions is NULL, then the colors are automatically spaced evenly. */ int[] colors = { 0xFFE5BD7D,0xFFFAAA64, 0xFFFFFFFF, 0xFF6AE2FD, 0xFF8CD0E5, 0xFFA3CBCB, 0xFFBDC7B3, 0xFFD1C299, 0xFFE5BD7D, }; float[] positions = {0,1f/8,2f/8,3f/8,4f/8,5f/8,6f/8,7f/8,1}; mSweepGradient = new SweepGradient(mCenter, mCenter, colors , positions); } @Override protected void onDraw(Canvas canvas) { /***************** 变色弧形部分 ************************/ // draw arc 設置空心邊框的寬度 // Set the width for stroking. Pass 0 to stroke in hairline mode. Hairlines always draws a single pixel independent of the canva's matrix. mArcPaint.setStrokeWidth(mGapWidth); int gapDegree = getDegree(); // 绘制梯度渐变 // Set or clear the shader object. mArcPaint.setShader(mSweepGradient); //画渐变色弧形 // Draw the specified arc, which will be scaled to fit inside the specified oval. // If the start angle is negative or >= 360, the start angle is treated as start angle modulo 360. // If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is negative, the sweep angle is treated as sweep angle modulo 360 // The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 degrees (3 o'clock on a watch.) // Parameters // oval The bounds of oval used to define the shape and size of the arc // startAngle Starting angle (in degrees) where the arc begins // sweepAngle Sweep angle (in degrees) measured clockwise // useCenter If true, include the center of the oval in the arc, and close it if it is being stroked. This will draw a wedge // paint The paint used to draw the arc canvas.drawArc(mArcRect, -225, gapDegree + 225, false, mArcPaint); /***************** 白色弧形部分 ************************/ mArcPaint.setShader(null); mArcPaint.setColor(Color.WHITE); //画渐变色弧形 canvas.drawArc(mArcRect, gapDegree, 45 - gapDegree, false, mArcPaint); // draw line /***************** 画线部分 ************************/ mLinePaint.setStrokeWidth(mMinSize*1.5f); // 将圆等分成120份,每份占360度的3度 for(int i = 0; i<120; i++){ // (75-45)*3 = 30*3 = 90,不绘直线部分占90度,正好符合空白区 if(i<=45 || i >= 75){ float top = mCenter-mInnerRadius-mGapWidth; // 2度分成15格 if(i%15 == 0){ top = top - 20*mMinSize; } // 绘制垂直线 canvas.drawLine(mCenter, mCenter - mInnerRadius, mCenter, top, mLinePaint); } /** * Preconcat the current matrix with the specified rotation. * * Parameters * degrees The amount to rotate, in degrees * px The x-coord for the pivot point (unchanged by the rotation) * py The y-coord for the pivot point (unchanged by the rotation) * * 坐标系旋转3度,不是已经绘制的图形旋转3度 */ canvas.rotate(3,mCenter,mCenter); } // draw text /***************** 弧形外部显示温度度数文字 部分 ************************/ mTextPaint.setTextSize(mMinSize*30); mTextPaint.setTextAlign(Paint.Align.CENTER); for(int i = 16; i<29; i+=2){ /** * 计算文字在圆弧边缘的位置,用到三角函数计算。 */ float r = mInnerRadius+mGapWidth + 40*mMinSize; float x = (float) (mCenter + r*Math.cos((26-i)/2*Math.PI/4)); float y = (float) (mCenter - r*Math.sin((26-i)/2*Math.PI/4)); canvas.drawText(""+i, x, y - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint); } } private int getDegree(){ checkTemperature(mTemperature); return -225 + (int)((mTemperature-16)/12*90+0.5f)*3; } /***************** 用来控制事件的三个方法 ************************/ private void checkTemperature(float t){ if(t<16 || t > 28){ throw new RuntimeException("Temperature out of range"); } } public void setTemperature(float t){ checkTemperature(t); mTemperature = t; // To force a view to draw, call invalidate(). invalidate(); } @Override public boolean onTouchEvent(MotionEvent event) { // 获取触摸位置 float x = event.getX(); float y = event.getY(); // 计算触摸点到中心点的距离,判断该点是否在圆弧之内 double distance = Math.sqrt((x - mCenter) * (x - mCenter) + (y - mCenter) * (y - mCenter)); if(distance < mInnerRadius || distance > mInnerRadius + mGapWidth){ return false; } // 判断是否在空白区 double degree = Math.atan2(-(y-mCenter),x-mCenter); if(-3*Math.PI/4<degree && degree < -Math.PI/4){ return false; } // 计算角度,并转换为对应的温度,设置当前温度,设置温度之后会自动对View进行重绘 if(degree < -3*Math.PI/4){ degree = degree + 2*Math.PI; } float t = (float) (26 - degree*8/Math.PI); setTemperature(t); return true; } /***************** 为了获得mMinSize ************************/ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int desiredWidth = Integer.MAX_VALUE; /** * Extracts the mode from the supplied measure specification. * * Parameters * measureSpec the measure specification to extract the mode from * Returns * UNSPECIFIED, AT_MOST or EXACTLY */ int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; //Measure Width if (widthMode == MeasureSpec.EXACTLY) { //Must be this size width = widthSize; //Toast.makeText(getContext(), "MeasureSpec.EXACTLY", 0).show(); } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); //Toast.makeText(getContext(), "MeasureSpec.AT_MOST"+desiredWidth+" "+widthSize, 0).show(); } else { //Be whatever you want width = desiredWidth; //Toast.makeText(getContext(), "MeasureSpec.UNSPECIFIED", 0).show(); } mMinSize = width/600f; int size = width; initSize(); //Measure Height if (heightMode == MeasureSpec.EXACTLY) { //Must be this size height = heightSize; } else if (heightMode == View.MeasureSpec.AT_MOST) { //Can't be bigger than... height = Math.min(size, heightSize); } else { //Be whatever you want height = size; } //MUST CALL THIS /** * This method must be called by onMeasure(int, int) to store the measured width and measured height. Failing to do so will trigger an exception at measurement time. * * Parameters * measuredWidth The measured width of this view. May be a complex bit mask as defined by MEASURED_SIZE_MASK and MEASURED_STATE_TOO_SMALL. * measuredHeight The measured height of this view. May be a complex bit mask as defined by MEASURED_SIZE_MASK and MEASURED_STATE_TOO_SMALL. */ setMeasuredDimension(width, height); } }